0%

数据库和缓存双写一致性

数据库和缓存双写一致性

背景

​ 使用缓存在存储热点数据是常用的提交系统响应速度的一种解决方案,但是在更新数据时如何确保数据库和缓存中的数据一致性,特别是在高并发的场景下,应如何保证双写数据的一致性。

双写一致性.png

通常来说,常见的应用场景为:

  1. 用户请求查询某些数据,先进入缓存查看是否存在
  2. 存在则直接返回数据
  3. 不存在则查数据库,查到则将数据添加一份到缓存中,再返回,使得下次查询可以直接从缓存中获取,从而提高系统的响应速度。

双写的四种场景

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

​ 如果采用这种方式,极易产生数据不一致的情况。因为先更新缓存,如果因为某些原因出现数据写入失败,比如主键冲突、非空字段未填值、数据库宕机等等情况导致数据库写入失败,这时就出现了缓存中是新值,数据库中是旧值,造成了数据库和缓存数据不一致。

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

​ 假设更新数据库的操作和更新缓存的操作在同一个事务中,那么更新数据库之后,再更新缓存,如果出现缓存更新失败,那么事务回滚,保证了数据的一致性。但由于数据库和缓存实际上都是采用远程链接的方式读写数据,所以一般来说只有在低并发的场景下,才会将二者放在同一个事务中,否则若写缓存过慢,直接导致数据库事务时间被拉长,而形成长事务。如果二者不在同一个事务中,若数据库更新数据成功,缓存中更新数据失败,就会导致数据的不一致。

​ 假设在高并发场景下,且更新缓存和更新数据库不在同一个事务中执行。假设有两个写操作,当写操作A更新数据库之后,在将数据写入缓存的过程中出现网络拥堵等情况,这时写操作B,也更新了数据库,拿着更新之后的值,写入到缓存中,同时写操作A的网络不拥堵了,写操作A又更新了缓存,这时数据库中是写操作B的值,缓存中是写操作A的值,从而造成了数据不一致。

先更新数据库再更新缓存.png

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

​ 同样在高并发下,如果请求A在删除缓存之后,写入数据库之前的这段时间内,如果有请求B进行获取数据,这是它从缓存中获取不到,则从数据库中获取,由于这时A还未将数据写入到数据库中,请求B读到的是旧值,而请求B又将读到的旧值写入到缓存中,之后请求A又将新值写入到数据库中,造成了数据库和缓存数据不一致。

先删缓存再写库.png

可以通过缓存双删的方式解决这个问题,写操作A在写入数据库之后,再删除一次缓存,这时又有一个新的问题,如果写操作A在一更新数据库之后,就去删缓存,可能存在某些线程中已经获取了旧值,只不过还没写入到缓存中,也就是上图的步骤7和步骤8。所以一般是在更新数据库之后一段时间之后进行缓存删除,比如500ms。

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

​ 同样假设有两个请求,一个读请求A,一个写请求B。

  • 当写请求B先到,在更新数据库的过程中或者更新数据库后还未删除缓存时,读请求到达,读取了缓存中的数据,然后写请求B再删除缓存,对于这种场景仅读请求A读取了一次旧值。
  • 当读请求A先到,读取到了缓存中的数据,直接返回了,这时写请求B在更新数据库,再删除缓存,同样对于这种场景,仅读请求A读取了一次旧值。

但是还是有一种场景下,会导致数据不一致,也就是缓存过期了。

也就是当写操作A到达,在更新数据时出现拥堵,这时读操作B来读取缓存,刚好缓存过期,则从数据库中获取到了旧值,同时写操作A更新了数据库,且删除了缓存,然后读操作B再将旧值写入到缓存中,造成数据不一致。

写库再删缓存.png

但是一般来说认为要造成上述场景需要满足两个条件:

  1. 缓存刚好过期了
  2. 读操作B从数据库读到数据之后,更新缓存的耗时比写操作A更新数据库+删除缓存的耗时长。(一般来说对缓存的操作耗时要远小于对数据的操作耗时)

综上,一般来说还是需要采用先更新数据库再删除缓存的策略。

结论

​ 实际上不论是方案3中的缓存双删还是方案4都存在一个问题,在二者操作不在同一个事务的前提下,如果删除缓存操作失败了,那么就会导致缓存和数据库数据不一致。

​ 这时就需要引入重试机制,缓存删除失败之后进行重试。关于重试就可简单可复杂了,简单就是直接捕获异常进行重试操作,但这可能影响接口时效,也可以将设置专门线程池,将重试操作推给线程池,又或者引入一些任务调度或者是MQ的中间件来处理。在查资料的过程中,有看到有人说也可以通过订阅mysqlbinlog,如果发现了更新数据请求,则删除对应的缓存,但是据我所知很多项目在生产环境甚至连binlog功能都没有开启[旺柴]。

-------------本文结束感谢您的阅读-------------