如何保证Redis缓存和数据库双写一致性

一 缓存和数据库双写有什么问题

想要知道如何解决问题,必须先知道问题是什么?那缓存和数据库双写会有什么问题?

1.1 设置过期时间的策略

一般来说,我们会选择设置过期时间的策略来保证最终一致性。怎么保证?
实际上就是一种片“读写分离”,我的读操作全部从缓存里读,写操作则直接写进数据库,也就是说,缓存里的值没过期之前是不会更新的

1.2 设置过期时间的问题

那有什么问题?当然有问题了,那些在缓存中的值在没过期的这段时间里不管数据库改了多少次,缓存里的对应值都不会变,这确实保证了用户每次读到的值不会左右横跳,但是数据库里的值可是真真的发生了改变,这对于那种需要掌握实时数据的业务就不适用

1.3 设置过期时间的例子

股民李四在某软件上炒股,10点之前他买的股票疯涨,可是程序员张三在前一天误操作了一波,使用了上述策略,在设置过期时间的时候,猝死倒在了键盘上,原本几毫秒的过期时间被多添了n个0。结果呢?李四在咬牙熬过了前一天的绿油油之后,准时在10点打开软件定睛那么一瞧,仍然是那么绿(因为显示的是昨天的数据),绷不住了,含泪怒卖一波,以为自己血亏。结果显示到账99999…,突然大悲转大喜,以为软件出错了,血压也飙升,但不敢声张,只能强行平复心情。10点之后,该股票又暴跌,跌的惨绝人寰,李四下午打开一看,我靠,大涨(因为看到的是10点前的数据),心情顿时又不好了,血压再次飙升,由于年事已高,一个没崩住,被送走了…
由此可见,这种实时业务场景就不适合设置这种策略,要是好事变坏事,徒增悲伤!

二 常规更新策略

不是说设置过期时间的策略一定不好,一些对实时读取要求不明显的业务就可以用,但是我们的业务那么多,那么复杂,只有一个策略必然是无法满足所有的要求,那就轮到我们好好根据不同场景选择不同的更新策略了

2.1 先更新数据库 再更新缓存

  • 当我们需要写操作时,先更新数据库,再把缓存里对应的值更新
  • 有问题吗?当然有!从线程安全的角度来讲,假设有请求A和请求B同时进行进行更新:
    • A更新数据库
    • B更新数据库
    • B更新缓存
    • A更新缓存
  • 例子:张三和李四先后追一个姑娘,张三决定在姑娘的生日蛋糕上放上戒指并署名,李四知道了这件事,决定抄袭他的想法并取而代之,于是他们进行了如下操作
    • 张三买了一枚戒指,写好了情话
    • 李四也买了一枚戒指,写好了情话
    • 结果李四先张三一步把戒指和情书放到蛋糕上了
    • 张三准备放的时候一看,决定姑娘面前不讲兄弟情面!丢掉了李四送的戒指和情话放上了自己的
    • 于是原本李四的计划就落空了,没有成功摘到张三的桃子,姑娘还是被张三追到手了,这是有问题的,毕竟谁也不想当李四吧
  • 从业务角度来讲,也是有问题的,如果没有读操作,每次更新数据库后都要更新缓存不是会造成很大的资源浪费吗
  • 例子:
    • 同样的场景,我们知道姑娘直到吃蛋糕之前都不会打开蛋糕盒看的,可是张三和李四较上劲了
    • 当张三买了戒指,写好情话就去放一下
    • 李四买了戒指,写好情话也去放一下,丢掉了张三的东西
    • 张三想想自己送的东西不完美,重新选戒指,写情书,重新放
    • 李四也一样
    • 就这么来来回回n次,浪费了整整一天选礼物写情书,结果最后姑娘只在打开的时候看到一封信一枚戒指,您说亏不亏

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

  • 当进行写操作时,把对应的缓存删了,再更新数据库
  • 同样的,有问题吗?当然有问题,同样在多请求场景下,一个写,一个读:
    • A准备更新数据库,删除缓存
    • B准备读数据库,找不到缓存,到mysql里把旧值找出来,不仅放进缓存,还读到了旧值
    • A把新值写入数据库
  • 例子:孔乙己去买酒,酒馆的黑板(缓存)和账簿(数据库)里都记着他的账单
    • 孔乙己排出9文大钱,大喊还账
    • 小二刚擦掉黑板上的欠账,这时候老板正好走过来问他孔乙己还欠多少酒钱
    • 小二也是个愣的,翻看账单回答还差9文大钱,老板就在黑板上又挂上了孔乙己欠九文大钱的字样
    • 然后小二才把账单上的欠账划掉,记录不欠账了
    • 但是老板和酒店客人都不知道,然后因为此事狠狠打趣孔乙己
    • 孔乙己也糊涂,真以为自己还欠着账,嘴里喃喃地说着什么“读书人的事,不叫欠账,读书人的事”,然后就是一顿之乎者也的晦涩难懂的话,店里店外顿时充满了快活的气息
    • 本来孔乙己好不容易腰板挺直一回,这种情况又导致他被嘲笑,划向悲剧深渊,不难受吗
  • 那怎么解决呢?我们可以用双删延时策略:
    • 先删除缓存
    • 写数据库
    • 休眠一秒再删缓存
  • 例子:
    • 同样的场景,老板问完欠账写到黑板
    • 小二才把账簿的账划掉
    • 等老板写完没开始打趣孔乙己之前,小二又把黑板擦掉
    • 老板刚想打趣,发现黑板上没有账了,明白孔乙己还了欠账
    • 一场闹剧被避免了
  • 因此,这个等待多长时间的时机很重要,不一定是一秒,要根据具体场景来
  • 那如果这么做,每次都要等待一段时间,吞吐量不就降低了?因此我们可以另起线程来做删除操作,而不是等待

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

  • 这个思路也很简单,每次更新完数据后再让缓存失效
  • 会有问题吗?会的,下面这种情况就会出问题:
    • 某一个值刚好失效
    • A向数据库修改该值
    • B从数据库读到旧值
    • A删除缓存
    • B将旧值写入缓存
  • 但在实际中,第四步和第五步是很难调换顺序的,为什么?
    • 因为读比写快!
    • 等到A修改完该值准备删除缓存的时候,B早就读到旧值并写入缓存了
    • 因此绝大多数情况都是第四步先发生,第五步再发生

2.4 删除失败的问题

实际上,在2.2和2.3中我们都默认删除缓存一定成功,但是这个操作是有可能失败的,怎么解决?删除失败就多删几次呗!使用重试机制,每次删除失败就重试,这里不深挖,有兴趣的同学自己可以探究一下

猜你喜欢

转载自blog.csdn.net/weixin_44062399/article/details/123768711