Redis 服务器 CPU 100% 问题调查

在做压测时发现 Redis 的 CPU 升到了 100% ,并且客户端创建了大量的连接,但是总的客户端数并没有变化。经调查最后发现是 JedisPool 配置不合理和使用不当导致的。这里记录下调查过程及一些个人理解,以作备忘。
Redis:多大的 key 算大?如何查找?如何拆分?

多大的 key 算大 ?

关于 Redis 中多大的 key 算大,这个貌似没有统一的定义。下面是摘自几篇博客中的关于大 key 的定义:

定义一:[1]

  1. 单个简单的 key 存储的 value 很大;
  2. HashSetZSetList 中存储过多的元素(以万为单位);
  3. 一个集群存储了上亿的 key , key 本身过多也带来了更多的空间占用;
Redis - ERR 'RENAME' command keys must in same slot

在测试环境(Redis 是单机版)使用 RENAME 功能时是好的,到了生产环境(阿里云的 Redis 集群版)报了如下错误:

ERR 'RENAME' command keys must in same slot.
channel: [id: 0x31b56a88, L:/10.0.3.34:46962 - R:r-xxxxxxxxxxxxxxxxxx.redis.rds.aliyuncs.com/xxx.xxx.xxx.xxx:6379]
command: (RENAME),
promise: java.util.concurrent.CompletableFuture@1e13ae01[Not completed, 1 dependents],
params: [[99, 108, 111, 99, 107, 45, 105, 110, 58, 102, ...], [99, 108, 111, 99, 107, 45, 105, 110, 58, 102, ...]];
nested exception is org.redisson.client.RedisException: ERR 'RENAME' command keys must in same slot.
channel: [id: 0x31b56a88, L:/10.0.3.34:46962 - R:r-xxxxxxxxxxxxxxxxxx.redis.rds.aliyuncs.com/xxx.xxx.xxx.xxx:6379]
command: (RENAME),
promise: java.util.concurrent.CompletableFuture@1e13ae01[Not completed, 1 dependents],
params: [[99, 108, 111, 99, 107, 45, 105, 110, 58, 102, ...], [99, 108, 111, 99, 107, 45, 105, 110, 58, 102, ...]]

Redis x Spring 使用 SCAN 命令搜索缓存

发现维护的项目中有个用户登录时间的缓存没有设置过期时间,导致产线环境的 Redis 中存在大量永不过期的废弃 Key 。这里使用 SCAN 命令搜索缓存已删除这些废弃的缓存数据。
使用 redis-shake 同步 Redis 数据

使用阿里开源的 redis-shake 工具同步 Redis 数据(这个开源工具貌似暂时仅支持单向同步,我记得阿里云提供的工具里貌似是支持双向同步的)。

这里是同步( sync )的示例,另外还支持从备份文件恢复( restore )数据。

记一次 Redis 不定时命中率降低问题调查

问题现象

  • 后端服务获取用户令牌信息时有几率获取不到。
    这个处理是在 Filter 中执行的,在所有业务处理之前。
    采用 StringRedisTemplate 操作 Redis 缓存,令牌是个 String 类型的缓存。
    令牌缓存的 KEY 在 Redis 中是一直存在的,远没到过期时间。

  • 获取不到令牌的请求,响应也很快,并不是由于 Redis 超时导致的。

  • 使用同一台 Redis(db 不同)的其它服务一切都是正常的,从没出现过类似问题。

  • 线下的测试环境使用的自建 Redis,并没有出现过这个问题。

  • 出问题服务的容器镜像是 9 天前构建的。

  • 服务重启之后会恢复正常,但不确定多长时间之后又会再次出现。
    重启后可能一天内都是好的,也可能几分钟之后就又出问题。

  • 与此同时,Redis 性能监控中的命中率图表会出现明显的波动,与服务出问题的时间基本一致。
    命中率波动非常大,正常情况下一般在 95% 左右,但出问题时可能会降到 20% 以下。

SpringBoot 多 Redis 接入

spring-boot-starter-data-redis 默认仅支持配置一个 redis 服务(spring.redis.xxx)。若要配置多个,则需要手动添加相关的配置代码。 spring-boot-with-multi-redis 就是一个多 redisspring-boot 示例,不过是基于 1.4.0.RELEASE 版的,部分配置方法在新版本中已经没有了。

通过 RedisTemplate 单次访问获取多个缓存值

使用 redisTemplate 尝试通过单次访问 Redis 获取多个数据时,使用了 multiexec 方法。但在运行时报了如下错误:

io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI

代码如下:

redisTemplate.multi();
for (String key : keys) {
    hmget(key);
}
List<Object> result = redisTemplate.exec();
return result;
Redis-Cli 常用命令

Key

  • DEL key 该命令用于在 key 存在时删除 key。

  • DUMP key 序列化给定 key,并返回被序列化的值。

  • EXISTS key 检查给定 key 是否存在。

  • EXPIRE key seconds 为给定 key 设置过期时间,以秒计。

  • EXPIREAT key timestamp EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳 (unix timestamp)。

  • PEXPIRE key milliseconds 设置 key 的过期时间以毫秒计。

  • PEXPIREAT key milliseconds-timestamp 设置 key 过期时间的时间戳 (unix timestamp) 以毫秒计

  • KEYS pattern 查找所有符合给定模式 ( pattern) 的 key。

  • MOVE key db 将当前数据库的 key 移动到给定的数据库 db 当中。

  • PERSIST key 移除 key 的过期时间,key 将持久保持。

  • PTTL key 以毫秒为单位返回 key 的剩余的过期时间。

  • TTL key 以秒为单位,返回给定 key 的剩余生存时间 (TTL, time to live)。

  • RANDOMKEY 从当前数据库中随机返回一个 key。

  • RENAME key newkey 修改 key 的名称

  • RENAMENX key newkey 仅当 newkey 不存在时,将 key 改名为 newkey。

  • SCAN cursor [MATCH pattern] [COUNT count] 迭代数据库中的数据库键。

  • TYPE key 返回 key 所储存的值的类型。

验证 Redis INCR 命令的原子性

想确认一下 INCR 命令是不是原子性的,所以写了段代码试了一下。

安装所需包

额外安装了一个 Args 包用来解析命令行参数,具体文档参考 这里

Install-Package StackExchange.Redis -Version 2.0.601
Install-Package Args -Version 1.2.1
Redis 数据结构 .NET Core 版代码示例(StackExchange.Redis)

Redis 数据结构 中简单介绍了 Redis 的 5 种数据结构及常用命令。
其中的示例是在命令行窗口执行的。

下面的代码则是 .NET Core 中通过 StackExchange.Redis 包实现的相同功能的示例。

其中最重要的一点区别就是很多命令(如 删除)在命令行中返回的是删除的元素数,而在 StackExchange.Redis 中返回则是 bool 类型。这点需要注意。

Redis 数据结构

Redis 可以存储键与 5 种不同的数据结构类型之间的映射:

  1. STRING(字符串)
  2. LIST(列表)
  3. SET(集合)
  4. HASH(散列)
  5. ZSET(有序集合)
结构类型 结构存储的值 结构的读写能力
STRING 可以是字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作;
对整数和浮点数执行自增(increment)或者自减(decrement)操作
LIST 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;
根据偏移量对链表进行修剪(trim);
读取单个或者多个元素;
根据值查找或者移除元素
SET 包含字符串的无序收集器(unordered collection),并且被包含的每个字符串都是独一无二、各不相同的 添加、获取、移除单个元素;
检查一个元素是否存在于集合中;
计算交集、并集、差集;
从集合里面随机获取元素
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对;
获取所有键值对
ZSET 字符串成员(member)与浮点数分支(score)之间的有序映射,元素的排列顺序由分支的大小决定 添加、获取、移除单个元素;
根据分支范围(range)或者成员来获取元素
RedisResponseException: No more data & Zero length respose

运维切换了新的 Redis 架构之后,偶尔会报 No more dataZero length respose 的错误。

架构

客户端 C# 使用的 ServiceStack.Redis 3.9.60.0 包,通过连接池 PooledRedisClientManager 管理连接;
Redis 是一主多从;
中间通过 HAproxy 代理,实现 LB;

异常信息

客户端

ServiceStack.Redis vs. CSRedisCore

第二次跳进这个坑了。不同的 Redis 客户端保存和读取数据的方式有些不一样的地方。
之前是在做 APP 的后台接口,用的 SpringBoot 的 Redis 包,和之前.Net 项目中使用的 ServiceStack.Redis 包保存到 Redis 服务器用的值会不同。最常用的 string 就不一样,ServiceStack.Redis 会在字符串的两头各加一个双引号,还有 Guid,除了会加双引号之外,还会去除中间的半角横线。
这次则是在 .Net Core 项目中使用的 CSRedisCore 包,现象和 SpringBoot 中类似。
下面是几个常用类型的测试结果。