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本身就是为了提升性能,加锁势必会降低性能,所以分布式锁不适合这个场景。

四、结语

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

相关推荐
小陈工1 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
科技小花6 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸6 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain6 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希6 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神6 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员6 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java7 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿7 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴7 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存