如果写Redis序列化与读Redis序列化不一致,你觉得会发生什么

开心一刻

昨天在我大哥家玩,嫂子在给小侄女泡奶粉

小侄女走到大嫂面前,大声的说了一句:妈妈,我爱你

大哥很期待的看着小侄女,然而小侄女只是一味的跟大嫂互动

大哥从沙发上起身,不开心的走进了卧室

大嫂看出大哥吃醋了,跟小侄女说到:你去把刚才给妈妈说的那句话也给爸爸说一遍

小侄女小跑进了卧室,趴着大哥的腿上说到:爸爸,我爱妈妈

Redis 写读

Spring Boot 接入 Redis 非常简单,2 步搞定

  1. POM 添加依赖

    常用的方式有两种

    • 添加 spring-boot-starter-data-redis 依赖
    • 添加 redisson-spring-boot-starter 依赖

    如果用到分布式锁的话,推荐用 redisson-spring-boot-starter

    为什么推荐用Redisson实现分布式锁,看完直呼好好好

  2. 配置连接

    在配置文件(一般是 application.yml 或 application.properties)中配置 Redis 的连接信息

    yaml 复制代码
    spring:
      redis:
        database: 0
        host: 192.168.1.225
        password: redis@qsl
        timeout: 1000

接入就算完成了;我们写读 Redis 试试

假设我们有 class

java 复制代码
@Data
public class CallbackStatus implements Serializable {

    private static final long serialVersionUID = -1L;

    private String taskId;
    private String bizLogId;
    private String status;

    public CallbackStatus() {
    }

    public CallbackStatus(String taskId, String bizLogId, String status) {
        this.taskId = taskId;
        this.bizLogId = bizLogId;
        this.status = status;
    }
    
    @Override
    public String toString() {
        return "CallbackStatus{taskId='" + taskId + "', bizLogId='" + bizLogId + "', status='" + status + "'}";
    }
}

将该类对象写入 Redis

java 复制代码
@Test
public void testSet() {
    CallbackStatus callbackStatus = new CallbackStatus(
        "1",
        "10000",
        "SUCCESS"
    );
    redisTemplate.opsForValue().set("qsl:callback-demo", callbackStatus);
}

写入成功后,Redis 中数据如下

怎么是一串乱码?这是因为我们未配置数据序列化,RedisTemplate 默认用的 JdkSerializationRedisSerializer,所以可读性差

然后我们从 Redis 中读取该对象

java 复制代码
@Test
public void testGet() {
    CallbackStatus callbackStatus = (CallbackStatus) redisTemplate.opsForValue().get("qsl:callback-demo");
    System.out.println(callbackStatus);
}

输出结果:CallbackStatus{taskId='1', bizLogId='10000', status='SUCCESS'}

是不是没问题?

写读序列化不一致

如果通过 Redis 实现服务间通信,上游服务将数据写入 Redis 时用的时默认序列化 JdkSerializationRedisSerializer

java 复制代码
@Bean("jdkObjRedisTemplate")
public RedisTemplate<String, Object> jdkObjRedisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new JdkSerializationRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(new JdkSerializationRedisSerializer());
    return template;
}

而下游服务读取的时候用的序列化是 FastJsonRedisSerializer

java 复制代码
@Bean("fastJsonRedisTemplate")
public RedisTemplate<String, Object> fastJsonRedisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);

    // 添加连接验证
    template.setEnableTransactionSupport(false);

    StringRedisSerializer stringRedisSerializer = new
        StringRedisSerializer();

    // 配置 FastJson 序列化器
    FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);

    // 设置 FastJson 的配置(如果有需要)
    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

    // key采用String的序列化方式
    template.setKeySerializer(stringRedisSerializer);
    // hash的key也采用String的序列化方式
    template.setHashKeySerializer(stringRedisSerializer);
    // value序列化方式采用jackson
    template.setValueSerializer(fastJsonRedisSerializer);
    // hash的value序列化方式采用jackson
    template.setHashValueSerializer(fastJsonRedisSerializer);
    template.afterPropertiesSet();
    return template;
}

毋庸置疑,通信会失败,但具体失败形式是什么?读取失败并抛出异常,还是其他形式?

我们试下就知道了,直接用 fastJsonRedisTemplate 去读 qsl:callback-demo

java 复制代码
@Test
public void testFastGet() {
    CallbackStatus callbackStatus = (CallbackStatus) fastJsonRedisTemplate.opsForValue().get("qsl:callback-demo");
    System.out.println(callbackStatus);
}

执行正常,并未抛出异常,只是读取的结果是 null

问题不就来了?

明明 Redis 中 qsl:callback-demo 有值,为什么读取的是 null

我再往真实业务上靠一靠,用 Redis 实现消息队列,简化版是不是直接用 Redis 的 List 来实现?

上游服务从 List 右边写入数据

java 复制代码
/**
 * 从 list 右边写入数据
 */
@Test
public void testWriteList() {
    CallbackStatus data = new CallbackStatus(
            "2",
            "20000",
            "failed"
    );
    jdkObjRedisTemplate.opsForList().rightPush("qsl:status:callback", data);
}

数据写入成功后,Redis 中数据如下

下游服务从 List 左边读取数据

java 复制代码
/**
 * 从 list 的左边读取数据
 */
@Test
public void testReadList() {
    Object o = fastJsonRedisTemplate.opsForList().leftPop("qsl:status:callback");
    CallbackStatus callbackStatus = convertToCallbackStatus(o);
    
    // 实际项目代码中没有 System 这一行
    System.out.println(callbackStatus);
    
    if (callbackStatus != null) {
        // TODO 业务处理,并打印相关日志
    }
}

private CallbackStatus convertToCallbackStatus(Object obj) {
    if (obj == null) {
        return null;
    }

    try {
        if (obj instanceof CallbackStatus) {
            return (CallbackStatus) obj;
        }

        if (obj instanceof JSONObject) {
            return ((JSONObject) obj).toJavaObject(CallbackStatus.class);
        }

        if (obj instanceof Map) {
            String jsonStr = JSON.toJSONString(obj);
            return JSON.parseObject(jsonStr, CallbackStatus.class);
        }

        String jsonStr = JSON.toJSONString(obj);
        return JSON.parseObject(jsonStr, CallbackStatus.class);

    } catch (Exception e) {
        LOGGER.error("【工作流任务回调】===》对象转换失败: class={}", obj.getClass().getName(), e);
        return null;
    }
}

读取并未异常,但结果却是 null

同时,Redis 中 qsl:status:callback 已经没了

然后上游开发与下游开发就开启了 battle

下游开发:你没往 Redis 中写入状态数据吗

上游开发:你把你的服务停下,我跑下试试,看 Redis 中有没有状态数据

几分钟后,上游开发给下游开发截了个 Redis 数据图,并气冲冲的跑到他面前,说到:Redis 里面不是有数据吗

下游开发启动服务,并看了看代码,说到:该打的日志都打了,现在一行日志都没有输出,就是从 Redis 没读到数据嘛

上游开发:那就不知道了,反正我是往 Redis 中成功写入数据了

然后下游开发陷入了无尽的猜想中

有别的节点消费了?开发环境的节点我已经停了,应该只有我本地节点了

Redis List 中数据已经没了,说明已经被消费了,本地没有业务日志,也没有异常日志,到底谁消费了?

......

从源码中找根因

从 Redis List leftPop 数据,到底是没查到数据,还是查到了反序列化成 null 了,跟下源码就知道了

先执行 testWriteList() 往 List 中插入一条数据

然后打好断点

以 debug 方式启动 testReadList

具体细节就不给你们展示了,直接给你们看答案

堆栈信息从下往上看,从 Redis leftPop 到的数据是 byte[131],说明什么?说明查到数据了!!!

只是 FastJsonRedisSerializer 将其反序列化异常了

可恶的是
fastjson 把异常吃了,没把异常往外抛,直接 return null

根因是不是就找到了?

通过 JdkSerializationRedisSerializer 把数据写入 Redis,读取的时候,FastJsonRedisSerializer 反序列化异常,但 fastjson 捕获了异常后直接返回 null

这个 null 并非指从 Redis 未查询到数据,而是读取到数据后反序列化异常,fastjson 返回的 null

不同的 Serializer 反序列化异常时所做的处理肯定是不一样的,FastJsonRedisSerializer 是捕获异常后什么也不做,只是返回 null,那 JdkSerializationRedisSerializer、Jackson2JsonRedisSerializer 等 Serializer 反序列化异常时,是不是也如 FastJsonRedisSerializer 一样呢,你们可以去试试

总结

  1. Redis 的写读序列化要保持一致,推荐用 json 序列化,可读性高

    json序列化包括 FastJsonRedisSerializer、Jackson2JsonRedisSerializer 等

  2. 捋清楚数据从 Redis 到程序,经历了哪些环节,逐个环节分析,而不是武断的省略中间环节

    Redis -> Redis 驱动 -> 反序列化器 -> 程序

  3. 从源码中找原因,有时候不仅治本,还很高效,不要畏惧它,结合 Debug 方式看源码,没那么难

相关推荐
煎饼学大模型1 小时前
架构决定上限:Skill 知识架构的三次重构实践
java·重构·架构·skill
llwszx1 小时前
【Java/Go后端手撸原生Agent(第五篇):多工具并行调用 + BashTool执行引擎 + Judge证据链升级】
java·后端·golang·状态机·pydantic·agnet·llm-as-judge
奈何不吃鱼1 小时前
【SpringBoot】业务多线程四大实战场景
java·spring boot·多线程
问商十三载2 小时前
2026法律行业AI引擎生成式优化怎么优化?三层专业加固提排名,零成本提33%引用率附适配表
java·前端·人工智能·算法
野生风长2 小时前
c++类和对象(this指针,重载operator,习题总结)
java·开发语言·c++
霸道流氓气质2 小时前
基于 Spring 事务同步机制的事务后置动作收集器 Starter 实践
java·后端·spring
難釋懷2 小时前
Nginx代理https请求
redis·nginx·https
过期动态2 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
刘小八2 小时前
Spring AI Tool Calling 生产化:参数校验、权限控制与超时隔离
java·人工智能·spring