redis与数据库双写一致性解决方案

一、场景介绍

在我们平时开发过程中,把某些数据放到redis中缓存起来用于快速读取已不是什么稀奇的事情。通常的流程是这样的:

1、项目启动时,将数据从数据库中加载出来保存到redis中

2、项目启动成功后,系统在运行过程中,如果需要这些数据,优先从redis中获取,如果redis中不存在,那么再去查询一次数据库,把查询到的数据先保存到redis中,然后再将数据返回给页面。

我们通常会把改动不频繁的数据加载到redis中,改动不频繁可不是不改动,当数据发生修改的时候,redis如果不及时更新,那么redis中保存的数据将不可靠,接下来我们分析一下这个问题并且给出一些解决方案。

二、问题复现

2.1、预先将测试数据加载到redis
java 复制代码
    /**
     * 功能描述:预先将数据加载到redis
     * @Author:huhy
     * @Date: 2025/3/21 22:24
     */
    @Test
    public void init(){
        searchCodeRuleAndSetRedis(2L);
    }

    /**
     * 功能描述: 查询并设置到缓存
     * @Author:huhy
     * @Date: 2025/3/21 20:56
     */
    private String searchCodeRuleAndSetRedis(Long id){
        //通过id查询编码规则
        TSCodeRule tsCodeRule = codeRuleService.selectTSCodeRuleById(id);
        String roleJson = JSON.toJSONString(tsCodeRule);
        //将测试数据保存到redis中
        redisTemplate.opsForValue().set("dataConsistent:"+tsCodeRule.getId(),roleJson);
        return roleJson;
    }

2.2、复现数据库与redis数据不一致
java 复制代码
    /**
     * 功能描述: 数据一致性测试方法
     * @Author:huhy
     * @Date: 2025/3/20 23:27
     */
    @Test
    public void dataConsistentTest(){
        //另起线程去查询
        new Thread(()->{
            String data = findData(2L);
            System.out.println(Thread.currentThread().getName()+"读取到的数据为:"+ data);
        },"测试线程").start();
        updateData(2L);
    }

    /**
     * 功能描述: 修改数据
     * @Author:huhy
     * @Date: 2025/3/21 21:54
     */
    private void updateData(Long id){
        //通过id查询数据
        TSCodeRule tsCodeRule = codeRuleService.selectTSCodeRuleById(id);
        if(Objects.isNull(tsCodeRule)){
            return;
        }
        //修改CodePrefix字段
        tsCodeRule.setCodePrefix("test-data-"+UUID.randomUUID().toString().replace("-",""));
        int result = codeRuleService.updateTSCodeRule(tsCodeRule);
        if(result!=0){
            System.out.println(Thread.currentThread().getName()+" 修改成功,修改后的信息为:"+JSON.toJSONString(tsCodeRule));
        }

    }

    /**
     * 功能描述: 通过id查询
     * @Author:huhy
     * @Date: 2025/3/21 21:49
     */
    private String findData(Long id){
        Object dataObj = redisTemplate.opsForValue().get("dataConsistent:" + id);
        //如果缓存中不存在,则从数据库中查询
        if(Objects.isNull(dataObj)){
            return searchCodeRuleAndSetRedis(id);
        }else {
            //如果缓存中存在,直接返回
            return dataObj.toString();
        }
    }

三、逐步修复一致性问题

3.1、坊间对话

这次我们请到了杠精大神猫哥,下面请看猫哥表演。

猫哥:" 第二步中写的那是什么玩意,那不就是bug吗?谁家好人写修改不维护redis?"

小永哥:" 被你发现了,没错,修改数据时确实应该将redis也考虑进去,那么我们开始修改这个bug。我的计划是在真正操作数据库之前,先将redis中的缓存数据删除了,这样读取数据的时候,redis中没有的话就会自动从数据库中查询,到时候再设置到redis中,这样不就一致了,你觉得呢?"

猫哥:"我暂时也没有好的办法,先这么修改吧!"

下面我们就对代码进行一些修改,在修改数据之前进行删除缓存,测试时分别在修改方法调用前后均进行一次读取,看看效果。

java 复制代码
 /**
     * 功能描述: 数据一致性测试方法
     * @Author:huhy
     * @Date: 2025/3/20 23:27
     */
    @Test
    public void dataConsistentTest(){
        System.out.println("第一次读取到的数据为:"+ findData(2L));
        updateData(2L);
        System.out.println("第二次读取到的数据为:"+ findData(2L));
    }

    /**
     * 功能描述: 修改数据
     * @Author:huhy
     * @Date: 2025/3/21 21:54
     */
    private void updateData(Long id){
        //通过id查询数据
        TSCodeRule tsCodeRule = codeRuleService.selectTSCodeRuleById(id);
        if(Objects.isNull(tsCodeRule)){
            return;
        }
        //修改之前,先将缓存删除
        redisTemplate.delete("dataConsistent:"+id);
        //修改CodePrefix字段
        tsCodeRule.setCodePrefix("test-data-"+UUID.randomUUID().toString().replace("-",""));
        int result = codeRuleService.updateTSCodeRule(tsCodeRule);
        if(result!=0){
            System.out.println(Thread.currentThread().getName()+" 修改成功,修改后的信息为:"+JSON.toJSONString(tsCodeRule));
        }

    }

小永哥:看样子是成功了,第二次读取的结果、数据库、redis中都已保持一致,看来这个改动非常成功!!!

猫哥:有瑕疵,首先测试代码太过理想,实际场景存在并发访问风险,到时候数据会混乱成什么样谁都不知道,比如说数据库中已修改了,但是缓存中还是旧值的情况。

小永哥,你说的有道理,可以详细说说吗?

猫哥:假如修改和请求都是独立的线程,修改代码运行的时间有点长,这时候虽然已经删除了redis,但是立刻有线程进行数据读取,此时数据库中依然还是旧数据,那么又把旧数据设置回redis中了。

3.2、数据不一致问题解决方案之延时双删

延时双删的意思是:在执行数据库修改之前先删一次缓存,在数据库修改之后,再进行一次删除,这样可以保证数据库与redis中的数据保持一致,我们来实现一下。

java 复制代码
/**
     * 功能描述: 数据一致性测试方法
     * @Author:huhy
     * @Date: 2025/3/20 23:27
     */
    @Test
    public void dataConsistentTest() throws Exception{
        CountDownLatch countDownLatch = new CountDownLatch(11);
        //修改数据
        new Thread(()->{
            updateData(2L);
            countDownLatch.countDown();
        },"修改线程").start();
        //查询数据
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                sleepTest(random.nextInt(6) + 1L);
                String data = findData(2L);
                System.out.println(Thread.currentThread().getName()+"读取到的数据为:"+ data);
                countDownLatch.countDown();
            },"查询线程"+(i+1)).start();
        }
        countDownLatch.await();
    }

    /**
     * 功能描述: 修改数据
     * @Author:huhy
     * @Date: 2025/3/21 21:54
     */
    private void updateData(Long id){
        //通过id查询数据
        TSCodeRule tsCodeRule = codeRuleService.selectTSCodeRuleById(id);
        if(Objects.isNull(tsCodeRule)){
            return;
        }
        //修改之前,先将缓存删除
        redisTemplate.delete("dataConsistent:"+id);
        //休眠几秒再修改CodePrefix字段,模拟业务执行缓慢
        try {
            sleepTest(5L);
        }catch (Exception e){
            e.printStackTrace();
        }
        tsCodeRule.setCodePrefix("test-data-"+UUID.randomUUID().toString().replace("-",""));
        int result = codeRuleService.updateTSCodeRule(tsCodeRule);
        //修改之后再删一次缓存
        redisTemplate.delete("dataConsistent:"+id);
        if(result!=0){
            System.out.println(Thread.currentThread().getName()+" 修改成功,修改后的信息为:"+JSON.toJSONString(tsCodeRule));
        }

    }

测试以后,我们可以看到,数据库和redis中的数据倒是保持一致了,看来修改的时候需要在修改前后都需要进行删除,才能保证数据库和缓存数据一致。

猫哥:我测试结果不敢苟同,虽然数据库和redis中的数据保持了一致,但是有一些线程还是读取到了旧数据,这个问题可以再改改吗?

小永哥:有这个必要吗?只要数据库和redis中数据一致不就好了,那我没修改数据之前可不知道已经有多少线程读取到了旧数据,想获得最新的数据,就是需要重新获取,不对吗?

猫哥:我不是这个意思,我的意思是在修改过程中我们读取到了旧数据,我想要数据只要发生修改就立刻更新到redis,这个可以实现吗?

3.3、数据不一致问题解决方案之分布式锁

如果需要保证数据库和redis数据时时一致,那么我们需要加锁。可以加分布式锁,上一期我们讲过分布式锁了,这次就不详细介绍了,但是不建议加锁,因为我们用redis本身就是为了提升性能,加锁势必会降低性能,所以分布式锁不适合这个场景。

四、结语

本次简单介绍了缓存和数据库数据不一致问题,这个问题虽然简单,但是平时开发过程中容易被忽略,所以我们单独拿出一篇来讨论这个事情。

相关推荐
椰椰椰耶12 分钟前
【redis】哨兵:docker搭建redis环境,容器的编排方式
数据库·redis·docker
forestsea17 分钟前
PostgreSQL:索引与查询优化
数据库·postgresql
小样vvv1 小时前
【Redis】深入解析 Redis 五大数据结构
数据结构·数据库·redis
行走在云端z1 小时前
MongoDB 的索引是提高查询性能的核心机制,类似于传统关系型数据库的索引。以下是对 MongoDB 索引的详细说明:
数据库·mongodb
Wo3Shi4七1 小时前
MySQL ORDER BY、 LIMIT和DISTINCT 用法和实例
数据库·后端
镜舟科技1 小时前
如何理解 Apache Iceberg 与湖仓一体(Lakehouse)?
数据库·数据分析
Fanmeang2 小时前
ISIS-3 LSDB链路状态数据库同步
运维·网络·数据库·华为·智能路由器·ensp·isis
珹洺2 小时前
计算机网络:(三)计算机网络体系结构(附带图谱表格更好对比理解)
运维·服务器·网络·数据库·网络协议·计算机网络·网络安全
笑远2 小时前
GaussDB 主从复制原理详解
数据库·gaussdb
{Hello World}2 小时前
MySQL学习之用户管理
数据库·学习·mysql