一次Redis生产事故,公司损失百万

本文讲述了在将Jedis替换为Lettuce客户端并使用Spring的RedisTemplate过程中,由于默认序列化器导致的线上bug。问题在于RedisTemplate默认使用JDK序列化,存储的String类型数据带有引号,引发类型转换错误。解决方案是自定义RedisTemplate配置,设置StringRedisSerializer作为key和value的序列化器。

一、前因

公司有个核心项目redis的客户端一直是使用的jedis,后面技术负责人要求把jedis客户端替换成效能更高的lettuce客户端,同时使用spring框架自带的RedisTemplate类来操作redis。

然而世事难料,就是这么一个简单的需求却让老师傅翻了船。。。

二、事故预演

按照预设的结果,本次开发任务应该是非常轻松的:

  1. 将配置文件中jedis连接池的配置项平移替换成lettuce的;
  2. 把项目中jedis配置相关的代码删掉;
  3. 把使用到jedis的地方替换成redisTemplate。

伪代码

其他配置项不一一展示

spring.redis.jedis.pool.max-idle = 200
spring.redis.jedis.pool.min-idle = 10
spring.redis.jedis.pool.max-active = 200
spring.redis.jedis.pool.max-wait = 2000
复制代码

替换成

spring.redis.lettuce.pool.max-idle = 200
spring.redis.lettuce.pool.min-idle = 10
spring.redis.lettuce.pool.max-wait = 2000
spring.redis.lettuce.pool.max-active = 200
复制代码

业务代码也从jedis换成redisTemplate

jedis的伪代码:

/**
 * 设置商品库存到redis - jedis
 * @param goodId 商品id
 * @param count 库存量
 * @return
 */    
@PatchMapping("/storage/jedis")
public String setStorageByJedis(
    @RequestParam("goodId") String goodId,
    @RequestParam("count") String count) {
    Jedis jedis = getJedis();
    jedis.set("good:" + goodId, count);
    jedis.close();
    return "success";
}
复制代码

redisTemplate的伪代码:

/**
 * 设置商品库存到redis - redisTemplate
 * @param goodId 商品id
 * @param count 库存量
 * @return
 */
@PatchMapping("/storage")
public String setStorage(
    @RequestParam("goodId") String goodId,
    @RequestParam("count") String count) {
    redisTemplate.opsForValue().set("good:" + goodId, count);
    return "success";
}
复制代码

然而一切工作做完,信心满满的上线发布之后,却大面积的爆发了线上bug。属于严重的生产事故。

从错误日志中我们可以清晰的看到是因为String类型的数据无法转换成int类型,我心中出现了一个大大的问号:明明我存到redis的是可以转成数字类型的字符串呀?

原因分析

通过Redis-Desktop-Manager可视化工具查看数据

发现string类型的键值对value值多了一对双引号

纳尼!怎么用jedis的时候就没有,换成redisTemplate就有了?

经过一番代码检查,发现使用redisTemplate的过程中好像少了一个步骤:配置序列化 一般如果没有特殊配置或者要使用redis连接池,就只用在配置中心或者配置文件中加入

spring.redis.host = 172.0.0.1
spring.redis.port = 6379
spring.redis.password = 123456Copy to clipboardErrorCopied
复制代码

然后注入redisTemplate就可以使用了,非常简单。

然而RedisTemplate使用的默认序列化器是JDK自带的序列化器,看源码:

看RedisTemplate的类图

由于RedisTemplate继承了RedisAccessor,RedisAccessor实现了InitializingBean,所以在RedisTemplate类初始化完成后,可以重写afterPropertiesSet()方法,设置序列化器。

解决方案

写一个redis的配置类,重新设置序列化器。

@Configuration
@ConditionalOnClass(RedisOperations.class)
public class RedisTemplateAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name="redisTemplate")
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate template=new RedisTemplate();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
复制代码

这里只针对redis的string类型配置StringRedisSerializer序列化器,大家可以根据项目实际需求增加Hash对象类型的配置。

Spring自带提供了多种序列化器,如下

也可以自定义序列化器,需要实现RedisSerializer接口,并重写serialize()和deserialize()方法。

为了方便演示,没有写全局的redis配置类,直接在接口中重置序列化器,伪代码如下:

@PatchMapping("/storage")
public String setStorage(
    @RequestParam("goodId") String goodId,
    @RequestParam("count") String count) {
    redisTemplate.setKeySerializer(new StringRedisSerializer()); // 重置redis string类型key的序列化器
    redisTemplate.setValueSerializer(new StringRedisSerializer()); // 重置redis string类型value的序列化器
    redisTemplate.opsForValue().set("good:" + goodId, count);
    return "success";
}
### 如何正确关闭 Redis 服务器 为了安全地停止 Redis 服务并防止数据丢失,可以按照以下方法操作: #### 方法一:通过 `redis-cli` 命令 可以通过 `redis-cli` 工具发送 `SHUTDOWN` 命令来优雅地关闭 Redis 服务器。此命令会触发 Redis 的持久化机制(如果配置了 RDB 或 AOF),并将所有未保存的数据写入磁盘后再退出。 ```bash redis-cli SHUTDOWN ``` 该命令支持两种可选参数: - `SAVE`:强制执行一次完整的 RDB 持久化再关闭。 - `NOSAVE`:直接关闭而不进行任何持久化操作。 例如,要强制保存当前状态到 RDB 文件后关闭 Redis,可以运行如下命令[^1]: ```bash redis-cli SHUTDOWN SAVE ``` #### 方法二:通过信号终止进程 可以直接向 Redis 进程发送 `SIGTERM` 信号以实现正常关闭。当接收到这个信号时,Redis 将像处理 `SHUTDOWN` 命令一样完成必要的清理工作。 获取 Redis 主进程 ID (PID),通常可以从 `/var/run/redis.pid` 中读取,或者使用 `ps aux | grep redis-server` 查找 PID 后手动杀死进程: ```bash kill $(cat /var/run/redis.pid) ``` 注意,只有在无法访问 `redis-cli` 或者其他特殊情况才推荐这种方法[^2]。 #### 方法三:禁用守护线程模式下的重启行为 如果你正在调试环境或测试环境中频繁启动和停止 Redis 实例,则可能需要调整其配置文件中的某些选项。比如将 `daemonize` 设置为 `no` 来阻止后台运行,并允许脚本控制生命周期[^3]: ```conf daemonize no ``` 这样做的好处是可以更方便地管理单次运行实例;缺点则是生产环境下一般不会采用这种方式因为缺乏稳定性保障。 另外需要注意的是,在高可用架构下(如哨兵 Sentinel 或 Cluster 部署场景), 单独关停某个节点之前应该先通知整个系统做好准备以防分区问题发生[^4]. --- ### 注意事项 无论采取哪种方式,请务必确认已经完成了所有的事务提交以及重要数据已被妥善存储至硬盘之中以免造成不必要的损失! ```python import os os.system('redis-cli SHUTDOWN') print("Redis has been shut down.") ``` 以上 Python 脚本展示了如何调用系统的 shell 接口去执行标准的 Redis 关闭流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值