数据库缓存一致性
原则
Redis 缓存和数据库一般采用最终一致性
- 第一考虑缓存的一致性
- 第二考虑并发一致性问题 多个线程操作一个key,要加锁
- 第三考虑缓存的利用率
默认配置
一般会默认配置Redis缓存的过期时间,到期后会自动更新
有些场景会配置定时刷缓存的任务,例如每天晚上更新ip白名单场景
优点
- 配置简单
不足
- 如果更新 mysql 成功,更新 redis 却失败,就退化到了redis定时刷新;
- 在高并发场景,业务 server 需要和 mysql,redis 同时进行连接。这样是损耗双倍的连接资源,容易造成连接数过多的问题
方案一 先刷DB再刷Redis
可能会有不一致,以及并发场景下会有问题
如何保证第二部成功
异步重试 - 重试太多还是失败,删除缓存
把操作缓存这一步,直接放到消息队列中,由消费者来操作缓存。
针对方案二的同步写 redis 进行优化,增加消息队列,将 redis 更新操作交给 kafka,由消息队列保证可靠性,再搭建一个消费服务,来异步更新 redis。
优点
- 消息队列可以用一个句柄,很多消息队列客户端还支持本地缓存发送,有效解决了方案二连接数过多的问题;
- 使用消息队列,实现了逻辑上的解耦;
- 消息队列本身具有可靠性,通过手动提交等手段,可以至少一次消费到 redis。
不足
- 依旧解决不了时序性问题,如果多台业务服务器分别处理针对同一行数据的两条请求,举个栗子,a = 1; a = 5;,如果 mysql 中是第一条先执行,而进入 kafka 的顺序是第二条先执行,那么数据就会产生不一致。
- 引入了消息队列,同时要增加服务消费消息,成本较高。
方案二 删除缓存延迟双删
先删除缓存,在更新DB
存在并发问题
为了解决刷缓存期间,缓存去从库里取值,获取的是旧值的问题
在线程 A 删除缓存、更新完数据库之后,先「休眠一会」,再「删除」一次缓存。
为什么删除两次?
防止并发场景,被其他线程刷新
为什么延迟?
因为数据库有主从,redis去从库读,由于主还没有完全同步给从,导致redis读到旧的值
延迟多久呢?
延迟时间要大于「主从复制」的延迟时间
延迟时间要大于线程 B 读取数据库 + 写入缓存的时间
通常可能是1-5s
方案三 binLog
只需要关注数据库,拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。
- 在 mysql 压力不大情况下,延迟较低;
- 和业务完全解耦;
- 解决了时序性问题。
缺点
- 要单独搭建一个同步服务,并且引入 binlog 同步机制,成本较大。
推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。
总结
1、性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案
2、掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存 + 数据库一起成功问题
3、失败场景下要保证一致性,常见手段就是「重试」,同步重试会影响吞吐量,所以通常会采用异步重试的方案
4、订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致
\