参考的小林coding,是给自己看的省流!
遇上写操作时,有两种方式写策略:
- 更新数据库,更新缓存(省流,不太行)
- 更新数据库,删除缓存(省流,还行)
更新数据库,更新缓存
先更新数据库,再更新缓存
结论:会出现数据不一致请求
A 线程更新完数据库,更新缓存之前,B 线程已经完成两次操作,数据不一致。版本:数据库:B,缓存:A
先更新缓存,再更新数据库
结论:会出现数据不一致请求
A 线程更新完缓存,更新数据库之前,B 线程已经完成两次操作,数据不一致。版本:数据库:A,缓存:B
更新数据库,删除缓存
先删缓存,再更新数据库
结论:会出现数据不一致请求
A 线程删完缓存之后,更新数据库之前(写操作),B 线程读数据库,存入缓存,数据不一致。版本:数据库:新,缓存:旧
⭐先更新数据库,再删缓存
结论:一般不会出现不一致
先更新数据库,数据库一定是最新的,再删除缓存,之后缓存的数据就来源于数据库的新数据
但可能先读了老数据,然后再删除缓存之后再进行了写回,造成不一致
但一般情况不会,因为数据库的写操作时间相对长,所以你写回的时间一般靠前,除非你喜欢卡点
并且记得加上过期时间,因为万一不一致,读的一直是老数据,还得让老数据过期
其他操作
延迟双删(先删缓存,再更新数据库)
第一次删是删除老数据,但是新线程进入就会读入老数据
所以需要在更新数据库后继续删除
# 删除缓存
redis.del(X)
# 更新数据库
db.update(X)
# 睡眠 / 异步通知(mq)
Thread.sleep(N) / mq.Notify(N)
# 再删除缓存
redis.del(X)
请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间
重试机制(先更新数据库,再删缓存)
因为更新数据库和删除缓存不是原子操作,可能删除缓存这一步寄掉了,所以需要保证他成功
比如用 mq,让一个消费者去删,在一定程度上重试
订阅 binlog(mysql)
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
下图是 Canal 的工作原理:
所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。