参考文章https://www.cnblogs.com/yanglang/p/9098661.html 『yanglang』
为什么写这篇文章?
因为缓存相对于数据库,有更好的性能和并发能力,所以我们会使用缓存将数据分为热数据和冷数据,将热数据放在缓存中,加快数据的读取并减小数据库的压力。但是使用缓存就随之伴随着缓存与数据库双写的问题。公司的项目最近因为更新策略的选择失误,导致了比较大个问题。导致一个活动的所有数据都是脏数据,所以需要调研目前的更新策略的优势和劣势。
对于缓存读取的,应该只有一种策略,那就是先读取缓存,如果缓存数据不在会读取数据库的数据写入缓存。其实相对来说,或者有些业务场景会直接读取数据库的数据来保持强一致性,这就不在我们的讨论范围了,因为这种情况属于跳过缓存直接读取数据库,等于没有用缓存的情况。
我们主要讨论的就是更新策略,对于更新完数据库,是更新缓存呢,还是删除缓存。又或者是先删除缓存,再更新数据库。这都是需要讨论的点。
更新策略
其实缓存更新策略就分为两大类四种
- 先更新缓存,后更新数据库(我们使用的方式)
- 先更新数据库,后更新缓存
- 先删除缓存,后更新数据库
- 先更新数据库,后删除缓存
首先,我们的前提条件是,是可以接受数据的短暂不一致,不是强一致,而是最终一致性。这样我们才有讨论的空间,因为数据库修改相对于缓存慢得多,不管选择哪种策略,缓存和数据库中的数据都会不一致。
其实相对来说,保持最终一致性的最有效的武器就是设置缓存的失效时间,这样会使得缓存和数据库最终一致,但是业务场景是千变万化,有时候等不到缓存失效的时间,所以才会选择更新策略。
1.先更新缓存,后更新数据库(我们使用的方式)
这种情况,非常不建议采用。
原因1
- 线程1修改了缓存
- 线程2修改了缓存
- 线程2更新了数据库
- 线程1更新了数据库
这种情况就是导致缓存的数据和数据库的数据不一致,导致数据错误。而且这种情况相对于策略2来说出现的概率更高,因为数据库的更新要比缓存慢得多,从而导致步骤1和步骤2的间隔变大。而缓存更好的读写性能从而增大了数据错误的概率。所谓成也萧何,败也萧何。
原因2
正常我们是要保证数据库中的数据一定正确。如果我们按照上面的步骤的话,会不会导致修改数据的顺序和实际的顺序不同呢,而且如果更新缓存成功后更新数据库失败也会导致数据错误。
为什么之前没有出错
但是为什么我们使用这个更新策略,一直没问题,直到最近才出问题呢。是因为我们缓存的数据只用于展示数据,服务端这边如果要先读后写的操作都是直接读取数据,这样就算缓存里的数据是错误的,只是影响数据的展示,并不影响数据的修改。但是因为并发量的增加,导致两个功能负载太高。所以一个同事就将数据改为缓存读取,最后导致所有数据都错乱了。
2.先更新数据库,后更新缓存
这个方案同样不建议采用。
原因
- 线程1更新了数据库
- 线程2更新了数据库
- 线程2更新了缓存
- 线程1更新了缓存
这就会导致数据错误,本来应该线程1先更新缓存,但是因为其他原因导致线程2先更新缓存,从而导致数据不同步
番外.为什么不建议采用更新缓存
- 如果导致了缓存数据和数据库数据不一致的情况。直到缓存被淘汰之前,数据其实一直都是错误的。
- 对于写操作比较多的情况,会导致每次修改数据之后都需要更新缓存,其实我们并不需要频繁更新数据。这样反而浪费性能。
- 如果我们想要的数据是经过计算得出的,那我们一般会把最终结果存入缓存,这样的每次更新数据之后,都需要进行一次计算,这样也会导致性能的浪费。
那我们接下来就是讨论删除缓存的情况。那么我们应该先删缓存还是先更新数据库呢。
3.先删除缓存,再更新数据库
其实这种情况会导致数据的不一致,原因如下:
原因
- 线程1进行写操作
- 线程1删除缓存
- 线程2进行读操作
- 线程2读取数据库旧数据
- 线程2将数据写入缓存
- 线程1更新数据库
上述情况就会导致数据最终也是不一致的。
其实这种情况是有一个不错的解决方式的,就是延迟双删策略
- 删除缓存
- 更新数据
- 休眠线程
- 删除缓存
这样的话可以将休眠时间之间造成脏数据删除。
那么,这个时间怎么确定的,具体该休眠多久呢?
针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
采用这种同步淘汰策略,吞吐量降低怎么办?
其实如果你仔细想过这个策略的话,我们会发现,其实第二次删除完全不用当前线程来做。我们可以异步来进行数据的删除。这样当前线程就不用阻塞了。
4.先更新数据库,再删除缓存
我看了网上大多数的博文,都是比较建议这种更新策略的。而且《Cache-Aside pattern》。其中就指出
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
这种情况就是完美的吗,如果你是位老司机的话,你就会知道世界上并没有完美的东西。这种策略也会有并发问题。
缓存中存在数据
这种情况我没有想到会出现脏数据的情况,其实会出现短暂的数据不一致。但是我在文章开头就说了。我们是要保证最终一致性,而不是强一致性。
缓存中不存在数据或缓存中的数据被淘汰
这样就会有这种情况
- 线程1查询数据库,得到旧值
- 线程2修改数据库
- 线程2删除缓存
- 线程1将旧址写入缓存
这样就会产生数据错误,导致脏数据产生。
但是其实仔细分析的上述情况的话,就会发现这是一个比较极端的情况。就是修改数据库要比读取数据库快,这样才会导致删除缓存早于线程1将数据写入数据库。但是我们都知道,数据库的读取是要比写入快的。所以这种情况只有在比较极端的情况才会产生。
其实上述情况也是可以解决的,就是使用异步延时删除策略,保证读请求完成以后,再进行删除操作。
所以相对来说这种更新策略是比较适用于大多数场景的。
番外.删除缓存存在的问题
问题1
这个番外是在查资料的时候才想到确实有这个问题。原博文我已经放在文章开头了。
如果缓存删除失败就会产生错误的数据,这也是需要考虑的问题。
如何解决?
我们需要设计一个重试模块,当我们删除数据的时候,如果删除失败,我们将重试删除操作。
方案:
如下图所示
流程如下所示
- 更新数据库
- 删除缓存数据失败
- 将需要删除的缓存key发送到消息队列
- 独立的删除模块进行缓存的删除
- 如果删除模块删除失败,会继续删除操作,直到成功
其实我认为这种情况应该适用于大多数情况,我引用的那篇博文还写了订阅binlog的情况,这种我觉得不是非常实用,有兴趣的同学可以去看一下我引用的博文。
问题2
这个问题就是如果我们用了读写分离的话,相对来说。每个从库同步主库的数据都会有一些延时,如果我们用了一个公共缓存的话,就会导致主库已经更新了数据,从库还是旧数据,从而导致:
- 线程1修改数据库
- 线程1删除缓存
- 线程2读取到从库的旧数据
- 线程2写入缓存
这样就会导致缓存数据错误。这种情况需要的前提条件就是读取操作早于主从同步完成的时间。其实策略3和策略4都有这种问题。
如何解决?
这种情况就是需要采用异步延时删除策略,这个就可以解决这种情况。我们首先获得同步时间,然后延迟合适的时间进行第二次删除,这样就可以解决这种问题。
总结
最后分析四种策略我们得出的最优策略就是先更新数据库,后删除缓存的策略。但是这个也不是绝对的。每种业务场景都有不同的解决方式。有些情况就不能删除缓存,只能使用更新缓存的场景。例如排行榜。这种场景一般都是更新缓存而不是删除缓存。
本文由 makese 于2020年02月01日发表在 我的博客
如未特殊声明,本站所有文章均为原创;你可以在保留作者及原文地址的情况下转载
转载请注明:缓存与数据库双写,不一致问题及解决方案 | 我的博客