讲讲 Redis 缓存更新一致性

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 讲讲 Redis 缓存更新一致性


当执行写操作后,需要保证从缓存读取到的数据与数据库中持久化的数据是一致的,因此需要对缓存进行更新。

因为涉及到数据库和缓存两步操作,难以保证更新的原子性。

在设计更新策略时,我们需要考虑多个方面的问题:

  • 对系统吞吐量的影响:比如更新缓存策略产生的数据库负载小于删除缓存策略的负载
  • 并发安全性:并发读写时某些异常操作顺序可能造成数据不一致,如缓存中长期保存过时数据
  • 更新失败的影响:若某个操作失败,如何对业务影响降到最小
  • 检测和修复故障的难度: 操作失败导致的错误会在日志留下详细的记录容易检测和修复。并发问题导致的数据错误没有明显的痕迹难以发现,且在流量高峰期更容易产生并发错误产生的业务风险较大。

更新缓存有两种方式:

  • 删除失效缓存: 读取时会因为未命中缓存而从数据库中读取新的数据并更新到缓存中
  • 更新缓存: 直接将新的数据写入缓存覆盖过期数据

更新缓存和更新数据库有两种顺序:

  • 先数据库后缓存
  • 先缓存后数据库

两两组合共有四种更新策略,现在我们逐一进行分析。

并发问题通常由于后开始的线程却先完成操作导致,我们把这种现象称为“抢跑”。下面我们逐一分析四种策略中“抢跑”带来的错误。

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

若数据库更新成功,删除缓存操作失败,则此后读到的都是缓存中过期的数据,造成不一致问题。

可能存在读写线程竞争导致的并发错误:

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

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

同删除缓存策略一样,若数据库更新成功缓存更新失败则会造成数据不一致问题。

该策略同样存在读写线程竞争导致数据不一致的问题:

也可能因为两个写线程竞争导致并发错误:

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

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

可能发生的并发错误:

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

若缓存更新成功数据库更新失败, 则此后读到的都是未持久化的数据。因为缓存中的数据是易失的,这种状态非常危险。

因为数据库因为键约束导致写入失败的可能性较高,所以这种策略风险较大。

可能发生的并发错误:

两个写线程竞争也会导致数据不一致:

解决方案

使用 CAS

CAS (Check-And-Set 或 Compare-And-Swap)是一种常见的保证并发安全的手段。CAS 当且仅当客户端最后一次取值后该 key 没有被其他客户端修改的情况下,才允许当前客户端将新值写入。

func CAS(oldVal, newVal) {
    if cache.get() == oldVal {
        cache.set(newVal)
    }
}
时间 线程A 线程B 数据库 缓存
0

v0 v0
1 更新数据库为 v1
v1 v0
2
更新数据库为 v2 v2 v0
3
执行 CAS 操作:当且仅当缓存中为 v0 时将 v2 写入缓存 v2 v2
4 执行 CAS 操作:当且仅当缓存中为 v0 时将v1写入缓存。当前缓存为 v2 故放弃写缓存
v2 v2

由上图可见,CAS 可以有效的避免并发错误的发生。

目前一些兼容 Redis 协议的中间件已经提供了 CAS 命令的支持,比如阿里的 Tair 以及腾讯的 Tendis。

Redis 官方提供了 Watch + 事务的方法来支持 CAS, 或者使用 redis 中 lua 脚本原子性执行的特点来实现 CAS。不过由于代码较为复杂,这两种方案都不常见。

使用分布式锁

CAS 假设发生并发问题的概率不大, 所以 CAS 也被称为乐观锁。那么悲观锁能否解决我们的问题呢?

还是以「先更新数据库,再更新缓存」方案中两个写线程竞争为例, 我们要求任何线程在写入或读取数据库前都需要获取排它锁。

时间 线程A 线程B 数据库 缓存
0

v0 v0
1 获取排它锁
v0 v0
2 更新数据库为 v1
v1 v0
3 更新缓存为 v1
v1 v1
4
等待排它锁 v1 v1
5 释放排它锁
v1 v1
6
获得排它锁 v1 v1
7
更新数据库为 v2 v2 v1
8
更新缓存为 v2 v2 v2
9
释放排它锁 v2 v2

分布式锁同样可以解决并发问题,只是成本可能略高。

异步更新

阿里开源了 MySQL 数据库binlog的增量订阅和消费组件 - canal。canal 模拟从库获得主库的 binlog 更新,然后将更新数据写入 MQ 或直接进行消费。

我们可以让API服务器只负责写入数据库,另一个线程订阅数据库 binlog 增量进行缓存更新。

因为 binlog 是有序的,因此可以避免两个写线程竞争。但我们仍然需要解决读写线程竞争的问题:

这里同样可以 CAS 解千愁:

延时双删

使用删除缓存策略时读线程先开始却后写缓存会导致不一致,那么我们在读线程结束后再次清除缓存是不是就可以解除错误状态了?延时双删就是写线程等待一段时间“确保”读线程都结束后再次删除缓存,以此清除可能的错误缓存数据。

理论上我们无法给出一个时间来“确保”读线程都结束,所以仍有存在并发问题的可能。但是延时双删实现成本很低而且极大的减少了并发问题出现的概率,不失为一种简单实用的手段。



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
存储 缓存 NoSQL
redis的两种持久化机制及底层原理实现
🍅程序员小王的博客:程序员小王的博客 🍅 欢迎点赞 👍 收藏 ⭐留言 📝 🍅 如有编辑错误联系作者,如果有比较好的文章欢迎分享给我,我会取其精华去其糟粕 🍅java自学的学习路线:
519 0
redis的两种持久化机制及底层原理实现
|
2月前
|
canal 缓存 NoSQL
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;先删除缓存还是先修改数据库,双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
|
4月前
|
缓存 NoSQL Redis
软件体系结构 - 缓存技术(7)Redis持久化方法
【4月更文挑战第20天】软件体系结构 - 缓存技术(7)Redis持久化方法
111 14
|
4月前
|
缓存 NoSQL Redis
深度解析Redis的缓存双写一致性
【4月更文挑战第20天】
216 1
|
4月前
|
缓存 NoSQL 关系型数据库
Redis 缓存 一致性
Redis 缓存 一致性
42 0
|
12月前
|
存储 缓存 NoSQL
Redis 缓存使用技巧和设计方案,建议收藏!
Redis 缓存使用技巧和设计方案,建议收藏!
1060 1
Redis 缓存使用技巧和设计方案,建议收藏!
|
11月前
|
缓存 NoSQL 算法
Redis 过期删除机制探究
Redis 过期删除机制探究
|
11月前
|
存储 缓存 NoSQL
Redis 基础知识和核心概念解析:理解 Redis 的键值操作和过期策略
Redis 基础知识和核心概念解析:理解 Redis 的键值操作和过期策略
109 1
|
缓存 NoSQL Redis
redis做为缓存,数据的持久化是怎么做的?
redis做为缓存,数据的持久化是怎么做的?
87 0
|
canal 消息中间件 缓存
讲讲 Redis 缓存更新一致性
讲讲 Redis 缓存更新一致性