redis慢?lettuce导致慢交易排查

410 阅读4分钟

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可能导致性能问题、数据被意外修改等问题。