缓存和数据库的数据一致性问题 | 青训营

104 阅读3分钟

参考的小林coding,是给自己看的省流!

遇上写操作时,有两种方式写策略:

  1. 更新数据库,更新缓存(省流,不太行)
  2. 更新数据库,删除缓存(省流,还行)

更新数据库,更新缓存

先更新数据库,再更新缓存

结论:会出现数据不一致请求

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 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。