Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

缓存与数据库的双写一致性 | Consistency between redis cache and sql database #54

Unanswered
BigBlackSheep asked this question in Q&A
Discussion options

诚如文章所说 为避免数据不一致 给出的思路是:先删除缓存,再更新数据库
那么如果现在有两个线程 一个A更新 一个B读取,当A线程在更新的时候先把缓存给删掉了然后去做更新数据库的时候,B线程来读取数据的时候发现缓存没有数据(被A给删掉了)然后去读取数据库把数据读取出来放到了缓存中,这个时候A线程再去做更新数据库的操作...那么好戏来了...数据库和缓存真的不一致了. 请问要如何解决呢?

You must be logged in to vote

Replies: 75 comments 32 replies

Comment options

双写一致性校验应该可以

You must be logged in to vote
0 replies
Comment options

双写一致性校验应该可以

其实根据 《Cache-Aside pattern》先更新数据库再删缓存就可以啦

You must be logged in to vote
1 reply
Comment options

这个说的就是删除缓存失败后造成的问题

Comment options

我怎么感觉先删除缓存,再更新数据库 和 先更新数据库再删除缓存是一样的都会有问题呢
假如是先更新数据库再 删缓存,此时如果数据库还在更新中,一个线程进行了读取,不是也读取了脏数据了么

You must be logged in to vote
2 replies
Comment options

还在更新中,这个读到老数据应该是可以的

Comment options

按道理来说,数据库已经开始执行更新了,不应该由于并发问题在把缓存设置成旧的值。
推荐一种策略是双删缓存: 删缓存 修改数据库 删缓存

Comment options

更新前删缓存 更新后删缓存 这种应该会比较好一点~
You must be logged in to vote
4 replies
Comment options

我觉得可以,这样数据就一致了

Comment options

推荐一种策略是双删缓存: 删缓存 修改数据库 删缓存

这种也有比较大的问题,比如再高并发的情况下 :

1-delete cache

 ### 2-query DB

3-update DB

4-delete cache

 ### 5-set cache

134是一个线程
25是另一个线程

这种场景下不一致出现了,cache中还是旧的值,DB中是新的值。

Comment options

所以双删策略,是不是 应该更新完数据库后,删除缓存,然后为了避免删除失败,在通过MQ延迟删除兜底啊,这种才能保证最终一致性吧

有删除缓存失败的可能,就会有放入MQ失败的可能性,却增加系统的复杂度,考虑数据丢失问题

Comment options

你没理解到延迟双删,第二次删除是发送MQ去做延迟删除,压根不会和 1 3 在同一个线程,保证了第二次删除一定是最后一步了

Comment options

先删除缓存,更新数据库,预计数据库更新需要时间t,然后在提交数据库更新t时间后再次删除缓存

You must be logged in to vote
1 reply
Comment options

为了稳健性不能搞预计数据库更新时间吧。。。

Comment options

不是读写分离数据库,悲观锁可以解决,但主从读写分离,就真不知怎么解决了,
...
先删除缓存,更新数据库,预计数据库更新需要时间t,然后在提交数据库更新t时间后再次删除缓存
You must be logged in to vote
0 replies
Comment options

所以作者说先删缓存,再更新数据,然后用内存队列保证串行化,等数据库更新完再去读库就没有这么多幺蛾子了。还有同学提到的用读写锁。读写锁底层用的AQS其实也是用内存队列做的,保证顺序

You must be logged in to vote
1 reply
Comment options

如果是分布式的服务,这个读写锁就没办法保证了,单机服务还是可以的

Comment options

个人觉得缓存双删比较简单靠谱,而且编码不是那么复杂

You must be logged in to vote
1 reply
Comment options

是的,如果系统允许短时间内的数据不一致,这种方案肯定是好的,而且是简单的。但是如何要求有请求存在时redis和数据库的数据必须一致,那就只能把请求串行化了。

Comment options

作者写的其实比较清楚了。

不管先删除缓存,还是先更新数据库,都会出现数据不一致问题,因为这本身就不是一个原子操作。只是作者提到,如果不在高并发的情况下,这种现象出现的可能性极低(但不代表不存在)。

其实原子操作相对于非原子操作来说,本来就是一个极其耗性能的东西,因为原子操作需要的是排他的串行操作,根据阿姆达尔定律就可以证明。

You must be logged in to vote
3 replies
Comment options

这不是原不原子操作关系不大吧,要解决的是删除缓存到更新完数据库这个时间段内其他请求又请求缓存接口导致回源的是老数据的问题吧

Comment options

这不是原不原子操作关系不大吧,要解决的是删除缓存到更新完数据库这个时间段内其他请求又请求缓存接口导致回源的是老数据的问题吧

他文章里面忽略了一个问题, 高并发的情况下,用队列 你怎么保证你 版本1的更新消息 一定会在 版本2的更新消息 之前进入队列?不还是得加悲观锁? 数据库拿到行锁 然后把更新丢进队列,如果数据库不拿到行锁 就去投递消息进队列 队列的顺序肯定没法保证, 这个问题我之前做消息队列高并发 demo时候 早就研究过,双写要保证完全一致性 一定要竞争单条数据的全局锁,否则没法保证原子性。

而且双写一致性 大部分时候都是一个伪命题,没有什么业务需要这么强一致性的缓存的读,而更新的话 直接用数据库的行锁就能保证触发一次当前读。

Comment options

这不是原不原子操作关系不大吧,要解决的是删除缓存到更新完数据库这个时间段内其他请求又请求缓存接口导致回源的是老数据的问题吧

而且他文章里面的队列还要去维护更新,例如后续的更新来了,要自动忽略掉前面的老版本,这个操作本身也是很费时间的。

另外还要涉及到消息路由的问题, 你是让集群里面的所有的JVM虚拟机 都接受所有的读请求 ,还是要做hash分片,对部分读请求只会hash路由到特定的JVM,如果是前者,那么你需要更新数个JVM的队列,数据行的写操作的性能会急剧下降,如果是后者,你特定的JVM挂逼了,那么你服务就会缺失一大块的响应,因为特定的请求只会路由到特定的JVM实例上。

Comment options

高并发下保证绝对的一致,就需要用到内存队列做异步串行化。非高并发场景,双删策略基本满足了。如果特别要求强一致性,并不适合放在redis

You must be logged in to vote
0 replies
Comment options

看到各位回答很多人说:内存队列做串行化.可是这样就降低了效率.这里提供一个新思路就是.先更新数据库再删除缓存.之后使用MQ延迟队列(可以延迟个50-100毫秒)去做异步再删除.

You must be logged in to vote
3 replies
Comment options

哈哈。这样代价好大啊

Comment options

又得保证mq的一致性

Comment options

整个业务流程是串行的,如果删除缓存失败,就进行后续加入MQ延迟队列的步骤?
是否可以这样做: 删除缓存失败原因(缓存宕机,网络抖动,项目停止)
1、前两种通过补偿策略,加入到MQ延迟队列,或者回滚数据。
2、项目停止(优雅停止时长少于业务时间),这种不可控因素,有个补偿策略(rabbitMQ),但代价很大。
a、在第一次删除时,异步MQ(CompletableFuture里创建一临时队列,ttl 可以根据优雅停止时长)。
b、第二次删除时,根据返回的临时队列异步消费,若项目停止则加入死信队列,延迟消费。

Comment options

看到各位回答很多人说:内存队列做串行化.可是这样就降低了效率.这里提供一个新思路就是.先更新数据库再删除缓存.之后使用MQ延迟队列(可以延迟个50-100毫秒)去做异步再删除.

首先我认为肯定要先删缓存,再更新数据库,这二者的顺序是不能换的,否则更新完成,缓存未删除,查到的数据那势必不一致。
使用MQ异步删除,并不能保证一致性
内存队列做串行化是考虑具体场景的,并不适用所有情况。有强一致性要求的才考虑这种做法。否则双删即可

You must be logged in to vote
0 replies
Comment options

看到各位回答很多人说:内存队列做串行化.可是这样就降低了效率.这里提供一个新思路就是.先更新数据库再删除缓存.之后使用MQ延迟队列(可以延迟个50-100毫秒)去做异步再删除.

首先我认为肯定要先删缓存,再更新数据库,这二者的顺序是不能换的,否则更新完成,缓存未删除,查到的数据那势必不一致。
使用MQ异步删除,并不能保证一致性
内存队列做串行化是考虑具体场景的,并不适用所有情况。有强一致性要求的才考虑这种做法。否则双删即可

对 这个是我说错了 要先删缓存,再更新数据库,这二者的顺序是不能换的.使用MQ做异步删除为什么能保证一致性呢 可以展开说一下嘛.

You must be logged in to vote
0 replies
Comment options

看到各位回答很多人说:内存队列做串行化.可是这样就降低了效率.这里提供一个新思路就是.先更新数据库再删除缓存.之后使用MQ延迟队列(可以延迟个50-100毫秒)去做异步再删除.

首先我认为肯定要先删缓存,再更新数据库,这二者的顺序是不能换的,否则更新完成,缓存未删除,查到的数据那势必不一致。
使用MQ异步删除,并不能保证一致性
内存队列做串行化是考虑具体场景的,并不适用所有情况。有强一致性要求的才考虑这种做法。否则双删即可

对 这个是我说错了 要先删缓存,再更新数据库,这二者的顺序是不能换的.使用MQ做异步删除为什么能保证一致性呢 可以展开说一下嘛.

比如说你先删缓存,现在去修改数据库,修改的过程中有查询操作去查了数据库,又把旧数据添加到了缓存,你现在数据库更新完了,用mq去延迟删除缓存,那这个延迟期间,所有的查询查的还是旧数据

You must be logged in to vote
0 replies
Comment options

保证了最终一致性,你要是要求强一致性,那在考虑别的方法
...
------------------------------------------------------------------ 发件人:geekhoon <notifications@github.com> 发送时间:2020年1月15日(星期三) 16:44 收件人:doocs/advanced-java <advanced-java@noreply.github.com> 抄 送:MissHuZeQuan <huzequan1109@dingtalk.com>; Comment <comment@noreply.github.com> 主 题:Re: [doocs/advanced-java] 缓存与数据库的双写一致性 | Consistency between redis cache and sql database (#54) 看到各位回答很多人说:内存队列做串行化.可是这样就降低了效率.这里提供一个新思路就是.先更新数据库再删除缓存.之后使用MQ延迟队列(可以延迟个50-100毫秒)去做异步再删除. 首先我认为肯定要先删缓存,再更新数据库,这二者的顺序是不能换的,否则更新完成,缓存未删除,查到的数据那势必不一致。 使用MQ异步删除,并不能保证一致性 内存队列做串行化是考虑具体场景的,并不适用所有情况。有强一致性要求的才考虑这种做法。否则双删即可 对 这个是我说错了 要先删缓存,再更新数据库,这二者的顺序是不能换的.使用MQ做异步删除为什么能保证一致性呢 可以展开说一下嘛. 比如说你先删缓存,现在去修改数据库,修改的过程中有查询操作去查了数据库,又把旧数据添加到了缓存,你现在数据库更新完了,用mq去延迟删除缓存,那这个延迟期间,所有的查询查的还是旧数据 — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe.
You must be logged in to vote
0 replies
Comment options

这是来自QQ邮箱的假期自动回复邮件。您好,~您的邮件我已收到,谢谢!
You must be logged in to vote
0 replies
Comment options

您好,我将尽快给您回复。
You must be logged in to vote
0 replies
Comment options

您的邮件我已收到!我会尽快回复您的!!!
You must be logged in to vote
0 replies
Comment options

这是来自QQ邮箱的假期自动回复邮件。 您好,邮件已收到。我将尽快给您回复。谢谢
You must be logged in to vote
0 replies
Comment options

信件已收到。
You must be logged in to vote
0 replies
Comment options

这是来自QQ邮箱的假期自动回复邮件。 您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
You must be logged in to vote
0 replies
Comment options

'你好。已收到你的邮件,我会在假期结束后尽快处理。————————————这是来自QQ邮箱的自动回复邮件。'
You must be logged in to vote
0 replies
Comment options

这是来自QQ邮箱的假期自动回复邮件。您好,~您的邮件我已收到,谢谢!
You must be logged in to vote
0 replies
Comment options

您好,我将尽快给您回复。
You must be logged in to vote
0 replies
Comment options

您的邮件我已收到!我会尽快回复您的!!!
You must be logged in to vote
0 replies
Comment options

这是来自QQ邮箱的假期自动回复邮件。 您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
You must be logged in to vote
0 replies
Comment options

'你好。已收到你的邮件,我会在假期结束后尽快处理。————————————这是来自QQ邮箱的自动回复邮件。'
You must be logged in to vote
0 replies
Comment options

信件已收到。
You must be logged in to vote
0 replies
Comment options

不管是何总方案,归根到底是强一致性和最终一致性,搞明白这两种的区别即可。强一致性必须保证全局事务,更新时任何读操作都是锁住的;最终一致性,在读的过程中会有短暂的数据不一致,但是任何写操作情况下也必须加全局事务锁或者采用补偿式去补偿不一致带来的后果。(读写操作,都是针对有业务关联的数据)

You must be logged in to vote
0 replies
Comment options

我有一个想法,不知道有啥问题,还请各位大佬指正:能不能弄两个redis,一个redisA,一个redisB
写线程1的操作:

  1. 更新DB
  2. 插入redisB
  3. 删除redisA
  4. 另起一个线程将新数据插入到redisA+删除redisB

读线程2的操作:

  1. 读redisA,若有值,直接返回(这里可能读取的是旧的值,因为redisA还没有被删除)
  2. 如果没有,则说明有更新,新的值在redisB,直接去reidsB读

对比延迟双删:

  1. 更新DB
  2. 删除redis
  3. 延迟删除redis

以空间换时间,减少了数据不一致的时间,另外,读线程不会读取到数据库的旧值,然后更新到redis,不知道这种想法,大家觉得咋样,多多批评指正

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /