# 1 更新策略
# 1.1 先更新缓存,再更新数据库
问题:
更新缓存成功,更新数据库失败;
并发问题
时间 线程A 线程B 数据库 缓存 0 v0 v0 1 更新缓存为v1 v0 v1 2 更新缓存为v2 v0 v2 3 更新数据库为v2 v2 v2 4 更新数据库为v1 v1 v2
# 1.2 先更新数据库,再更新缓存
问题:
数据库更新成功,缓存更新失败,在缓存失效之前都有问题
并发问题:
时间 线程A 线程B 数据库 缓存 0 v0 v0 1 更新数据库为v1 v1 v0 2 更新数据库为v2 v2 v0 3 更新缓存为v2 v2 v2 4 更新缓存为v1 v2 v1
# 2 删除策略
# 2.1 先删除缓存,再更新数据库
问题:
删除缓存成功,更新数据库失败(这不是什么大问题)
并发问题:
时间 线程A 线程B 数据库 缓存 0 删除缓存v0 v0 1 读取数据库v0 v0 2 更新数据库为v1 v1 3 更新缓存v0 v1 v0 4 删除缓存(延时双删策略) v1
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
// 这个时间不太好确定
Thread.sleep(1000);
redis.delKey(key);
}
要自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可;但是如果第二次删除缓存失败,还是会产生脏数据
# 2.2 先更新数据库,再删除缓存
问题:
- 更新数据库成功,删除缓存失败,在缓存失效之前都有问题
- 并发问题:
时间 线程A 线程B 数据库 缓存 0,缓存刚好失效 读取数据库v0 v0 1 更新数据库为v1 v1 2 删除缓存 v1 3 更新缓存为v0 v1 v0 4 删除缓存(延时双删策略) v1
# 3 删除缓存失败(采用删除策略)
- 2.1 与 2.2 延迟双删可能删除缓存失败
- 2.2 相对于 2.1 来说发生并发问题的可能性更低,因为刚好读缓存、写数据库、缓存失效三者并发;而且写数据库不太可能比读数据库慢
删除策略还有问题,就是缓存击穿问题;可以考虑使用两级缓存来解决不一致问题;一级和二级失效的时间不一样,让读请求永远打不到数据库
# 3.1 第一种删除重试机制
流程如下所示
- 更新数据库数据
- 缓存因为种种问题删除失败
- 将需要删除的key发送至消息队列
- 自己消费消息,获得需要删除的key
- 继续重试删除操作,直到成功
# 3.2 第二种删除重试机制
上面方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作
流程如下图所示:
- 更新数据库数据
- 数据库会将操作信息写入binlog日志当中
- 订阅程序提取出所需要的数据以及key(订阅binlog程序在mysql中有现成的中间件叫canal)
- 另起一段非业务代码,获得该信息
- 尝试删除缓存操作,发现删除失败
- 将这些信息发送至消息队列
- 重新从消息队列中获得该数据,重试操作
# 4 其他
# 4.1 Cache Aside Pattern
Cache Aside Pattern 是处理缓存一致性问题的一种模式,其实就是 2.2 的策略
# 4.2 Read/Write Through Pattern
将缓存服务作为主要的存储,应用的所有读写请求都是直接与缓存服务打交道,而不管最后端的数据库了,数据库的数据由缓存服务来维护和更新。不过缓存中数据变更的时候是同步去更新数据库的,在应用的眼中只有缓存服务,流程就相当简单了:(LevelDB、RocksDB、TiDB )
- 读取:应用要读数据和更新数据都直接访问缓存服务
- 更新:缓存服务同步的将数据更新到数据库
# 4.3 Write Behind Caching Pattern
是Read/Write Through模式的一个变种。区别就是Read/Write Through模式的缓存写数据库的时候是同步的,而Write Behind模式的缓存操作数据库是异步的,流程如下:
- 应用要读数据和更新数据都直接访问缓存服务
- 缓存服务异步的将数据更新到数据库(通过异步任务)