redis慢?lettuce碰到的问题
redis慢了?一个登陆相关项目生产出现大量慢交易,检查日志看到是redis慢,让我一起看下。我们应用并发不会超过1百,redis配置应该完全满足的。首先怀疑是大key(目前使用场景确实有几M的大key),但简单的hincrby也慢了。我们的az1、az2是独立部署的,目前只有az2慢;第二天流量全切到az1也慢了。开发怀疑是redis问题,看到的是非常多交易变慢,关于redis我们也未做任何特殊配置。
- redis不慢
我对这个项目的整体部署架构不太了解,应用侧也没有有用的监控信息,先找DBA取下慢日志看看。最终确认,redis未记录任何慢日志(配置超过10ms就会记录),我们用的是腾讯的产品,它记录了非常多的慢日志,等待原厂专家来排查给下结论。
- 客户端问题
专家那边给的结论是,客户端收包队列慢导致的该问题。且每台机器连redis只有一个链接,批量命令共用一个包,有大key慢,所以hincrby命令也慢。客户端慢不应该所有机器都慢啊,通过DBA提供慢日志的ip和开发确认,多个微服务共用redis(设计不合理),只有部分微服务慢,甚至某微服务只有其中一台机器慢。这样基本确认是应用问题。
- shareNativeConnection配置
那么现在问题就是由于底层只有一个链接,导致简单命令也变慢了。我们使用的默认lettuce连接池,底层默认shareNativeConnection为true,共享链接,所以底层只有一个链接。写了简单单元测试(大key是10M,小key是个ccc字符串),测试shareNativeConnection参数的影响,以及换jedis客户端对性能的影响。
- 改写shareNativeConnection代码:
//在对应redisTemplate的地方配置该属性
public StringRedisTemplate stringRedisTemplate(LettuceConnectionFactory factory) {
factory.setShareNativeConnection(false);
StringRedisTemplate redisTemplate = new StringRedisTemplate(factory);
return redisTemplate;
}
- 换jedis方法:
spring-boot-starter-date-redis项目的2.X版本中,默认使用lettuce。更换jedis需要更改POM文件和连接池配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0
- 测试代码
大key是10M的字符串,小key是ccc的字符串。多线程内部循环get,统计get的平均耗时。关键代码如下
//大key相关代码
String key="testRedistemplate";
String val="";
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 1000000; i++) {
stringBuilder.append("aaaaabbbbb");
}
val = stringBuilder.toString();
stringRedisTemplate.opsForValue().set(key,val);
CountDownLatch countDownLatch =new CountDownLatch(bigKeyThreadNum*bigRunTimes);
AtomicLong bigSum = new AtomicLong(0);
for (int i = 0; i < bigKeyThreadNum; i++) {
int finalI = i;
Thread thread = new Thread() {
@Override
public void run() {
for (int j = 0; j < bigRunTimes; j++) {
long startTime = System.currentTimeMillis();
stringRedisTemplate.opsForValue().get(key);
long endTime = System.currentTimeMillis();
bigSum.addAndGet(endTime-startTime);
countDownLatch.countDown();
}
}
};
thread.setName("testBigKey");
thread.start();
}
//####################
//小key相关代码
String key2="testRedistemplate2";
String val2="ccc";
//同上多线程get
countDownLatch.await();
countDownLatch2.await();
//计算时间打印日志
- 测试结果
分别测试了ShareNativeConnection默认为true、false以及换jedis客户端三种场景。每个场景跑三次,分别是a.20个线程同时get小key; b.10个线程同时get大key; c.10个线程同时get大key,且20个线程同时get小key。每个线程固定get 100次,测试结果如下:
默认true
大key线程数[0],平均耗时[0]
小key线程数[20],平均耗时[2]
大key线程数[10],平均耗时[217]
小key线程数[0],平均耗时[0]
大key线程数[10],平均耗时[238]
小key线程数[20],平均耗时[220]
false:
大key线程数[0],平均耗时[0]
小key线程数[20],平均耗时[11]
大key线程数[10],平均耗时[157]
小key线程数[0],平均耗时[0]
大key线程数[10],平均耗时[176]
小key线程数[20],平均耗时[36]
jedis:
大key线程数[0],平均耗时[0]
小key线程数[20],平均耗时[1]
大key线程数[10],平均耗时[156]
小key线程数[0],平均耗时[0]
大key线程数[10],平均耗时[166]
小key线程数[20],平均耗时[15]
- 总结
能看到在低并发且有大key场景下,jedis的性能表现更好,shareNativeConnection为false时小key的get性能也有提升。lettuce底层用netty的异步IO,有大key的读写会影响简单key的耗时。避免类似问题,需要
* 规范redis的使用场景。避免存大key,redis只存必要数据
* redis也应做好隔离。在缓存场景下,微服务应尽量独立自治,共用redis可能导致性能问题、数据被意外修改等问题。