Redis scan 命令的一次坑

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 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 Redis 数据库
10- 你们用过Redis的事务吗 ? 事务的命令有哪些 ?
```markdown Redis事务包括MULTI、EXEC、DISCARD、WATCH四个命令。虽具备事务功能,但在实际开发中使用较少。 ```
44 7
|
3天前
|
NoSQL Redis 数据库
Redis的全局命令及相关误区
Redis的全局命令及相关误区
25 0
|
3天前
|
存储 缓存 NoSQL
深入了解Redis键管理:探索Redis键命令及其功能与应用场景
深入了解Redis键管理:探索Redis键命令及其功能与应用场景
|
3天前
|
NoSQL Redis 数据库
Redis中的常用命令非常丰富,涵盖了各种数据类型的基本操作以及服务器管理和维护的相关指令
【5月更文挑战第15天】Redis常用命令包括通用(如PING、SELECT)、键操作(KEYS、EXISTS、DEL)、字符串(SET、GET)、哈希(HSET、HGET)、列表(LPUSH、LPOP)、集合(SADD、SMEMBERS)和有序集合(ZADD、ZRANGE)等。这些命令用于数据操作及服务器管理,满足不同场景需求。了解更多命令,建议参考Redis官方文档。
11 2
|
3天前
|
存储 NoSQL Redis
Redis基础命令集详解
Redis基础命令集详解
15 1
|
3天前
|
存储 NoSQL Redis
Redis 常用命令
Redis 常用命令
17 0
|
3天前
|
存储 NoSQL Redis
深入浅出Redis(零):Redis常用命令的使用
深入浅出Redis(零):Redis常用命令的使用
|
3天前
|
存储 NoSQL 关系型数据库
深入浅出Redis(十二):Redis的排序命令Sort
深入浅出Redis(十二):Redis的排序命令Sort
|
3天前
|
NoSQL Linux Redis
Redis的介绍,以及Redis的安装(本机windows版,虚拟机Linux版)和Redis常用命令的介绍
Redis的介绍,以及Redis的安装(本机windows版,虚拟机Linux版)和Redis常用命令的介绍
27 0
|
3天前
|
存储 NoSQL 定位技术
Redis常用数据类型及常用命令
这些是Redis中常用的数据类型和命令。Redis还提供了许多其他命令和功能,用于数据存储、操作和查询。你可以根据需要选择适当的数据类型和命令来满足你的应用程序需求。
25 4