双写兜兜转转,又回到了串行化的方式

开发 前端
我们开门见山,这个很好理解,双写就是说,一份数据在数据库存一份,在缓存中也存一份,给缓存一个过期时间,当读不到缓存时从数据库读出来然后写入缓存。

[[393138]]

本文转载自微信公众号「moon聊技术」,作者 moon聊技术。转载本文请联系 moon聊技术公众号。

什么是双写?

我们开门见山,这个很好理解,双写就是说,一份数据在数据库存一份,在缓存中也存一份,给缓存一个过期时间,当读不到缓存时从数据库读出来然后写入缓存。

为什么需要双写呢?

当请求量越来越大的时候,系统会慢慢出现瓶颈,由于数据库的链接是有限的,无法支撑较高的QPS,所以我们要想一个办法分担数据库的压力,于是就有了双写,将数据写入缓存,客户端读取数据直接从缓存中读取,这样就可以提高系统的性能。

但是如果要使用双写,那么不管是先更新缓存还是先更新mysql,总会有时间间隔,那么就要保证你的业务在一定程度上允许短暂的数据不一致的情况出现,否则,还是不建议使用的.

那么就有人问了?双写一定不能保证强一致性吗?

答案是可以,只要把所有与其相关的读写请求用队列串行化,这样就可以保证双写的强一致性了,但是这样会极大的降低系统的QPS,非常不推荐这种做法。

既然要双写,那么肯定会出现数据库和缓存数据不一致的情况,要怎样去避免呢?

双写不一致问题要怎么解决

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

这种情况会有什么问题呢?我们看下图:

首先a先更新数据库,按照正常流程来走,紧接着要a线程删除缓存,可是突然后面来了个b线程,并且a线程因为各种业务原因卡住了,导致b线程先完成了,之后a线程才更新缓存。这时突然有其他线程进来读数据,就会读到a的数据,但是按照业务流程来走,应该读到b的数据,此时,就出现了数据错乱的问题。

  • 1.线程a更新数据库
  • 2.线程b更新数据库
  • 3.线程b更新缓存
  • 4.线程a更新缓存
  • 5.其他线程读数据(读错了)

到这里我们会发现,直接更新缓存是有很大的问题的,而且很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值,有可能是联合其他的很多数据结合计算出来的一个值。

而且可能会有一种场景,我们经常在更新数据库后直接更新缓存,但是在此之间并没有缓存被访问的需求,这样我们就做了很多无用功,付出了很多代价。

大家应该对单例模式有所了解,其中有一种懒加载的思想,就是说,在你需要的时候再去加载,用在双写的情况下非常合适,也就有了下面这种先更新数据库,再删除缓存的模式。

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

这种情况又会有什么问题呢?

当然,这还是一种有问题的方案,我们来跟着图盘一盘。

1:线程a更新数据库

2:程序挂了,没来的及删除缓存

3.其他线程来读数据(全都是错的)

这种方案的问题一目了然,只要程序挂了,就会出现数据读错的情况,真实的业务你是应该读到a线程的值,却一直在读之前的值。

那这种方案有没有优化呢?

当然也有了,其实我们可以每次写入都记录日志,然后修改结束后也记录日志,通过日志状态来判断是否写入成功,

  • 如果没有写入成功后续并且没有新的写入请求,就补写,
  • 否则不做处理。

但是这种情况也会出现不一致的问题,就是如果写数据库程序断了,到下次恢复数据之前这段时间,还会出现数据不一致的情况。

并且如果是频繁写入的情况,很有可能日志机制没有发挥作用,就有新数据写入覆盖,并且日志系统还要占用额外的资源。

我懂了!应该先删除缓存再更新数据库,这样就可以了!

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

来来来,继续贴图,是不是很熟悉?

这种方案会有问题吗??当然有,继续盘道:

  • 1:线程a删除缓存
  • 2:线程b删除缓存
  • 3:线程a卡了
  • 4:线程b更新数据库
  • 5:线程a更新数据
  • 6:其他线程读数据,读到了a的(又错了)

完了,这种情况居然也有问题,线程a到底行不行,每次都是你出事。

这种情况中间会有一段数据乱掉,但是随着下次的更新数据还是会恢复正确。

难道终极方案是先删除缓存,再更新数据库,再更新缓存??

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

继续贴图

  • 1.线程a删除缓存
  • 2.其他线程读取数据,读到的是a之前的数据
  • 3.线程a更新数据库
  • 4.线程a删除缓存
  • 5.其他线程设置缓存数据,是a之前的数据(此时应该是a的)

大家是不是又发现了,这种设计方案还是会有问题的,直到下次数据更新才有可能将数据恢复正确。

来吧,最后一种大家经常讨论的延时双删方案,我们一起盘一盘。

五.延时双删

go on

  • 1.先删除缓存
  • 2.再写数据库
  • 3.休眠一段时间(根据具体的业务时间来定)
  • 4.再次删除缓存

这里加了一个延时的操作,目的是确保 修改数据库 -> 清空缓存前,其他事务的更改缓存操作已经执行完。

所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

但这其中难免还是会大量的查询到旧缓存数据的,因为延时时间是根据业务自己定义的,时间太长和太短在高并发情况下都会有查询到脏数据的情况产生。

这样最差的情况就是在超时时间内数据存在不一致。

结语

到这里大家应该会发现,除了串行化这种方式以外,其他无论哪种方式大大小小都会有数据不一致的现象发生,有时为了维护数据一致性问题还要做很多额外很重的操作,比如加一些日志来做状态处理双写问题,具体的方案选择还是要根据业务的敏感度来定的。

 

责任编辑:武晓燕 来源: moon聊技术
相关推荐

2009-07-10 09:38:06

Java swing组

2009-11-18 11:05:27

PHP串行化

2009-06-09 16:14:47

Java swing组件串行化

2009-09-11 12:17:59

C#控件属性

2016-11-17 22:18:31

id串行化服务器

2009-11-02 16:41:55

VB.NET串行化对象

2019-03-25 07:39:35

ID串行化消息顺序性高可用

2010-01-12 10:29:51

VB.NET对象串行化

2010-01-06 10:49:54

PHP串行化JSON

2010-01-14 18:00:07

VB.NET串行化对象

2009-11-17 16:24:27

PHP变量串行化

2010-01-06 10:58:06

建立JavaScrip

2010-01-06 11:05:35

JSON

2021-09-10 09:58:35

AvlBST时间

2022-02-28 21:15:42

火狐火狐浏览器浏览器

2017-03-13 16:58:05

戴尔

2011-05-18 15:20:13

XML

2018-08-20 16:00:23

MySQL并发控制MVCC

2023-03-29 08:33:03

仓储自动化系统

2016-01-08 10:38:44

Java对象深复制
点赞
收藏

51CTO技术栈公众号