在RedisTemplate中使用scan代替keys指令操作
keys * 這個(gè)命令千萬別在生產(chǎn)環(huán)境亂用。特別是數(shù)據(jù)龐大的情況下。因?yàn)镵eys會(huì)引發(fā)Redis鎖,并且增加Redis的CPU占用。很多公司的運(yùn)維都是禁止了這個(gè)命令的
當(dāng)需要掃描key,匹配出自己需要的key時(shí),可以使用 scan 命令
scan操作的Helper實(shí)現(xiàn)
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 實(shí)現(xiàn) * @param pattern 表達(dá)式 * @param consumer 對(duì)迭代到的key進(jìn)行操作 */ 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 表達(dá)式 * @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; } }
但是會(huì)有一個(gè)問題:沒法移動(dòng)cursor,也只能scan一次,并且容易導(dǎo)致redis鏈接報(bào)錯(cuò)
先了解下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ì)出問題
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
當(dāng)然這個(gè)網(wǎng)上的例子其實(shí)也不對(duì),因?yàn)闆]有拿著cursor遍歷,只scan查了一次
可以偷懶使用 .count(Integer.MAX_VALUE),一下子全查回來;但是這樣子和 keys 有啥區(qū)別呢?搞笑臉 & 疑問臉
可以使用 (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()) { Object key = cursor.next().getKey(); Object valueSet = cursor.next().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); }
2. scan
2.1 網(wǎng)上給的例子多半是這個(gè)
這個(gè) connection.scan 沒法移動(dòng)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()實(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); 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; }); }
發(fā)散思考
cursor沒有close,到底誰阻塞了,是 Redis 么
測(cè)試過程中,我基本只要發(fā)起十來個(gè)scan操作,沒有關(guān)閉cursor,接下來的請(qǐng)求都卡住了
redis側(cè)分析
client lists``info clients``info stats查看
發(fā)現(xiàn) 連接數(shù) 只有 十幾個(gè),也沒有阻塞和被拒絕的連接
config get maxclients查詢r(jià)edis允許的最大連接數(shù) 是 10000
1) "maxclients"
2) "10000"`
redis-cli在其他機(jī)器上也可以直接登錄 操作
綜上,redis本身沒有卡死
應(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ù)
過了一個(gè)中午,redis client lists顯示 scan 連接還在,沒有釋放;應(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和 原來表現(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é)合代碼配置,沒有指定的話,默認(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. 修改后配置
配置方式一:部分接口已經(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 貌似沒生效,30s超時(shí),沒有關(guān)閉連接池的連接; // --redis沒有設(shè)置超時(shí),會(huì)一直ESTABLISHED;redis設(shè)置了超時(shí),且超時(shí)之后,會(huì)一直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指令操作就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring?多數(shù)據(jù)源方法級(jí)別注解實(shí)現(xiàn)過程
多數(shù)據(jù)源管理是Spring框架中非常重要的一部分,它可以提高應(yīng)用程序的靈活性和可靠性,從而更好地滿足業(yè)務(wù)需求,這篇文章主要介紹了Spring?多數(shù)據(jù)源方法級(jí)別注解實(shí)現(xiàn),需要的朋友可以參考下2023-07-07spring聲明式事務(wù)@Transactional底層工作原理
這篇文章主要為大家介紹分析spring聲明式事務(wù)@Transactional底層工作原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-02-02Java中八種基本數(shù)據(jù)類型的默認(rèn)值
這篇文章主要介紹了Java中八種基本數(shù)據(jù)類型的默認(rèn)值 的相關(guān)資料,需要的朋友可以參考下2016-07-07Spring使用Jackson實(shí)現(xiàn)轉(zhuǎn)換XML與Java對(duì)象
這篇文章主要為大家詳細(xì)介紹了Spring如何使用Jackson實(shí)現(xiàn)轉(zhuǎn)換XML與Java對(duì)象,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02java并發(fā)編程專題(四)----淺談(JUC)Lock鎖
這篇文章主要介紹了java并發(fā)編程(JUC)Lock鎖的相關(guān)內(nèi)容,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06Java OpenCV圖像處理之仿射變換,透視變換,旋轉(zhuǎn)詳解
這篇文章主要為大家詳細(xì)介紹了Java OpenCV圖像處理中仿射變換,透視變換,旋轉(zhuǎn)的實(shí)現(xiàn),文中的示例代碼講解詳細(xì),快跟隨小編一起學(xué)習(xí)一下2022-10-10