keys * 這個指令千萬别在生産環境亂用。特别是資料龐大的情況下。因為Keys會引發Redis鎖,并且增加Redis的CPU占用。很多公司的運維都是禁止了這個指令的
當需要掃描key,比對出自己需要的key時,可以使用 scan 指令
keys *
這個指令千萬别在生産環境亂用。特别是資料龐大的情況下。因為Keys會引發Redis鎖,并且增加Redis的CPU占用。很多公司的運維都是禁止了這個指令的
當需要掃描key,比對出自己需要的key時,可以使用
scan
指令 scan
操作的Helper實作
scan
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查了一次
- 可以偷懶使用
,一下子全查回來;但是這樣子和 keys 有啥差別呢?搞笑臉 & 疑問臉.count(Integer.MAX_VALUE)
- 可以使用
的 hscan、sscan、zscan 方法實作cursor周遊,參照下文2.2章節(JedisCommands) connection.getNativeConnection()
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(); 遊标一定要關閉,不然連接配接會一直增長;可以使用
指令檢視用戶端連接配接狀态,會發現scan操作一直存在client lists``info clients``info stats
- 我們平時使用的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
實際對象是Jedis(debug可以看出) ,Jedis實作了很多接口connection.getNativeConnection()
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
檢視
發現 連接配接數 隻有 十幾個,也沒有阻塞和被拒絕的連接配接
-
查詢redis允許的最大連接配接數 是 10000config get maxclients
1) "maxclients"
2) "10000"`
-
在其他機器上也可以直接登入 操作redis-cli
綜上,redis本身沒有卡死
應用側分析
-
檢視和redis的連接配接,6333是redis端口;連接配接一直存在netstat
➜ ~ 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
顯示 scan 連接配接還在,沒有釋放;應用線程也還是處于卡死狀态client lists
- 檢查
,redis未設定逾時時間,可以用config get timeout
設定,機關秒;但是設定了redis的逾時,redis釋放了連接配接,應用還是一樣卡住config set timeout xxx
1) "timeout"
2) "0"
-
檢視和redis的連接配接,6333是redis端口;連接配接從ESTABLISHED變成了CLOSE_WAIT;netstat
-
和 原來表現一樣,卡在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連接配接池配置
根據上面
基本可以确定 redis 連接配接池的大小是 8 ;結合代碼配置,沒有指定的話,預設也确實是8netstat -an
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使用注意事項
如何使用RedisTemplate通路Redis資料結構
Redis 中 Keys 與 Scan 的使用
深入了解Redis的scan指令
spring-boot-starter-redis配置詳解
線上大量CLOSE_WAIT原因排查
redis如何配置standAlone版的jedisPool
一次jedis使用不規範,導緻redis用戶端close_wait大量增加的bug