Redis和数据库的数据一致性问题

本文详述了在数据读多写少的场景下,Redis作为缓存的常见使用,分析了数据一致性问题。在用户信息更新时,讨论了先更新数据库还是先更新缓存的选择,以及删除缓存后再更新数据库的策略,提出了失败重试、异步更新和延时双删等解决方案,强调了在实际操作中只能保证一定程度上的最终一致性。
摘要由CSDN通过智能技术生成

在数据读多写少的情况下,作为缓存来使用,恐怕Redis的使用是最普遍的场景了。当时用Redis作为缓存的时候,一般流程是这样的。

如果缓存在Redis中存在,即缓存命中,则直接返回数据。

如果Redis中没有对应缓存,则需要直接查询数据库,然后存入Redis,最后把数据返回。

通常情况下,我们会为某个缓存设置一个key值,并针对key值设置一个过期时间,如果被查询的数据对应的key过期了,则直接查询数据库,并将查询得到的数据存入Redis,然后重置过期时间,最后将数据返回,伪代码如下:

/**
 * 根据用户名获取用户详细信息
 */
public User getUserInfo(String userName) {
      User user = redisCache.getName("user:" + userName);
      if (user != null) {
          return user;
      }

      // 从数据库中直接搜索
      user = selectUserByUserName(userName);
      // 将数据写入Redis,并设置过期时间
      redisCache.set("user:" + userName, user, 30000);
      // 返回数据
      return user;
}

一致性问题

在Redis的key值未过期的情况下,用户修改了个人信息,我们此时既要操作数据库数据,也要操作Redis数据。导致我们现在面临着2种选择:

1. 先操作Redis的数据,在操作数据库的数据。

2. 先操作数据库的数据,在操作Redis的数据。

不论选择哪种处理方式,最理想的情况下,两个操作要么同时成功,要么同时失败,否则就会出现Redis和数据库的书库不一致情况。

遗憾的是,目前没有什么框架能够保证Redis和数据库的数据一致性。我们只能根据场景和所需付出的代码来采取一定的措施,降低数据不一致出现的概率,在一致性和性能之间取一个折中。

下面我们来讨论关于Redis和数据库质检数据一致性的一些方案。

方案选择

是删除缓存还是更新缓存?

当数据库的数据发生变化的时候,Redis的数据也需要进行相应的操作,那么这个操作是选择 更新or删除呢?

更新的话调用Redis的set方法,新值替换旧值;删除直接删除原来的缓存,下次查询直接去数据库读取,然后再更新Redis。

结论:推荐直接使用删除操作。

使用更新操作的话,会面临两种选择

1.先更新缓存,再更新数据库。

2.先更新数据库,再更新缓存。

第一种不用考虑了,下面讨论一下第二种先更新数据库的方案。

如果线程1和线程2同时进行更新操作,但是每个线程的执行顺序如上图所示,此时就会导致数据不一致,因此从这个角度上我们推荐直接使用删除缓存的方式。

此外,推荐使用删除缓存还有两点原因。

1.如果写数据库的场景比读数据库场景多,采用这种方案就会导致缓存被频繁写入,浪费性能;

2.如果缓存要结果一系列的复杂计算才能得到,那么每次写入数据库后,都再次计算写入的缓存,无疑也是浪费性能的。

明确了这个问题后,摆在我们面前的还是两个选择。

1.先更新数据库,再删除缓存

2.先删除缓存,再更新数据库

先更新数据库,再删除缓存

数据不一致的情况列举:

1) 线程A读取商品数据,刚好缓存失效了,故查询数据库数据,刚要把得到的数据放入Redis缓存中,这时候cpu发生上下文切换,线程A暂时得不到执行。

2)好,那么此时线程B来修改数据,执行update操作,然后删除缓存。

3)再到线程A获得执行时间片后,继续执行未完成的操作,将之前查询到的旧数据存到Redis中,此时就会出现Redis和数据库的数据不一致。

当然这种情况,条件比较严苛,出现的可能性较低一些。

解决方案:

那这种情况一般会有两种解决方式:失败重试异步更新。

失败重试

如果删除缓存失败,我们可以捕捉这个异常,把需要删除的key发送给消息队列。自己创建一个消费者消费,尝试再次删除这个key,直到删除成功为止。

 

这种处理方式有个缺陷,首先会对业务代码造成入侵,其次引入了消息队列,增加了系统的不确定性。

异步更新缓存

因为更新数据库时会忘binlog中写入日志,所以我们可以启动一个监听binlog变化的服务(比如使用阿里的canal开源组件),然后再客户端完成删除key操作。如果删除失败的话,再发送到消息队列。

总结:

总而言之,对于删除缓存失败的情况,我们的做法是不断地尝试删除操作,直到成功。无论选择哪种处理方式,始终只能保证一点时间内的最终一致性。只要系统有写的操作,就无法保证任意时刻的一致性。

先删除缓存,再更新数据库

数据不一致的情况:

1) 线程A修改数据库时,需先删除缓存数据

2) 线程B在线程A删除缓存和执行update操作期间,查询了数据库得到了旧数据,再写入Redis缓存,此时就有极大的概率会出现数据不一致的情况。

 

解决方案列举:

之前项目里就有过类似的解决方案:先操作缓存,但是不能删除缓存,可以将缓存设置为一个特殊值,与业务无关的一个特殊值(-999),客户端读取缓存时,发现时特殊值,就休眠一会,再去查一次Redis。(注意:特殊值是对业务有侵入的,指不定什么时候就会用到这个特殊值。以及休眠时间,可能会多次重复,如高并发情况下,缓存频繁被设置为特殊值,那么进来访问的线程都会进入休眠状态,这样对性能会有影响)

下面介绍另一种业内常见的机制,延时双删策略

先删除缓存,再写入数据库,休眠一会,再次删除缓存,如果写操作很频繁,同样会存在读取到脏数据的问题。注意:如线程进行sleep休眠一秒,系统对接口的性能要求比较高,超过半秒都不行,更别说一秒了。那么可以启动一个线程来异步执行延迟删除key,就可以解决时间问题。使用@Async注解来进行删除key即可。

@Async介绍:在Spring中,基于@Async标注的方法,称之为异步方法。这些方法在执行的时候,会独立起一个线程,调用者无需等待它的完成,即可继续其他操作。

延迟双删策略理解图。

用伪代码表示就是:

/**
 * 延时双删
 */
public void update(String key, Object data) {
    // 首先删除缓存
    redisCache.delKey(key);
    // 更新数据库
    db.updateData(data);
    // 休眠一段时间,时间依据数据的读取耗费的时间而定
    Thread.sleep(500);
    // 再次删除缓存
    redisCache.delKey(key);
}

总结:

总结还是同理,不论如何处理,选择那种方式,始终只能保证一点时间内的最终一致性。只要系统有写的操作,就无法保证任意时刻的一致性。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值