1. 程式人生 > 程式設計 >在RedisTemplate中使用scan代替keys指令操作

在RedisTemplate中使用scan代替keys指令操作

keys * 這個命令千萬別在生產環境亂用。特別是資料龐大的情況下。因為Keys會引發Redis鎖,並且增加Redis的CPU佔用。很多公司的運維都是禁止了這個命令的

當需要掃描key,匹配出自己需要的key時,可以使用 scan 命令

scan操作的Helper實現

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisHelper {
 
 @Autowired
 private StringRedisTemplate stringRedisTemplate;
 
 /**
 * scan 實現
 * @param pattern 表示式
 * @param consumer 對迭代到的key進行操作
 */
 public void scan(String pattern,Consumer<byte[]> consumer) {
 this.stringRedisTemplate.execute((RedisConnection connection) -> {
 try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(Long.MAX_VALUE).match(pattern).build())) {
 cursor.forEachRemaining(consumer);
 return null;
 } catch (IOException e) {
 e.printStackTrace();
 throw new RuntimeException(e);
 }
 });
 }

 /**
 * 獲取符合條件的key
 * @param pattern 表示式
 * @return
 */
 public List<String> keys(String pattern) {
 List<String> keys = new ArrayList<>();
 this.scan(pattern,item -> {
 //符合條件的key
 String key = new String(item,StandardCharsets.UTF_8);
 keys.add(key);
 });
 return keys;
 }
}

但是會有一個問題:沒法移動cursor,也只能scan一次,並且容易導致redis連結報錯

先了解下scan、hscan、sscan、zscan

http://doc.redisfans.com/key/scan.html

keys 為啥不安全?

keys的操作會導致資料庫暫時被鎖住,其他的請求都會被堵塞;業務量大的時候會出問題

Spring RedisTemplate實現scan

1. hscan sscan zscan

例子中的"field"是值redis的key,即從key為"field"中的hash中查詢

redisTemplate的opsForHash,opsForSet,opsForZSet 可以 分別對應 sscan、hscan、zscan

當然這個網上的例子其實也不對,因為沒有拿著cursor遍歷,只scan查了一次

可以偷懶使用 .count(Integer.MAX_VALUE),一下子全查回來;但是這樣子和 keys 有啥區別呢?搞笑臉 & 疑問臉

可以使用 (JedisCommands) connection.getNativeConnection()的 hscan、sscan、zscan 方法實現cursor遍歷,參照下文2.2章節

try {
 Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash().scan("field",ScanOptions.scanOptions().match("*").count(1000).build());
 while (cursor.hasNext()) {
  Object key = cursor.next().getKey();
  Object valueSet = cursor.next().getValue();
 }
 //關閉cursor
 cursor.close();
} catch (IOException e) {
 e.printStackTrace();
}

cursor.close(); 遊標一定要關閉,不然連線會一直增長;可以使用client lists``info clients``info stats命令檢視客戶端連線狀態,會發現scan操作一直存在

我們平時使用的redisTemplate.execute 是會主動釋放連線的,可以檢視原始碼確認

client list
......
id=1531156 addr=xxx:55845 fd=8 name= age=80 idle=11 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=scan
......
org.springframework.data.redis.core.RedisTemplate#execute(org.springframework.data.redis.core.RedisCallback<T>,boolean,boolean)

finally {
 RedisConnectionUtils.releaseConnection(conn,factory);
}

2. scan

2.1 網上給的例子多半是這個

這個 connection.scan 沒法移動cursor,也只能scan一次

public Set<String> scan(String matchKey) {
 Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
  Set<String> keysTmp = new HashSet<>();
  Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("*" + matchKey + "*").count(1000).build());
  while (cursor.hasNext()) {
   keysTmp.add(new String(cursor.next()));
  }
  return keysTmp;
 });

 return keys;
}

2.2 使用 MultiKeyCommands

獲取 connection.getNativeConnection;connection.getNativeConnection()實際物件是Jedis(debug可以看出) ,Jedis實現了很多介面

public class Jedis extends BinaryJedis implements JedisCommands,MultiKeyCommands,AdvancedJedisCommands,ScriptingCommands,BasicCommands,ClusterCommands,SentinelCommands

當 scan.getStringCursor() 存在 且不是 0 的時候,一直移動遊標獲取

public Set<String> scan(String key) {
 return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
  Set<String> keys = Sets.newHashSet();

  JedisCommands commands = (JedisCommands) connection.getNativeConnection();
  MultiKeyCommands multiKeyCommands = (MultiKeyCommands) commands;

  ScanParams scanParams = new ScanParams();
  scanParams.match("*" + key + "*");
  scanParams.count(1000);
  ScanResult<String> scan = multiKeyCommands.scan("0",scanParams);
  while (null != scan.getStringCursor()) {
   keys.addAll(scan.getResult());
   if (!StringUtils.equals("0",scan.getStringCursor())) {
    scan = multiKeyCommands.scan(scan.getStringCursor(),scanParams);
    continue;
   } else {
    break;
   }
  }

  return keys;
 });
}

發散思考

cursor沒有close,到底誰阻塞了,是 Redis 麼

測試過程中,我基本只要發起十來個scan操作,沒有關閉cursor,接下來的請求都卡住了

redis側分析

client lists``info clients``info stats檢視

發現 連線數 只有 十幾個,也沒有阻塞和被拒絕的連線

config get maxclients查詢redis允許的最大連線數 是 10000

1) "maxclients"

2) "10000"`

redis-cli在其他機器上也可以直接登入 操作

綜上,redis本身沒有卡死

應用側分析

netstat檢視和redis的連線,6333是redis埠;連線一直存在

➜ ~ netstat -an | grep 6333
netstat -an | grep 6333
tcp4  0  0 xx.xx.xx.aa.52981  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52979  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52976  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52971  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52969  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52967  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52964  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52961  xx.xx.xx.bb.6333  ESTABLISHED

jstack檢視應用的堆疊資訊

發現很多 WAITING 的 執行緒,全都是在獲取redis連線

所以基本可以斷定是應用的redis執行緒池滿了

"http-nio-7007-exec-2" #139 daemon prio=5 os_prio=31 tid=0x00007fda36c1c000 nid=0xdd03 waiting on condition [0x00007000171ff000]
 java.lang.Thread.State: WAITING (parking)
  at sun.misc.Unsafe.park(Native Method)
  - parking to wait for <0x00000006c26ef560> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
  at org.apache.commons.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:590)
  at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:441)
  at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:362)
  at redis.clients.util.Pool.getResource(Pool.java:49)
  at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
  at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16)
  at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:276)
  at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:469)
  at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:132)
  at org.springframework.data.redis.core.RedisTemplate.executeWithStickyConnection(RedisTemplate.java:371)
  at org.springframework.data.redis.core.DefaultHashOperations.scan(DefaultHashOperations.java:244)

綜上,是應用側卡死

後續

過了一箇中午,redis client lists顯示 scan 連線還在,沒有釋放;應用執行緒也還是處於卡死狀態

檢查 config get timeout,redis未設定超時時間,可以用 config set timeout xxx設定,單位秒;但是設定了redis的超時,redis釋放了連線,應用還是一樣卡住

1) "timeout"

2) "0"

netstat檢視和redis的連線,6333是redis埠;連線從ESTABLISHED變成了CLOSE_WAIT;

jstack和 原來表現一樣,卡在JedisConnectionFactory.getConnection

➜ ~ netstat -an | grep 6333
netstat -an | grep 6333
tcp4  0  0 xx.xx.xx.aa.52981  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52979  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52976  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52971  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52969  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52967  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52964  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52961  xx.xx.xx.bb.6333  CLOSE_WAIT

回顧一下TCP四次揮手

ESTABLISHED 表示連線已被建立

CLOSE_WAIT 表示遠端計算器關閉連線,正在等待socket連線的關閉

和現象符合

redis連線池配置

根據上面 netstat -an基本可以確定 redis 連線池的大小是 8 ;結合程式碼配置,沒有指定的話,預設也確實是8

redis.clients.jedis.JedisPoolConfig
private int maxTotal = 8;
private int maxIdle = 8;
private int minIdle = 0;

如何配置更大的連線池呢?

A. 原配置

@Bean
public RedisConnectionFactory redisConnectionFactory() {
 RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
 redisStandaloneConfiguration.setHostName(redisHost);
 redisStandaloneConfiguration.setPort(redisPort);
 redisStandaloneConfiguration.setPassword(RedisPassword.of(redisPasswd));
 JedisConnectionFactory cf = new JedisConnectionFactory(redisStandaloneConfiguration);
 cf.afterPropertiesSet();
 return cf;
}
readTimeout,connectTimeout不指定,有預設值 2000 ms

org.springframework.data.redis.connection.jedis.JedisConnectionFactory.MutableJedisClientConfiguration
private Duration readTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
private Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); 

B. 修改後配置

配置方式一:部分介面已經Deprecated了

@Bean
public RedisConnectionFactory redisConnectionFactory() {
 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
 jedisPoolConfig.setMaxTotal(16); // --最多可以建立16個連線了
 jedisPoolConfig.setMaxWaitMillis(10000); // --10s獲取不到連線池的連線,
            // --直接報錯Could not get a resource from the pool

 jedisPoolConfig.setMaxIdle(16);
 jedisPoolConfig.setMinIdle(0);

 JedisConnectionFactory cf = new JedisConnectionFactory(jedisPoolConfig);
 cf.setHostName(redisHost); // -- @Deprecated 
 cf.setPort(redisPort); // -- @Deprecated 
 cf.setPassword(redisPasswd); // -- @Deprecated 
 cf.setTimeout(30000); // -- @Deprecated 貌似沒生效,30s超時,沒有關閉連線池的連線;
       // --redis沒有設定超時,會一直ESTABLISHED;redis設定了超時,且超時之後,會一直CLOSE_WAIT

 cf.afterPropertiesSet();
 return cf;
}

配置方式二:這是群裡好友給找的新的配置方式,效果一樣

RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(redisHost);
redisStandaloneConfiguration.setPort(redisPort);
redisStandaloneConfiguration.setPassword(RedisPassword.of(redisPasswd));

JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(16);
jedisPoolConfig.setMaxWaitMillis(10000);
jedisPoolConfig.setMaxIdle(16);
jedisPoolConfig.setMinIdle(0);

cf = new JedisConnectionFactory(redisStandaloneConfiguration,JedisClientConfiguration.builder()
  .readTimeout(Duration.ofSeconds(30))
  .connectTimeout(Duration.ofSeconds(30))
  .usePooling().poolConfig(jedisPoolConfig).build());

以上這篇在RedisTemplate中使用scan代替keys指令操作就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。