什么是延迟双删?
延迟双删是一种Redis和数据库之间一致性问题的解决方法。
并发读写时可能出现:
- 线程A删除缓存
- 线程B读缓存未命中,从数据库读取旧值
- 线程B将旧值写入缓存
- 线程A更新数据库 → 缓存中保留脏数据
具体过程其实就是:
1、删除缓存
2、更新数据库
3、删除缓存
这里举一个简单的例子
public void updateData(String key, Object value) {
// 1. 第一次删除缓存
redis.delete(key);
// 2. 更新数据库
db.update(key, value);
// 3. 延迟500ms再删一次(异步)
new Thread(() -> {
try {
Thread.sleep(500);
redis.delete(key);
} catch (Exception e) {
// 记录日志
}
}).start();
}
第一次删除的原因:
之所以选择先删除缓存,而不是直接更新数据库,主要是因为先更新数据库的话会存在一个比较关键的问题就是缓存的更新和数据库的更新不是一个原子操作,那么就存在失败的可能性。
如果数据库写成功了,删除缓存失败,那么就会出现数据不一致的情况。
如果删除缓存成功了,数据库更新失败了,如果现在有用户查询该数据也只不过是更新下缓存,不会有错误数据,也不会产生数据数据不一致的情况。
并且,相对于缓存和数据库来说,数据库的失败的概率更大一些,并且删除动作和更新动作来说,更新的失败的概率也会更大一些。
所以,为了避免这个因为两个操作无法作为一个原子操作而导致的不一致问题,我们选择先删除缓存,再更新数据库。这是第一次删除缓存的原因。
操作顺序 | 数据库成功 | 缓存成功 | 结果 |
---|---|---|---|
先删缓存→后更新DB | 成功 | 成功 | 一致 |
先删缓存→后更新DB | 失败 | 成功 | 缓存空(无害) |
先更新DB→后删缓存 | 成功 | 失败 | 不一致(缓存旧数据) |
第二次删除的原因:
一般来说,一些并发量不大的业务,这么做就已经可以了,先删缓存,后更新数据(如果业务量不大,其实先更新数据库,再删除缓存其实也可以),基本上就能满足业务上的需求了。
但是如果是并发量比较高的话,那么就可能存在一定的问题。
如何避免缓存在更新后,又被一个其他的线程给把脏数据覆盖进去呢,那么就需要第二次删除了,就是我们的延迟双删。
因为“读写并发”的问题会导致并发发生后,缓存中的数被读线程写进去脏数据,那么就只需要在写线程在删缓存、写数据库之后,延迟一段时间,再执行一把删除动作就行了。
这样就能保证缓存中的脏数据被清理掉,避免后续的读操作都读到脏数据。当然,这个延迟的时长也很有讲究,到底多久来删除呢? 一般建议设置1-2s就可以了
当然,这种方案也是有一个弊端的,那就是可能会导致缓存中准确的数据被删除掉。当然这也问题不大就像我们前面说过的,只是增加一次cache miss罢了。
所以,为了避免因为先删除缓存而导致的”读写并发问题“被放大的情况,所以引入了第二次缓存删除。