一、Redis 缓存与数据库(DB)一致性问题
1、定义
缓存与数据库一致性,指的是缓存中的数据和数据库中的数据保持同步,不会出现 "缓存有数据、数据库无数据" 或 "两者数据内容不一致" 的情况。由于缓存和数据库是两个独立的存储组件,且请求操作存在时序差异,在高并发场景下极易出现数据不一致问题,这也是缓存使用中的核心难点。
2、核心矛盾与产生原因
- 核心矛盾:缓存更新策略与请求并发量的冲突,即 "何时更新 / 删除缓存" 与 "多个请求同时操作数据" 的时序问题。
- 主要产生原因:
- 更新数据时,先操作缓存还是先操作数据库,时序选择不当。
- 高并发场景下,多个请求同时更新数据,出现 "时序错乱"。
- 缓存操作(更新 / 删除)失败,数据库已更新但缓存未同步。
- 缓存过期机制导致的 "旧数据残留"。
3、两种核心更新策略(对比与选型)
(1)策略一:先更新数据库,再更新 / 删除缓存
这是工程实践中最常用、最推荐的策略,分为 "先 DB 后更新缓存" 和 "先 DB 后删除缓存" 两个细分方案,其中 "先 DB 后删缓存" 更为通用。
具体方案 A:先更新数据库,再删除缓存(Cache Aside Pattern,旁路缓存模式)
这是业界主流的标准方案,核心思想是 "数据库是数据的唯一权威来源,缓存仅作为查询加速"。
- 查询流程:请求 → 查缓存 → 缓存命中直接返回 → 缓存未命中 → 查数据库 → 将数据库结果写入缓存 → 返回结果。
- 更新流程:请求 → 更新数据库 → 删除对应缓存(不直接更新缓存) → 返回结果。
- 为何选择 "删除缓存" 而非 "更新缓存"?
- 减少无效操作:如果更新缓存后,长时间没有请求查询该数据,缓存中的更新数据就是 "无效数据",浪费内存。
- 避免复杂场景:部分数据更新需要多表关联计算,直接更新缓存会增加业务复杂度,而删除缓存后,后续查询会自动从数据库加载最新数据并回填缓存,更简洁。
-
解决并发不一致问题:该策略可能出现 "缓存刚被删除,新的查询请求已加载旧数据写入缓存" 的时序问题,可通过分布式锁 或给缓存设置短暂过期时间解决(后者更简单,容忍少量数据延迟)。
-
Java 示例:
// 更新商品数据(先更DB,再删缓存)
public void updateProduct(Long productId, Product newProduct) {
// 1. 先更新数据库(权威数据源)
productMapper.updateById(newProduct);
// 2. 再删除对应缓存(后续查询会自动回填最新数据)
String cacheKey = "product:" + productId;
redisTemplate.delete(cacheKey);
}// 查询商品数据(旁路缓存模式)
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 先查缓存
Product product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 2. 缓存未命中,查数据库
product = productMapper.selectById(productId);
if (product != null) {
// 3. 回填缓存(设置短暂过期时间,避免并发问题)
redisTemplate.opsForValue().set(cacheKey, product, 5, TimeUnit.MINUTES);
}
return product;
}
具体方案 B:先更新数据库,再更新缓存
- 适用场景:数据更新频率高,且查询频率也极高(如热门商品的库存数据),需要缓存数据实时性较强。
- 缺点:存在无效缓存、业务复杂度高,高并发下易出现 "更新覆盖错乱"(如请求 A 和请求 B 同时更新,A 先更 DB,B 后更 DB,但 A 后更缓存,导致缓存数据为 A 的旧数据),一般不推荐。
(2)策略二:先操作缓存,再更新数据库
这是不推荐在高并发场景下使用的策略,极易出现数据不一致问题。
- 核心问题:如果先删除 / 更新缓存,再去更新数据库,在数据库更新完成前,有新的查询请求进来,会从数据库加载旧数据写入缓存,导致后续请求获取到错误数据,且该数据会一直存在直到缓存过期。
- 示例:请求 A 删除缓存 → 请求 A 准备更新数据库(耗时 10ms) → 请求 B 查询缓存(未命中) → 请求 B 从数据库加载旧数据 → 请求 B 将旧数据写入缓存 → 请求 A 更新数据库完成。此时缓存中是旧数据,数据库中是新数据,出现一致性问题。
- 仅适用场景:低并发、对数据一致性要求极低的场景,几乎无工程实用价值。
4、进阶方案
如果业务对数据一致性要求极高(如金融场景),上述基础策略无法满足,可采用以下进阶方案:
- 分布式锁:更新数据时,先获取分布式锁,保证同一时刻只有一个请求更新数据库和缓存,避免并发时序问题。
- Canal 监听数据库 binlog:通过 Canal 监听 MySQL 的 binlog 日志,当数据库数据发生变化时,异步触发缓存更新 / 删除,实现 "最终一致性",这是高并发场景下兼顾性能和一致性的最优方案之一。
- TCC 事务:采用分布式事务的 TCC 模式,对数据库和缓存操作进行事务管理,保证两者要么都成功,要么都回滚(实现复杂,性能开销大)。
5、总结
- 优先选择「先更新数据库,再删除缓存」(Cache Aside Pattern),满足 90% 以上的业务场景。
- 缓存一致性无法做到 "强一致性",大多数场景下追求 "最终一致性" 即可。
- 避免过度设计,优先通过 "缓存短暂过期时间" 解决少量并发不一致问题,再逐步进阶。
二、Redis 事务
1、定义
Redis 事务是一组命令的集合,Redis 会将事务中的所有命令按顺序一次性执行,在执行过程中不会被其他客户端的命令插入或打断 ,即事务具有 "一次性、顺序性、隔离性"(注意:Redis 事务不满足关系型数据库的 "原子性",这是核心区别)。
2、与关系型数据库事务的核心区别
关系型数据库(如 MySQL)事务满足 ACID 原则,而 Redis 事务仅满足 "C(一致性)、I(隔离性)",不满足 "A(原子性)、D(持久性)",核心差异如下:
| 特性 | 关系型数据库事务 | Redis 事务 |
|---|---|---|
| 原子性 | 要么全部执行成功,要么全部回滚 | 命令要么按顺序执行,要么部分执行(错误命令不会影响正确命令执行,无回滚机制) |
| 持久性 | 事务提交后,数据永久写入磁盘 | 依赖 Redis 持久化策略(RDB/AOF),事务本身不保证持久性 |
| 隔离性 | 支持不同隔离级别(读未提交、读已提交等) | 事务执行过程中不会被其他命令打断,只有 "串行执行" 一种隔离级别 |
| 一致性 | 保证数据从一个合法状态变为另一个合法状态 | 仅保证命令按顺序执行,若命令本身逻辑错误,无法保证数据一致性 |
3、Redis 事务的核心命令
Redis 事务通过以下 4 个命令完成,不支持嵌套事务:
MULTI:开启事务 ,标记一个事务块的开始。执行MULTI后,后续输入的所有命令都会被放入 "事务队列" 中,不会立即执行,只会返回QUEUED。- 普通命令(如
SET、GET、HSET等):入队事务,这些命令不会立即执行,而是被添加到事务队列中,按输入顺序排队。 EXEC:执行事务 ,触发事务队列中所有命令的执行,返回所有命令的执行结果列表(按命令入队顺序)。如果事务被取消,EXEC返回nil。DISCARD:取消事务,清空事务队列,放弃执行事务,回到正常命令执行模式。- 补充命令
WATCH:事务乐观锁 ,用于监视一个或多个 key,如果在事务执行前(EXEC之前),被监视的 key 被其他客户端修改或删除,那么整个事务会被取消,EXEC返回nil。
4、事务的执行流程
- 开启阶段 :执行
MULTI命令,Redis 客户端进入事务模式。 - 入队阶段 :输入一系列普通命令,这些命令被依次放入事务队列,Redis 返回
QUEUED确认入队成功。 - 执行 / 取消阶段 :
- 执行
EXEC:Redis 按顺序执行事务队列中的所有命令,返回执行结果列表。 - 执行
DISCARD:清空事务队列,退出事务模式,不执行任何命令。
- 执行
5、实操示例
示例 1:正常执行事务
# 1. 开启事务
127.0.0.1:6379> MULTI
OK
# 2. 命令入队
127.0.0.1:6379> SET user:1 name "zhangsan"
QUEUED
127.0.0.1:6379> HSET user:1 age 25
QUEUED
127.0.0.1:6379> GET user:1
QUEUED
# 3. 执行事务
127.0.0.1:6379> EXEC
1) OK
2) (integer) 1
3) "zhangsan"
示例 2:取消事务(DISCARD)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET user:2 name "lisi"
QUEUED
# 取消事务
127.0.0.1:6379> DISCARD
OK
# 验证:事务未执行,user:2 不存在
127.0.0.1:6379> GET user:2
(nil)
示例 3:WATCH 乐观锁(事务被取消)
# 客户端A:监视 user:3,并开启事务
127.0.0.1:6379> WATCH user:3
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET user:3 name "wangwu"
QUEUED
# 客户端B:在客户端A执行 EXEC 前,修改 user:3
127.0.0.1:6379> SET user:3 name "zhaoliu"
OK
# 客户端A:执行事务,由于 user:3 被修改,事务取消
127.0.0.1:6379> EXEC
(nil)
# 验证:数据为客户端B修改的结果
127.0.0.1:6379> GET user:3
"zhaoliu"
6、Redis 事务的局限性
- 无回滚机制 :如果事务队列中的某个命令存在语法错误(入队时就能发现),则整个事务会被拒绝执行;但如果是运行时错误(如对字符串执行
INCR操作,入队时无法发现),则其他正确命令仍会执行,不会回滚。 - 不支持复杂事务逻辑 :无法在事务中实现条件判断(如
if-else),所有命令都是预先入队的,无法根据前一个命令的执行结果调整后续命令。 - 无持久性保证:事务执行成功后,数据仅存在于内存中,若此时 Redis 宕机,数据会丢失(除非开启了持久化)。
- 乐观锁
WATCH仅能监视有限数量的 key,且一旦事务被取消,需要重新执行WATCH和事务流程。
7、替代方案
对于复杂的业务场景,Redis 事务无法满足需求,可采用以下替代方案:
- Lua 脚本:Redis 会将整个 Lua 脚本作为一个原子操作执行,不会被其他命令打断,支持复杂逻辑判断,且可以实现 "要么全部执行,要么全部不执行" 的伪原子性,是 Redis 复杂操作的首选方案。
- Redis Cluster + 分布式事务:在集群环境下,可结合 TCC、Saga 等分布式事务模式,实现跨节点的事务一致性。
三、Redis 持久化
1、定义
Redis 是一款基于内存的键值数据库 ,所有数据默认存储在内存中,一旦 Redis 进程退出、服务器宕机或重启,内存中的数据会全部丢失。Redis 持久化就是将内存中的数据以某种格式写入到磁盘文件中,当 Redis 重启时,可通过磁盘文件恢复内存中的数据,避免数据丢失的机制。
2、两种核心持久化方式
Redis 提供了两种独立的持久化方式,可单独使用,也可组合使用,两者各有优劣,适用于不同的业务场景。
(一)RDB 持久化(Redis Database)
1、原理
RDB 是在指定的时间间隔内,将 Redis 内存中的所有数据生成一个 "快照"(二进制压缩文件,后缀为 .rdb),保存到磁盘上 的持久化方式。其核心是 "数据快照",即记录某一时刻的全量数据状态,类似于给数据拍了一张 "照片"。
2、触发方式(3 种)
-
手动触发 :
SAVE:同步执行快照生成,会阻塞 Redis 服务器的所有其他命令,直到 RDB 文件生成完成。适用于数据量较小、服务器空闲时使用,生产环境不推荐(会导致服务不可用)。BGSAVE:异步执行快照生成,Redis 会创建一个子进程来负责生成 RDB 文件,主进程继续处理客户端的请求,不会阻塞服务。这是生产环境中手动触发 RDB 的首选方式。
-
自动触发 :
通过 Redis 配置文件(redis.conf)中的
save <seconds> <changes>配置项触发,例如:save 900 1 # 900秒内(15分钟),至少有1个key发生变化,自动触发BGSAVE save 300 10 # 300秒内(5分钟),至少有10个key发生变化,自动触发BGSAVE save 60 10000 # 60秒内,至少有10000个key发生变化,自动触发BGSAVE
其他自动触发场景:Redis 主从复制时,从节点全量同步主节点数据,主节点会自动执行 BGSAVE 生成 RDB 文件发送给从节点;执行 FLUSHALL 命令清空所有数据时,若开启了 RDB 持久化,会生成一个空的 RDB 文件。
3、执行流程
- 触发 RDB 持久化(
BGSAVE为例)。 - Redis 主进程创建一个子进程(fork 操作),子进程拥有与主进程相同的内存数据副本。
- 主进程继续处理客户端请求,子进程负责将内存中的数据写入到临时 RDB 文件中。
- 子进程完成数据写入后,将临时 RDB 文件替换掉旧的 RDB 文件。
- 子进程退出,RDB 持久化完成。
4、优缺点
优点:
- 文件体积小:RDB 文件是二进制压缩格式,占用磁盘空间小,便于传输和备份(如跨机房同步、数据迁移)。
- 恢复速度快:恢复数据时,Redis 直接加载 RDB 文件到内存,无需执行额外命令,恢复速度远快于 AOF。
- 对性能影响小 :
BGSAVE采用子进程方式执行,主进程无需参与磁盘 I/O 操作,对 Redis 服务的性能影响极小。
缺点:
- 数据安全性低(存在数据丢失) :RDB 是周期性持久化,若在两次 RDB 快照之间 Redis 宕机,这段时间内的所有数据变更都会丢失,丢失数据量取决于
save配置的时间间隔。 - fork 操作开销大:生成 RDB 快照时,需要 fork 一个子进程,fork 操作会消耗大量内存(复制主进程的内存页表),如果数据量较大(如几十 GB),fork 操作会阻塞主进程短暂时间,影响服务性能。
- 不支持增量持久化:RDB 每次都是生成全量数据快照,即使只有少量数据变更,也需要重新生成整个 RDB 文件,随着数据量增大,持久化时间会越来越长。
(二)AOF 持久化(Append Only File)
1、原理
AOF 是将 Redis 执行过的所有写命令(如 SET、HSET、DEL 等),以文本格式(或二进制格式)追加到 AOF 文件的末尾 的持久化方式。其核心是 "记录命令",即记录数据的变更过程,类似于给数据的操作做了一份 "日志"。当 Redis 重启时,会重新执行 AOF 文件中的所有写命令,从而恢复内存中的数据。
2、开启方式
AOF 默认为关闭状态,需要在 Redis 配置文件(redis.conf)中手动开启:
appendonly yes # 开启 AOF 持久化(默认 no)
appendfilename "appendonly.aof" # AOF 文件名称(默认 appendonly.aof)
3、核心配置(3 个关键参数)
AOF 的核心是 "何时将缓冲区中的命令写入到磁盘文件",Redis 提供了 3 种同步策略,通过 appendfsync 配置:
# appendfsync 配置项可选值:
appendfsync always # 每次执行写命令后,立即将缓冲区中的命令同步到 AOF 文件(同步写)
appendfsync everysec # 每秒将缓冲区中的命令同步到 AOF 文件(每秒同步,默认值)
appendfsync no # 不主动同步,由操作系统负责将缓冲区中的数据写入 AOF 文件(异步写)
3 种同步策略对比:
| 同步策略 | 性能 | 数据安全性 | 适用场景 |
|---|---|---|---|
| always | 最差(每次写命令都要做磁盘 I/O) | 最高(几乎不丢失数据) | 金融、支付等对数据安全性要求极高的场景 |
| everysec | 较好(仅每秒做一次磁盘 I/O) | 较高(最多丢失 1 秒数据) | 大多数生产环境场景(兼顾性能和安全性) |
| no | 最好(无额外磁盘 I/O 开销) | 最差(丢失数据量不确定,取决于操作系统) | 对数据安全性无要求,追求极致性能的场景 |
4、解决 AOF 文件膨胀:AOF 重写
问题背景:
AOF 是不断追加写命令,随着时间推移,AOF 文件会变得越来越大(例如,对同一个 key 执行 1000 次 INCR 命令,AOF 会记录 1000 条命令)。过大的 AOF 文件会占用大量磁盘空间,且 Redis 重启时恢复数据的时间会大幅延长。
核心原理:
AOF 重写是创建一个新的 AOF 文件,用最少的命令来表示当前 Redis 内存中的所有数据,替换掉旧的 AOF 文件 ,从而实现 AOF 文件的 "瘦身"。例如,对同一个 key 执行 1000 次 INCR 命令,重写后的 AOF 文件中仅会保留一条 SET key 1000 命令。
触发方式(2 种):
-
手动触发 :执行
BGREWRITEAOF命令,Redis 会创建一个子进程来执行 AOF 重写,主进程继续处理客户端请求,不会阻塞服务。 -
自动触发 :通过 Redis 配置文件中的两个参数配合触发:
auto-aof-rewrite-percentage 100 # 当 AOF 文件体积增长到上一次重写后体积的 100%(即翻倍)时,触发重写 auto-aof-rewrite-min-size 64mb # AOF 文件的最小体积,只有当文件体积超过 64mb 时,才会触发自动重写
5、优缺点
优点:
- 数据安全性高 :支持 3 种同步策略,可根据业务需求选择,最多仅丢失 1 秒数据(
everysec策略),甚至不丢失数据(always策略)。 - 支持增量持久化:AOF 仅追加写命令,每次持久化只记录新增的命令,无需生成全量数据,持久化开销小。
- 文件可读性高:AOF 文件是文本格式(默认),可直接查看命令内容,甚至可以手动修改 AOF 文件(修复错误命令),便于问题排查和数据恢复。
缺点:
- 文件体积大:AOF 文件记录的是命令,相同数据情况下,AOF 文件体积远大于 RDB 文件,占用更多磁盘空间。
- 恢复速度慢:Redis 重启恢复数据时,需要重新执行 AOF 文件中的所有命令,随着文件体积增大,恢复时间会越来越长,远慢于 RDB。
- 对性能影响较大 :AOF 的同步策略会带来一定的磁盘 I/O 开销,尤其是
always策略,对 Redis 服务的性能影响较为明显。
(三)RDB 与 AOF 的组合使用(生产环境推荐)
1、组合策略
生产环境中,推荐同时开启 RDB 和 AOF 持久化,兼顾两者的优点,实现 "性能、安全性、恢复速度" 的平衡。
2、工作机制
当 Redis 重启时,会优先加载 AOF 文件恢复数据(因为 AOF 文件的数据更完整,丢失数据更少),只有当 AOF 持久化未开启或 AOF 文件损坏且无法修复时,才会加载 RDB 文件恢复数据。
3、核心优势
- 数据安全性:平时依赖 AOF 持久化,保证最少丢失 1 秒数据;定期通过 RDB 生成全量快照,便于数据备份和快速恢复。
- 恢复速度:Redis 重启时,若 AOF 文件过大,可先通过 RDB 文件快速恢复大部分数据,再通过 AOF 文件恢复增量数据,提升恢复效率。
- 灵活性:既可以通过 RDB 文件进行跨机房数据迁移、备份,又可以通过 AOF 文件保证数据的高安全性。
3、对比总结
| 对比维度 | RDB | AOF |
|---|---|---|
| 核心思想 | 记录数据状态(快照) | 记录数据变更过程(命令) |
| 文件格式 | 二进制压缩格式 | 文本格式(默认)/ 二进制格式 |
| 数据安全性 | 较低(周期性持久化,丢失数据较多) | 较高(最多丢失 1 秒数据) |
| 恢复速度 | 快(直接加载快照到内存) | 慢(重新执行所有命令) |
| 磁盘占用 | 小 | 大 |
| 对性能影响 | 小(子进程异步执行) | 较大(磁盘 I/O 开销) |
| 适用场景 | 数据备份、跨机房迁移、对数据丢失容忍度较高的场景 | 对数据安全性要求较高的大多数生产环境场景 |