数据库缓存双写一致性的实现方案
对于常规项目来讲,为数据库层再加上一层缓存提升接口响应速度是我们的常规高并发手段,但是,在高并发情况下,对于数据的读写,可能会出现数据不一致的问题
在开始介绍方案之前,我们先还原一下业务场景:
如果我们为数据库添加了缓存,在读写场景下,是这样操作数据的:
- 读:先读取缓存,如果缓存没有数据,就读取数据库中的数据,然后再写入到缓存中
- 写:在判断缓存中没有数据后,就读取数据库中的数据写入到缓存中,或者当有更新请求过来时,需要修改数据库内容
主要是在更新数据库数据的场景下,此时如果有读数据的请求过来,可能会读取到旧数据
我们此时就要制定读写方案,来保证不同场景下的数据一致性
解决方案
1.先删除缓存,再操作数据库
如果有更新请求过来,此时先删除缓存(脏数据),再更新数据库,就可以保证数据库内的数据是最新的,并且如果有读请求过来时,就会自动将新的数据存在缓存中
但是,这种方法在高并发请求下,可能会发生数据库和缓存不一致的情况:
如图:当线程1更新数据时,线程2发起查询数据请求,此时线程1刚删除缓存,cpu时间片就给了线程2,此时线程2查询缓存,发现未命中,然后将数据库的旧数据写入缓存,此时cpu时间片给到线程1,线程1继续更新数据库,最后缓存里的是旧数据,数据库中的是新数据;
2.先操作数据库,再删除缓存(旁路缓存模式)
如果有更新请求过来,先操作数据库,再删除缓存。
在高并发下,同样会出现不一致的情况:
如图:当线程1读取数据时,发现未命中缓存,然后查询数据库,此时线程2发起更新数据库请求,拿到了cpu时间片,更新数据库并删除缓存,最后线程1拿去了cpu时间片,写入了旧数据的缓存,仍然会发生不一致的情况
对于以上场景,如果我们允许一定的不一致性,可以使用如上方法,但是对于较强一致性的情况,需要其他方案:
3.延迟双删
通过上面两个场景我们会发现,脏数据主要在缓存中,如果我们能够删除缓存中的脏数据就可以了,但是由于更新缓存的是另外一个读请求,我们写入到数据库中请求如何删除呢,我们可以在操作数据库后,延迟一段时间再删除脏数据即可
延迟双删就是在旁路缓存模式先删除缓存再操作数据库,最后延迟一段时间删除缓存,这样就可以避免写入缓存脏数据
为什么要延迟删除而不是直接删除呢,因为我们如果在脏数据写入前删除了,就会删除空数据,脏数据仍然会写入,所以我们要定义一个准确的时间,确保脏数据写入后再删除,这个时间就是延迟的时间,并且,我们的数据库是主从模式的,需要延时等待同步以后再删除缓存,避免脏数据
但是,延时时间也需要精准把握,并且,同样有脏数据风险,只不过相对于旁路缓存模式要低
4.异步更新
异步更新一般是:先操作数据库,再异步更新缓存,好处是数据一致性风险低,但是存在 “缓存滞后” 窗口(数据库更新后,缓存未更新前读旧值)
5.分布式锁
如果业务需要保证强一致性,就可以使用分布式锁,当修改数据库时,不允许读请求和写请求过来,这样就可以保证数据一致性
存在 “缓存滞后” 窗口(数据库更新后,缓存未更新前读旧值)
使用分布式锁,将删除缓存和写数据这两个操作绑定,当执行这两个操作时,其他线程进不来,所以保证了强一致性,但是性能会有所损失,但是
我们可以使用Redis的读写锁,来提高性能
使用异步通知来保证数据的最终一致性:
使用MQ来保证缓存更新
异步通知通过可靠的消息传递、幂等性处理、状态补偿等机制,解决了分布式系统中因同步阻塞、网络不可靠、子系统故障导致的数据不一致问题
这种操作保证了数据的安全性,但是在高并发条件下依然可能会发生线程不安全