Spring RedisTemplate實(shí)現(xiàn)scan操作,畢竟keys不安全

先了解下scan、hscan、sscan、zscan

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

keys 為啥不安全?

  • keys的操作會(huì)導(dǎo)致數(shù)據(jù)庫(kù)暫時(shí)被鎖住,其他的請(qǐng)求都會(huì)被堵塞;業(yè)務(wù)量大的時(shí)候會(huì)出問(wèn)題

Spring RedisTemplate實(shí)現(xiàn)scan

1. hscan sscan zscan

  • 例子中的"field"是值redis的key,即從key為"field"中的hash中查找
  • redisTemplate的opsForHash,opsForSet,opsForZSet 可以 分別對(duì)應(yīng) sscan、hscan、zscan
  • 也可以使用 (JedisCommands) connection.getNativeConnection() 的 hscan、sscan、zscan 方法實(shí)現(xiàn)cursor遍歷,參照下文2.2章節(jié)
try {
    Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash().scan("field",
    ScanOptions.scanOptions().match("*").count(1000).build());
    while (cursor.hasNext()) {
        Map.Entry<Object,Object> entry = cursor.next();
        Object key = entry.getKey();
        Object valueSet = entry.getValue();
    }
    //關(guān)閉cursor
    cursor.close();
} catch (IOException e) {
    e.printStackTrace();
}
  • cursor.close(); 游標(biāo)一定要關(guān)閉,不然連接會(huì)一直增長(zhǎng);可以使用client lists info clients info stats 命令查看客戶端連接狀態(tài),會(huì)發(fā)現(xiàn)scan操作一直存在
  • 我們平時(shí)使用的redisTemplate.execute 是會(huì)主動(dòng)釋放連接的,可以查看源碼確認(rèn)
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);
}
  • 代碼雖然只是調(diào)用一次scan方法,但是spring-data-redis已經(jīng)對(duì)scan做了封裝,這個(gè)scan結(jié)合cursor.hasNext會(huì)多次redis scan,最終拿到所有match的結(jié)果

2. scan

2.1 使用spring-data-redis封裝好的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 使用redis.clients.jedis的MultiKeyCommands,自己循環(huán)scan

  • 獲取 connection.getNativeConnectionconnection.getNativeConnection() 實(shí)際對(duì)象是Jedis(debug可以看出) ,Jedis實(shí)現(xiàn)了很多接口
public class Jedis extends BinaryJedis implements JedisCommands, MultiKeyCommands,
    AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands 
  • 當(dāng) scan.getStringCursor() 存在 且不是 0 的時(shí)候,一直移動(dòng)游標(biāo)獲取
    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); // 這個(gè)不是返回結(jié)果的數(shù)量,應(yīng)該是每次scan的數(shù)量
            ScanResult<String> scan = multiKeyCommands.scan("0", scanParams);
            while (null != scan.getStringCursor()) {
                keys.addAll(scan.getResult()); // 這一次scan match到的結(jié)果
                if (!StringUtils.equals("0", scan.getStringCursor())) { // 不斷拿著新的cursor scan,最終會(huì)拿到所有匹配的值
                    scan = multiKeyCommands.scan(scan.getStringCursor(), scanParams);
                    continue;
                } else {
                    break;
                }
            }

            return keys;
        });
    }

發(fā)散思考

cursor沒(méi)有close,到底誰(shuí)阻塞了,是 Redis 么

  • 測(cè)試過(guò)程中,我基本只要發(fā)起十來(lái)個(gè)scan操作,沒(méi)有關(guān)閉cursor,接下來(lái)的請(qǐng)求都卡住了

redis側(cè)分析

  • client lists info clients info stats 查看
    發(fā)現(xiàn) 連接數(shù) 只有 十幾個(gè),也沒(méi)有阻塞和被拒絕的連接
  • config get maxclients 查詢r(jià)edis允許的最大連接數(shù) 是 10000
1) "maxclients"
2) "10000"`
  • redis-cli 在其他機(jī)器上也可以直接登錄 操作

綜上,redis本身沒(méi)有卡死

應(yīng)用側(cè)分析

  • 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 查看應(yīng)用的堆棧信息
    發(fā)現(xiàn)很多 WAITING 的 線程,全都是在獲取redis連接
    所以基本可以斷定是應(yīng)用的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)

綜上,是應(yīng)用側(cè)卡死

后續(xù)

  • 過(guò)了一個(gè)中午,redis client lists 顯示 scan 連接還在,沒(méi)有釋放;應(yīng)用線程也還是處于卡死狀態(tài)
  • 檢查 config get timeout,redis未設(shè)置超時(shí)時(shí)間,可以用 config set timeout xxx 設(shè)置,單位秒;但是設(shè)置了redis的超時(shí),redis釋放了連接,應(yīng)用還是一樣卡住
1) "timeout"
2) "0"
  • netstat 查看和redis的連接,6333是redis端口;連接從ESTABLISHED變成了CLOSE_WAIT;
  • jstack 和 原來(lái)表現(xiàn)一樣,卡在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 表示遠(yuǎn)程計(jì)算器關(guān)閉連接,正在等待socket連接的關(guān)閉
    和現(xiàn)象符合

  • redis連接池配置
    根據(jù)上面 netstat -an 基本可以確定 redis 連接池的大小是 8 ;結(jié)合代碼配置,沒(méi)有指定的話,默認(rèn)也確實(shí)是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不指定,有默認(rèn)值 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. 修改后配置

    1. 配置方式一:部分接口已經(jīng)Deprecated了
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(16); // --最多可以建立16個(gè)連接了
        jedisPoolConfig.setMaxWaitMillis(10000); // --10s獲取不到連接池的連接,
                                                 // --直接報(bào)錯(cuò)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 貌似沒(méi)生效,30s超時(shí),沒(méi)有關(guān)閉連接池的連接;
                              // --redis沒(méi)有設(shè)置超時(shí),會(huì)一直ESTABLISHED;redis設(shè)置了超時(shí),且超時(shí)之后,會(huì)一直CLOSE_WAIT

        cf.afterPropertiesSet();
        return cf;
    }
    1. 配置方式二:這是群里好友給找的新的配置方式,效果一樣
            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());

Standalone Sentinel Cluster區(qū)別

待更新

參考

redistemplate-游標(biāo)scan使用注意事項(xiàng)

如何使用RedisTemplate訪問(wèn)Redis數(shù)據(jù)結(jié)構(gòu)

Redis 中 Keys 與 Scan 的使用

深入理解Redis的scan命令

spring-boot-starter-redis配置詳解

線上大量CLOSE_WAIT原因排查

redis如何配置standAlone版的jedisPool

一次jedis使用不規(guī)范,導(dǎo)致redis客戶端close_wait大量增加的bug

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容