简单解决缓存+数据库数据一致性问题(伪代码简单演示)

我们在进行更新操作的同时,必定要更新缓存、更新数据库。这俩个操作不是原子性的,在一些严苛的情况下面,难免会出现一些差错,导致数据库中与缓存中的数据不一致的情况出现,出现了这种情况,会导致读取脏数据的情况出现。

常见的读取线程请求流程

请求缓存,缓存中存在数据直接返回给客户端,缓存中没有数据,接着请求数据库,数据库请求到的数据写入缓存,同时把数据返回给客户端
在这里插入图片描述

常见更新操作方案

1:先删缓存、再更新数据库(有隐患)

假设情景: a(更新线程),b(读取线程)
导致的情况: 缓存为旧数据、数据库为新数据

  • a:删除缓存
  • b:读取缓存,此时缓存为空
  • b:读取数据库,拿到旧数据
  • b:成功拿取到旧数据后,将旧数据写入缓存
  • a:成功删除缓存成功后,将新数据写入数据库

只要出现了这么一次情况,之后读取线程一直是读到的脏数据

伪代码实现

/**
     * 更新线程(先删缓存,再更新数据库)
     */
    @RequestMapping("/update3")
    public String update2(@RequestParam("id") int id) {
    
    
        try {
    
    
            userService.delCacheBykey(id);
            userService.creatPessimismOrder(id);
            //userService.creatOptimisticOrder(id);
        } catch (Exception e) {
    
    
            log.info(e.getMessage());
            return "操作失败";
        }
        return "操作成功";
    }

采用延时双删策略(优化)

先删缓存、再更新数据库、指定时间再次删除缓存,这样能确保缓存一定删除成功、且下次读取数据回写给缓存中的数据一定为最新的数据,保证了数据一致性

假设情景:a(更新线程),b(读取线程)

  • a:删除缓存
  • b:读取缓存,此时缓存为空
  • b:读取数据库,拿到旧数据
  • b:成功拿取到旧数据后,将旧数据写入缓存
  • a:成功删除缓存成功后,将新数据写入数据库
  • a:指定时间,再次删除缓存

伪代码实现

/**
     * 更新线程(先删缓存,再更新数据库)优化,延时双删
     */
    @RequestMapping("/update4")
    public String update4(@RequestParam("id") int id) {
    
    
        try {
    
    
            //删除缓存
            userService.delCacheBykey(id);
            //悲观锁更新数据库(查库存数据,更新库存,创建订单)
            userService.creatPessimismOrder(id);
            //userService.creatOptimisticOrder(id);
            //延时删除缓存
            poolExecutor.execute(new delayDelCache(id));
        } catch (Exception e) {
    
    
            log.info(e.getMessage());
            return "操作失败";
        }
        return "操作成功";
    }

2:先更新数据库,再更新缓存(直接pass)

假设场景: a(更新线程),b(更新线程)
导致的情况: 先更新的数值覆盖后更新的数值

  • a:更新数据库
  • b:更新数据库
  • b:成功更新好数据库后,更新缓存
  • a:成功更新好数据库后,更新缓存

出现了网络延迟,明明是b后更新的,但是数据库最终的数据是a线程的值

3: 先更新数据库,再删除缓存(推荐)

假设场景:a(更新线程),b(读取线程)

  • (1) b:读取缓存,恰好此时缓存过期了
  • (2)b:读取数据库,得到旧值
  • (3)a:更新数据库
  • (4)a:成功更新好数据库后,删除缓存
  • (5)b:成功读取到旧值后,将旧值写入缓存

此时也会出现数据不一致的问题,但是读操作是快于写操作的(不然读写分离干嘛吃的),步骤二耗时<步骤三耗时,耗时短的先执行后面的操作,所以步骤五先于步骤四执行,所以上面这种情况发生的概率很低。

伪代码实现

/**
     * 更新线程(先更新数据库,在删缓存)
     */
    @RequestMapping("/update1")
    public String update(@RequestParam("id") int id) {
    
    
        try {
    
    
            userService.creatPessimismOrder(id);
            //userService.creatOptimisticOrder(id);
            userService.delCacheBykey(id);
        } catch (Exception e) {
    
    
            log.info(e.getMessage());
            return "操作失败";
        }
        return "操作成功";
    }

优化:消息中间件,删除重试机制

/**
     * 更新线程(先更新数据库,在删缓存)优化,rabbitmq
     */
    @RequestMapping("/update2")
    public String update3(@RequestParam("id") int id) {
    
    
        try {
    
    
            userService.creatPessimismOrder(id);
            //userService.creatOptimisticOrder(id);
            userService.delCacheBykey(id);
            //延时删除缓存
            poolExecutor.execute(new delayDelCache(id));
            // TODO 通知消息队列,删除缓存


        } catch (Exception e) {
    
    
            log.info(e.getMessage());
            return "操作失败";
        }
        return "操作成功";
    }

完整代码链接

https://github.com/zhangzihang3/myRedisUniformityBlog.git

猜你喜欢

转载自blog.csdn.net/qq_42875345/article/details/113795241