Redis scan 命令的一次坑

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: Redis scan 命令的一次坑

Redis 作为当前服务架构不可或缺的 Cache,其支持丰富多样的数据结构,Redis 在使用中其实也有很多坑,本次博主遇到的坑或许说是 Java 程序员会遇到的多一点,下面就听博主详细道来。

线上服务堵塞

String key = keyOf(appid);
int retryCount = 3;
int socketRetryCount = 3;
Exception ex = null;
while(retryCount > 0 && socketRetryCount > 0) {
    try {
        return redisDao.getMap(key);
    }catch (Exception e) {
    }
}

12 月 2 日被告知服务出现异常,查看日志发现其运行到上述代码 getMap 方法处后日志就没有内容了。

问题分析

"pool-13-thread-6" prio=10 tid=0x00007f754800e800 nid=0x71b5 waiting on condition [0x00007f758f0ee000]
    java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x0000000779b75f40> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
    at org.apache.commons.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:583)
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:442)
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
    at redis.clients.util.Pool.getResource(Pool.java:49)
    at redis.clients.jedis.JedisPool.getResource(JedisPool.java:99)
    at org.reborndb.reborn.RoundRobinJedisPool.getResource(RoundRobinJedisPool.java:300)
    at com.le.smartconnect.adapter.spring.RebornConnectionFactory.getConnection(RebornConnectionFactory.java:43)
    at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:128)
    at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:91)
    at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:78)
    at xxx.run(xxx.java:80)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
    Locked ownable synchronizers:
- <0x000000074f529b08> (a java.util.concurrent.ThreadPoolExecutor$Worker)

从线程日志可以看出服务堵塞在获取redis连接处.

分析:

  • 代码配置中 redis 最大连接为 3000
  • redis 配置中 session_max_timeout 为 0,即永不断开连接

一次修改分析

从以上两点分析得出,redis 连接被耗尽,于是查找代码得知由于重写 spring-data-redis 中的 hscan 方面导致,代码如下:

RedisConnection rc = redisTemplate.getConnectionFactory().getConnection();
if (rc instanceof JedisConnection) {
    JedisConnection JedisConnection = (JedisConnection) rc;
    return new ConvertingCursor<Map.Entry<byte[], byte[]>, Map.Entry<String, String>>(
            JedisConnection.hScan(rawValue(key), cursor, scanOptions),
            new Converter<Map.Entry<byte[], byte[]>, Map.Entry<String, String>>() {
            @Override
            public Entry<String, String> convert(final Entry<byte[], byte[]> source) {
                return new Map.Entry<String, String>() {
                @Override
                public String getKey() {
                    return hashKeySerializer.deserialize(source.getKey());
                }
                @Override
                public String getValue() {
                    return hashValueSerializer.deserialize(source.getValue());
                }
                @Override
                public String setValue(String value) {
                    throw new UnsupportedOperationException(
                        "Values cannot be set when scanning through entries.");
                }
            };
        }
    });
} else {
    return hashOps.scan(key, scanOptions);
}

上述代码返回 ConvertingCursor 后未释放连接,导出连接被占满。

二次修改分析

于是修改代码为正常释放连接

try {
    ...
}finally {
    RedisConnectionUtils.releaseConnection(rc, factory);
}

代码经过上线,再次跑程序查看线上日志发现报了大量的 Connection time out.

于是博主就思考是不是由于重写代码不对,尝试使用 spring-data-redis 的原生代码,即直接调用 hashOps.scan(key, scanOptions) 方法,再次上线。

上线后观察日志:发现这次不是报 Connection time out, 日志中大量报 Unknown reply: 错误。

分析如下:

由于代码是在多线程环境下运行,有几百个线程去调用 hscan 操作,spring-data-redis 原生的代码执行完一次 hscan 操作后就会关闭连接并返回一个迭代器 Cursor,但是遍历 Cursor 时在本次 count 后会再次根据游标重新使用该连接进行查询,可是连接却已经被关闭,这时会使用新的连接是可以正常迭代的,但是一旦复用到其他线程使用的连接则会导致报错 Unknown reply.

三次修改分析

经过思考后得出结论,redis 在执行 scan 操作时一旦连接被释放,那么 scan 操作将不会进行下去,则报 Connection time out.

查阅官方文档得出结论,redis 的 scan 操作需要 full iteration,即最优方式是一个连接将以此 scan 任务执行完全后释放该连接。

redis-scan-doc

修改代码如下:

RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = factory.getConnection();
if (rc instanceof JedisConnection) {
    JedisConnection JedisConnection = (JedisConnection) rc;
    Cursor<Map.Entry<String, String>> cursorResult = new ConvertingCursor<Map.Entry<byte[], byte[]>, Map.Entry<String, String>>(
            JedisConnection.hScan(rawValue(key), cursor, scanOptions),
            new Converter<Map.Entry<byte[], byte[]>, Map.Entry<String, String>>() {
            ...
            });
return new ScanResult<Map.Entry<String, String>>(cursorResult, factory, rc);}
public void releaseConnection() throws IOException{
    IOException ex = null;
    if(cursor != null) {
        try {
            cursor.close();
        } catch (IOException e) {
            ex = e;
        }
    }
    try {
        RedisConnectionUtils.releaseConnection(rc, factory);
    } catch (Exception e) {
    }
    if(ex != null) {
        throw ex;
    }
}

将连接返回给业务代码,并在业务代码执行完毕后将连接释放,问题解决。

总结

  1. 连接一旦开启就必须释放,否则造成内存泄漏或服务堵塞不可用
  2. 重写代码时需要谨记仔细查阅官方文档给出的方案并实施
  3. 多线程下使用 redis 的 scan 操作需要使用一个连接遍历完 Cursor,而不能复用连接,否则导致报错 Unknown reply.
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
NoSQL Java Redis
分布式锁实现原理问题之使用Redis的setNx命令来实现分布式锁问题如何解决
分布式锁实现原理问题之使用Redis的setNx命令来实现分布式锁问题如何解决
|
5天前
|
NoSQL Redis 数据库
12)Redis 的游标迭代器(scan)
12)Redis 的游标迭代器(scan)
12 1
|
14天前
|
监控 NoSQL Redis
redis-server --service-install redis.windows.conf --loglevel verbose 命令的作用是什么?
redis-server --service-install redis.windows.conf --loglevel verbose 命令的作用是什么?
27 3
|
5天前
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
13 0
|
2月前
|
存储 消息中间件 NoSQL
Redis命令详解以及存储原理
Redis命令详解以及存储原理
|
3月前
|
存储 缓存 NoSQL
Redis 全局通用命令整理
Redis 全局通用命令整理
60 10
|
2月前
|
SQL 缓存 NoSQL
【Azure Redis 缓存】使用Azure Redis服务时候,如突然遇见异常,遇见命令Timeout performing SET xxxxxx等情况,如何第一时间查看是否有Failover存在呢?
【Azure Redis 缓存】使用Azure Redis服务时候,如突然遇见异常,遇见命令Timeout performing SET xxxxxx等情况,如何第一时间查看是否有Failover存在呢?
|
2月前
|
缓存 监控 NoSQL
【Azure Redis 缓存】Azure Redis出现了超时问题后,记录一步一步的排查出异常的客户端连接和所执行命令的步骤
【Azure Redis 缓存】Azure Redis出现了超时问题后,记录一步一步的排查出异常的客户端连接和所执行命令的步骤
|
3月前
|
存储 JSON NoSQL
Redis常见命令大全
【7月更文挑战第30天】
21 3
|
2月前
|
缓存 NoSQL 测试技术
【Azure Redis 缓存 Azure Cache For Redis】使用Redis自带redis-benchmark.exe命令测试Azure Redis的性能
【Azure Redis 缓存 Azure Cache For Redis】使用Redis自带redis-benchmark.exe命令测试Azure Redis的性能
下一篇
无影云桌面