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

先了解下scan羡洛、hscan挂脑、sscan、zscan

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

keys 為啥不安全欲侮?

  • keys的操作會導致數據庫暫時被鎖住崭闲,其他的請求都會被堵塞;業(yè)務量大的時候會出問題

Spring RedisTemplate實現(xiàn)scan

1. hscan sscan zscan

  • 例子中的"field"是值redis的key锈麸,即從key為"field"中的hash中查找
  • redisTemplate的opsForHash镀脂,opsForSet,opsForZSet 可以 分別對應 sscan忘伞、hscan薄翅、zscan
  • 也可以使用 (JedisCommands) connection.getNativeConnection() 的 hscan、sscan氓奈、zscan 方法實現(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();
    }
    //關閉cursor
    cursor.close();
} catch (IOException e) {
    e.printStackTrace();
}
  • cursor.close(); 游標一定要關閉,不然連接會一直增長舀奶;可以使用client lists info clients info stats 命令查看客戶端連接狀態(tài)暑竟,會發(fā)現(xiàn)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);
}
  • 代碼雖然只是調用一次scan方法,但是spring-data-redis已經對scan做了封裝但荤,這個scan結合cursor.hasNext會多次redis scan罗岖,最終拿到所有match的結果

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.getNativeConnection腹躁;connection.getNativeConnection() 實際對象是Jedis(debug可以看出) 桑包,Jedis實現(xiàn)了很多接口
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); // 這個不是返回結果的數量纺非,應該是每次scan的數量
            ScanResult<String> scan = multiKeyCommands.scan("0", scanParams);
            while (null != scan.getStringCursor()) {
                keys.addAll(scan.getResult()); // 這一次scan match到的結果
                if (!StringUtils.equals("0", scan.getStringCursor())) { // 不斷拿著新的cursor scan哑了,最終會拿到所有匹配的值
                    scan = multiKeyCommands.scan(scan.getStringCursor(), scanParams);
                    continue;
                } else {
                    break;
                }
            }

            return keys;
        });
    }

發(fā)散思考

cursor沒有close,到底誰阻塞了烧颖,是 Redis 么

  • 測試過程中弱左,我基本只要發(fā)起十來個scan操作,沒有關閉cursor炕淮,接下來的請求都卡住了

redis側分析

  • client lists info clients info stats 查看
    發(fā)現(xiàn) 連接數 只有 十幾個拆火,也沒有阻塞和被拒絕的連接
  • 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 查看應用的堆棧信息
    發(fā)現(xiàn)很多 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)

綜上乘综,是應用側卡死

后續(xù)

  • 過了一個中午,redis client lists 顯示 scan 連接還在套硼,沒有釋放卡辰;應用線程也還是處于卡死狀態(tài)
  • 檢查 config get timeout,redis未設置超時時間邪意,可以用 config set timeout xxx 設置九妈,單位秒;但是設置了redis的超時雾鬼,redis釋放了連接萌朱,應用還是一樣卡住
1) "timeout"
2) "0"
  • netstat 查看和redis的連接,6333是redis端口策菜;連接從ESTABLISHED變成了CLOSE_WAIT晶疼;
  • jstack 和 原來表現(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 表示遠程計算器關閉連接又憨,正在等待socket連接的關閉
    和現(xiàn)象符合

  • 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. 修改后配置

    1. 配置方式一:部分接口已經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;
    }
    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-游標scan使用注意事項

如何使用RedisTemplate訪問Redis數據結構

Redis 中 Keys 與 Scan 的使用

深入理解Redis的scan命令

spring-boot-starter-redis配置詳解

線上大量CLOSE_WAIT原因排查

redis如何配置standAlone版的jedisPool

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末壁榕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赎瞎,更是在濱河造成了極大的恐慌牌里,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件务甥,死亡現(xiàn)場離奇詭異牡辽,居然都是意外死亡,警方通過查閱死者的電腦和手機敞临,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門态辛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挺尿,你說我怎么就攤上這事奏黑。” “怎么了编矾?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵熟史,是天一觀的道長。 經常有香客問我窄俏,道長蹂匹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任凹蜈,我火速辦了婚禮限寞,結果婚禮上,老公的妹妹穿的比我還像新娘踪区。我一直安慰自己昆烁,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布缎岗。 她就那樣靜靜地躺著静尼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鼠渺,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天鸭巴,我揣著相機與錄音,去河邊找鬼拦盹。 笑死鹃祖,一個胖子當著我的面吹牛,可吹牛的內容都是我干的普舆。 我是一名探鬼主播恬口,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沼侣!你這毒婦竟也來了祖能?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蛾洛,失蹤者是張志新(化名)和其女友劉穎养铸,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體轧膘,經...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡钞螟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谎碍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳞滨。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖椿浓,靈堂內的尸體忽然破棺而出太援,到底是詐尸還是另有隱情闽晦,我是刑警寧澤扳碍,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站仙蛉,受9級特大地震影響笋敞,放射性物質發(fā)生泄漏。R本人自食惡果不足惜荠瘪,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一夯巷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哀墓,春花似錦趁餐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春臀突,著一層夾襖步出監(jiān)牢的瞬間勉抓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工候学, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留藕筋,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓梳码,卻偏偏與公主長得像隐圾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掰茶,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348