redis是一个高性能键值对(key-value)内存数据库,主打内存存储、超快读写是基于内存中的数据结构存储系统。

redis 是高性能键值对(k-v)内存数据库,主打内存存储、超快读写、多数据类型,既可以做缓存,也能做数据库,也能做消息中间件、分布式锁、限流等,

redis通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。

  • String: 字符串
  • Hash: 散列
  • List: 列表
  • Set: 集合
  • Sorted Set: 有序集合

基础 5 种:String、Hash、List、Set、ZSet

高级 3 种:Bitmap、HyperLogLog、GEO

结构 特点 典型场景
String 简单键值 缓存、计数器、锁
Hash 对象存储 用户信息、商品
List 有序可重复 队列、评论
Set 无序去重 点赞、共同好友
ZSet 排序去重 排行榜、热度
Bitmap 位存储 签到、状态
HyperLogLog 统计数量 UV、日活
GEO 地理位置 附近的人

redis 与其他 key - value 缓存产品有以下三个特点:

  • redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • redis支持数据的备份,即master-slave模式的数据备份。

redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – redis支持Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 纯内存运行,读写速度极快(读 11 万 / 秒,写 8 万 / 秒)
  • 支持丰富数据类型(string、hash、list、set、zset 等)
  • 单线程 + I/O 多路复用,无锁竞争
  • 可持久化(把内存数据存硬盘,重启不丢)
  • 支持主从、哨兵、集群高可用方案

关系型数据库与非关系型数据库

关系型数据库:采用关系模型来组织数据的数据库,关系模型就是二维表格模型。一张二维表的表名就是关系,二维表中的一行就是一条记录,二维表中的一列就是一个字段

非关系型数据库

非关系型,一般不保证ACID原则的数据存储系统。是键值对存储,结构不固定,严格上讲不是一种数据库,而是一种数据结构化存储方法的集合

redis 数据类型

redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

string(字符串)

string 是 redis 最基本的类型,一个 key 对应一个 value。

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。

redis 能存图片吗?可以存,但不推荐

redis 是内存型键值数据库,本身不直接识别图片,但可以把图片转成二进制字节流(bytes),以字符串(string)类型存储。

存储方式

  1. 把图片文件读取为二进制数据

  2. key-value 形式存入 Redis 的 string 类型

  3. 读取时再把二进制数据还原成图片

为什么不推荐用 Redis 存图片?

1. 内存成本极高

  • Redis 是内存存储,内存价格是硬盘的几十倍
  • 一张 1MB 的图片,10 万张就是 100GB 内存,成本爆炸

2. 浪费 Redis 核心性能

  • Redis 设计用来做缓存、计数器、会话、消息队列等高频小数据操作
  • 存大体积图片会挤占内存,拖慢其他核心业务

3. 大 key 风险(Redis 大忌)

  • Redis 官方建议:单个 value 不超过 10KB

  • 图片动辄几十 KB~ 几 MB,属于大 key

    • 导致 Redis 阻塞、卡顿

    • 主从同步、集群迁移极慢

    • 容易引发线上故障

什么场景可以用 Redis 存图片?

只有极小、极高频的图片才适合:

  • 极小头像(<5KB)、图标

  • 验证码图片(一次性、体积小)

  • 临时二维码(短期缓存)

并且必须配合:

  • 设置过期时间(不要永久存储)

  • 控制大小(<10KB)

标准架构

  1. 图片上传到 专业对象存储

    阿里云 OSS、腾讯云 COS、七牛云、MinIO(自建)
  2. 把图片 URL存入 Redis

  3. 业务读取 URL,前端直接加载图片

string 类型是 Redis 最基本的数据类型,注意:string 类型的值最大能存储 512MB。

hash(哈希)

redis hash 是一个键值(key=>value)对集合。

redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

list(列表)

redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

set(集合)

redis 的 set 是 string 类型的无序集合。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)

zset(sorted set:有序集合)

redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

5 种最常用数据类型 & 命令

1. String(字符串)

最基础类型,存数字、文本、JSON

SET key value # 存

GET key # 取

DEL key # 删

INCR key # 自增(计数器、点赞数)

使用场景:缓存、计数器、分布式锁、Session 共享

2. hash(哈希 / 对象)

适合存对象(如用户信息)

HSET user id 1 name "张三"

HGET user name

HGETALL user # 取全部

场景:用户信息、商品详情、配置信息

3. List(列表)

有序可重复,双向链表

LPUSH list a b c # 左插

RPUSH list d # 右插

LRANGE list 0 -1 # 查看所有

LPOP list # 左弹出

场景:消息队列、栈、队列、评论列表

4. Set(集合)

无序、不重复

SADD set a b c

SISMEMBER set a # 判断是否存在

SMEMBERS set # 查看所有

场景:去重、共同好友、点赞用户记录

5. ZSet(有序集合)

带分数(score)排序,不重复

ZADD rank 100 "小明" 90 "小红"

ZRANGE rank 0 -1 WITHSCORES # 按分数从小到大

场景:排行榜、热度排序、

redis 两大持久化机制(防止数据丢失)

1. RDB(快照)

  • 定时把内存数据全量保存到硬盘
  • 优点:恢复快、文件小
  • 缺点:可能丢最后一次快照后的数据

2. AOF(日志)

  • 记录每一条写命令
  • 优点:数据几乎不丢
  • 缺点:文件大、恢复慢

企业常用:RDB + AOF 混合模式

Redis持久化方案

  1. 做了数据操作的命令后,输入指令bgsave就持久化了
  2. rdb方案
  3. aof方案

redis 高可用方案

1. 主从复制

  • 主节点写,从节点读
  • 分担读压力

2. 哨兵(sentinel)

  • 自动监控主节点
  • 主节点挂了自动切换从节点为主

3. redis Cluster(集群)

  • 数据分片存储(多主多从)
  • 支持海量数据 + 高并发

Redis 为什么快?

内存操作 + 单线程避免锁 + IO 多路复用

缓存常见问题?

  • 缓存穿透、击穿、雪崩

    1. 穿透:查不存在的数据 → 布隆过滤器
    2. 击穿:热点 key 过期 → 互斥锁 / 永不过期
    3. 雪崩:大量 key 同时过期 → 随机过期时间、集群

Redis 分布式锁怎么实现?

  • SET key value NX PX 3000

  • Redis 淘汰策略 volatile-lru(最常用:删除最近最少使用的过期 key)

数据结构实现?

  1. ZSet 底层用什么实现? 压缩列表(ziplist)+ 跳表(skiplist)
  2. Hash 底层? ziplist / hashtable
  3. List 底层? quicklist(双向链表 + 压缩列表)
  4. 为什么 Redis 用跳表不用红黑树? 范围查询更快、实现更简单

     zip:压缩     skip:跳过   quick:快  Hyper:超级   Bitmap 位图  GEO:地理空间

  • 基础 5 种:String、Hash、List、Set、ZSet
  • 高级 3 种:Bitmap、HyperLogLog、GEO
结构 特点 典型场景
String 简单键值 缓存、计数器、锁
Hash 对象存储 用户信息、商品
List 有序可重复 队列、评论
Set 无序去重 点赞、共同好友
ZSet 排序去重 排行榜、热度
Bitmap 位存储 签到、状态
HyperLogLog 统计数量 UV、日活
GEO 地理位置 附近的人

缓存三大问题

缓存穿透、缓存击穿、缓存雪崩

整体场景:请求优先查询缓存,缓存无数据再查询数据库。

(一)缓存穿透

定义:查询缓存、数据库都不存在的数据,无效请求全部直达数据库。
成因:恶意请求非法参数、业务查询无结果数据。
解决方案:
空值缓存:查询为空时,缓存空数据,设置较短过期时间;
参数校验:网关 / 接口拦截非法 ID、格式异常请求;
布隆过滤器:前置过滤非法 Key,海量数据场景优选;
限流:对异常 IP、高频无效请求做流量限制。

布隆过滤器(Bloom Filter)

解决缓存穿透神器

布隆过滤器 = 一个超级省空间的 “存在性判断工具”,作用:快速判断一个元素 一定不存在 / 可能存在

为什么要用?(解决缓存穿透)

缓存穿透问题

用户查一个根本不存在的数据

  1. 先查 Redis → 没有
  2. 再查数据库 → 也没有
  3. 大量这种请求 → 数据库压力爆炸 → 宕机

布隆过滤器就是用来挡住这些无效请求的!

核心特点

如果判断不存在 → 一定真的不存在(100% 准确)

如果判断存在 → 可能存在,也可能不存在(有误判率)

不存真实数据,只存哈希标记 → 极省空间

不能删除元素(这是硬伤)

原理

  1. 准备一个全是 0 的 bit 数组
  2. 一个元素进来,用 多个哈希函数 算出多个位置
  3. 把这些位置的 bit 改成 1
  4. 判断时:只要有一个位置是 0 → 一定不存在
  5. 全部是 1 → 可能存在

多个哈希是为了降低误判率


优点 & 缺点

优点

  • 空间极小(百万数据只占几 MB)
  • 查询速度极快
  • 抗缓存穿透神器

缺点

  • 有误判率(可以调小,但不能消除)
  • 不支持删除

1. 布隆过滤器为什么不能删?

因为多个元素可能共用同一个 bit 位,删一个会影响其他元素。

2. 怎么解决删除问题?

用计数布隆过滤器,或直接重建过滤器。

3. 缓存穿透怎么解决?

布隆过滤器 + 空值缓存

总结

  • 布隆过滤器 = 判断存在性的高效工具
  • 核心作用:防缓存穿透
  • 判断规则:不存在必真,存在不一定真

(二)缓存击穿

定义:单个热点 Key过期,瞬时大量并发请求直接打到数据库。
成因:秒杀、爆款、首页等高访问热点数据统一过期。
解决方案:
分布式互斥锁:仅一个请求查询数据库并更新缓存,其余请求等待;
热点 Key 永不过期:代码异步定时刷新数据,规避过期问题;
逻辑过期:存储逻辑过期时间,过期后异步更新缓存,旧数据正常返回。

(三)缓存雪崩

定义:大量缓存数据同时失效,海量请求全部压到数据库,把数据库拖垮、服务瘫痪,瞬时大量并发请求直接打到数据库。
成因:

  • 首页、活动爆款、秒杀商品这类热点数据,缓存设置了相同过期时间。时间一到,这批缓存集体失效。 瞬间成千上万的请求拿不到缓存数据,全都直奔数据库查询,数据库压力暴增,轻则查询卡顿,重则宕机,整个业务受影响。

  • 缓存服务整体宕机,Redis / 缓存集群挂了,所有请求都无法走缓存,全部击穿到数据库,同样引发雪崩。

缓存雪崩 两类场景

场景 1:大量缓存 Key 同时过期(最常见)

场景 2:缓存集群整体宕机 / 不可用

解决方案:

1. 过期时间加随机值(最简单、优先使用)

原理:给缓存过期时间增加随机偏移量,打散过期时间,避免批量 Key 同一时刻失效。

2. 热点 Key 永不过期

原理:热点数据不设置主动过期,通过定时任务异步更新缓存,从根源杜绝过期。

3.分布式互斥锁(防并发击穿)

原理:缓存失效瞬间,只允许一个请求查询数据库并更新缓存,其余请求等待重试 / 直接读旧缓存。 常用:Redis 分布式锁、ZooKeeper 锁。

流程

  1. 缓存失效,请求尝试获取锁;

  2. 拿到锁 → 查库 → 更新缓存 → 释放锁;

  3. 没拿到锁 → 短暂休眠后重试读缓存。

优点

严格控制数据库请求量,数据强一致。

缺点

部分请求会等待,高并发下有少量性能损耗。

适用

要求数据一致性较高的业务。

快速区分口诀
穿透:查不存在的数据 → 无效请求打 DB
击穿:单个热点 Key 失效 → 高并发单点打 DB
雪崩:批量 Key 失效 / 缓存宕机 → 全量流量打 DB

  • 缓存雪崩大量 key 同时失效 / 缓存整体挂掉,请求集体打向 DB(范围大)
  • 缓存击穿单个热点 key 失效,并发打向 DB(只针对一个 key)
  • 缓存穿透:查询根本不存在的数据,缓存永远不命中,请求一直访问 DB

缓存更新方案

一、基础概念:两种常见错误更新方案

方案 1:先改数据库 → 后删缓存(主流普通方案,有瑕疵)

执行流程:更新数据库 → 删除缓存

并发问题: 线程 A 更新库(未删缓存)→ 线程 B 查询,读取旧缓存返回 → 线程 A 再删除缓存

结果:查询线程短暂读到旧数据,存在短时不一致。

方案 2:先删缓存 → 后改数据库(严重缺陷,不推荐)

执行流程:删除缓存 → 更新数据库

并发问题: 线程 A 删除缓存(缓存为空)→ 线程 B 查询,查库拿到旧数据并写入缓存 → 线程 A 才更新数据库

结果:缓存永久保存旧数据,数据库为新数据,彻底数据不一致

二、方案一:缓存延迟双删策略(最终一致性,通用业务首选)

1. 核心流程

  1. 第一次删除缓存
  2. 更新数据库
  3. 延迟几百毫秒~1 秒,第二次删除缓存

2. 解决的问题

专门清除「删缓存、改库间隙中,查询线程写入的旧缓存」,规避方案 2 的致命缺陷。

3. 搭配兜底方案

设置缓存过期时间

  • 操作:写入缓存时,统一配置 TTL(读多写少设 5 分钟,写多设 1~3 分钟)
  • 作用:极端场景下双删失效时,缓存超时自动刷新,避免永久脏数据

4. 优缺点

  • 优点:实现简单、代码改动小,大幅降低脏数据概率
  • 缺点:存在短暂数据不一致窗口,不适合强一致性业务

5. 适用场景

普通业务、读多写少、允许短暂数据不一致的场景。

三、方案二:分布式锁 + 同步更新缓存(强一致性,核心业务首选)

1. 核心思想

利用分布式锁实现读写互斥,所有请求排队执行,彻底杜绝并发导致的数据不一致。

2. 标准执行流程

  1. 尝试获取分布式锁(同一数据共用一把锁)
  2. 加锁成功 → 更新数据库
  3. 直接向缓存写入最新数据(不删除缓存)
  4. 释放分布式锁
  5. 抢锁失败的请求,阻塞等待锁释放后再执行

3. 并发演示(时间轴)

示例:数据原值 100,需更新为 200

时间节点 线程 A (更新) 线程 B (查询) 缓存 数据库
T0 获取锁成功 抢锁失败,阻塞等待 100 100
T1 更新数据库为 200 持续等待 100 200
T2 缓存写入 200 持续等待 200 200
T3 释放锁 开始执行查询 200 200
T4 执行结束 读取最新缓存 200 200 200

4. 代码核心伪代码(SpringBoot+Redisson)

String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
lock.lock(); // 加锁

try {
    // 1. 更新数据库
    productMapper.updatePrice(productId, 200);
    // 2. 同步写入最新缓存(带过期时间)
    redisTemplate.opsForValue().set("product:" + productId, 200, 5, TimeUnit.MINUTES);
} finally {
    lock.unlock(); // 释放锁
}

5. 优缺点

  • 优点:全程强数据一致,无不一致时间窗口,支持写写 / 读写高并发
  • 缺点:实现复杂,引入分布式锁,有少量性能损耗

6. 适用场景

金融、支付、订单、余额等要求 100% 数据一致的核心业务。

四、三大方案对比 & 选型总结

方案 数据一致性 实现难度 性能 适用场景
先改库后删缓存 短暂不一致 大部分普通业务
延迟双删 最终一致 读多写少,可容忍短时不一致
分布式锁 + 同步更缓存 强一致 中等 支付、订单、金融等核心业务

选型口诀

  1. 普通业务 → 延迟双删 + 缓存过期兜底
  2. 核心敏感业务 → 分布式锁 + 同步更新缓存
  3. 禁止使用:单纯「先删缓存、后改数据库」方案

缓存双删策略(先删缓存 → 改数据库 → 再删缓存)

核心目的:解决「读写并发」导致的缓存脏数据、数据不一致问题

缓存双删 = 延迟双删,标准流程:

  1. 先删除 Redis 缓存
  2. 更新 MySQL 数据库
  3. 延迟一小段时间(几百 ms~1s),再删除一次缓存

1. 先看为什么普通方案会出问题

方案 1:先改库 → 后删缓存(最常用,但有漏洞)

并发场景:

  • 线程 A:更新数据库(还没删缓存)
  • 线程 B:查询数据 → 读到旧缓存,直接返回
  • 线程 A:再删除缓存

结果:B 把旧数据留在缓存里,造成脏数据

方案 2:先删缓存 → 后改库(漏洞更大)

  • 线程 A:删缓存
  • 线程 B:查询 → 缓存空,查旧库数据写入缓存
  • 线程 A:再更新数据库

结果:缓存永久存旧数据,彻底不一致

三、双删策略怎么解决问题

  1. 第一次删缓存:清空旧缓存
  2. 更新数据库(新数据落地)
  3. 延迟二次删缓存: 专门干掉并发读线程中途写入的旧缓存

延迟时间只要大于数据库查询 + 写入缓存的耗时,就能彻底清掉脏缓存。

#一般配合缓存过期时间使用:就算双删漏了,超时自动刷新,就算双删策略出了小 bug,缓存也#不会永远脏数据,到期自动刷新。为什么要这么做?双删策略是尽量保证一致, 过期时间是兜底#保险,极端情况下缓存脏了,最多几分钟就自动恢复

适用

  • 读多写少、允许短暂缓存不一致的业务

优点

  • 实现简单,代码改动小
  • 大幅降低并发下缓存脏数据概率

缺点

  • 短暂不一致窗口,强一致性业务不能用
  • 依赖延迟时间设置,时间太长 / 太短都失效
  • 极端高并发仍有极小概率出问题

强一致性场景(不能用双删)怎么做?

适用场景

  • 金融、订单、余额、支付
  • 必须 100% 一致,不能有一毫秒脏数据

双删为什么不能用?

双删有短暂不一致窗口,强一致业务不允许

强一致性方案:

步骤 1:加分布式锁

      更新数据库 + 分布式锁 + 同步更新缓存

  1. 要更新数据前,先加锁(Redisson 最常用)
  2. 加锁成功才能继续更新
  3. 防止并发更新、并发读写交叉

步骤 2:更新数据库

步骤 3:同步更新缓存(不是删!是直接 set 新值)

步骤 4:释放分布式锁

强一致性完整伪代码

// 1. 加分布式锁
lock = redisson.getLock("lock:" + id)
lock.lock()

try {
    // 2. 更新数据库
    updateDB(新数据)

    // 3. 同步更新缓存(直接 set 最新值)
    setRedis(key, 新数据, 300)

} finally {
    // 4. 释放锁
    lock.unlock()
}

为什么这样能 100% 一致?

  • 加锁 = 所有读写排队执行
  • 先改库 → 立刻改缓存
  • 读请求必须等写请求完成才能读
  • 缓存永远是最新的

一句话总结怎么选

  • 普通业务(允许短暂不一致)缓存双删 + 过期时间兜底
  • 金融 / 订单 / 余额(必须强一致)分布式锁 + 更新数据库 + 更新缓存

先删缓存 → 后改库的问题?

线程A:先删缓存
线程B:查询 → 缓存空 → 查数据库旧值 → 写回缓存
线程A:再更新数据库

最终结果:缓存是旧的,数据库是新的,永远不一致!

真实并发场景

假设:

  • 商品价格:原来 = 100 元
  • 现在要改成:200 元

时间点 1

线程 A(更新价格): 第一步:删除缓存里的 100 元 → 缓存现在:空

时间点 2(关键!并发来了)

线程 B(查询价格): 去查缓存 → 空 → 去查数据库 → 读到旧数据 100 元 → 把 100 元写回缓存!

现在状态:

  • 缓存:100 元(旧)

  • 数据库:100 元(旧)

时间点 3

线程 A(继续执行): 更新数据库 → 改成 200 元

最终结果

  • 数据库:200 元(新)

  • 缓存:100 元(旧)

灾难来了!

从此以后,所有查询都读到缓存里的 100 元旧价格 除非缓存过期,否则永远不一致!

用一句话先删缓存后改库 的坑

你删缓存的瞬间 + 你更新数据库之前 只要有人查询,就会把旧数据重新塞回缓存 你后面更新数据库也没用了!

为什么双删能解决这个坑?

双删 =

  1. 删缓存
  2. 更新数据库
  3. 延迟几百毫秒,再删一次缓存

第三步就是专门干掉: 线程 B 刚刚写入缓存的那个旧数据!

先删缓存后改库最大的问题?如何补救?

  • 问题:删缓存后,旧数据会被重新写回缓存
  • 问题:写回缓存发生在 “删缓存” 和 “更新库” 之间
  • 补救:双删的第二次删除,就是把这个 “偷偷写回去的旧缓存” 删掉

总结

  • 先删缓存 → 再改库 = 坑!缓存会变旧,永远不一致
  • 先删缓存 → 改库 → 延迟再删缓存 = 双删(解决坑)

强一致性场景里「分布式锁 + 更新数据库 + 同步更新缓存」

分布式锁是干嘛的?

就是让 “写操作” 和 “读操作” 不能同时跑,必须排队! 谁先拿到锁,谁先执行,没拿到的必须等着。

为什么要用分布式锁?(不锁会发生什么)

还是用商品价格 = 100 元 → 改成 200 元举例

不加锁的危险并发(脏数据根源)

  1. 线程 A 开始更新:删缓存
  2. 线程 B 突然来查询 → 缓存空 → 查库 100 → 写回缓存 100
  3. 线程 A 才更新库成 200
  4. 结果:缓存 100,库 200 → 脏数据!

加锁后会发生什么?

线程 A 拿到锁 → 别人都必须等!

  • 线程 A:更新库 → 直接把缓存改成 200 → 释放锁
  • 线程 B:等锁释放后再查 → 读到最新 200

完美不脏数据!

分布式锁 强一致性方案:完整操作步骤

步骤 1:更新数据前,先抢锁

谁抢到锁,谁才能执行更新,别人必须排队等

步骤 2:更新数据库(改成最新值)

直接 update 数据库,确保数据是最新的

步骤 3:直接把新数据 set 进缓存(不是删除!)

这是关键! 不是删缓存,是直接把新数据写进 Redis

步骤 4:释放锁

让排队的请求可以继续执行

演示

商品价格:100 → 要改成 200

线程 A(更新价格)

  1. 抢锁成功 → 别人都要等
  2. 更新数据库 → 200
  3. set Redis → 200(直接写新值,不删)
  4. 释放锁

线程 B(查询)

  1. 来的时候,锁被 A 拿着 → 必须等
  2. A 释放锁后,B 才执行
  3. B 查 Redis → 直接拿到 200 最新值

最终结果

数据库 = 200,缓存 = 200 → 完全一致!

总结

普通双删(最终一致)

删缓存 → 改库 → 延迟删缓存

分布式锁(强一致)

加锁 → 改库 → 直接 set 新缓存 → 解锁

  • 分布式锁 = 让读写排队
  • 强一致性 = 不能删缓存,要直接 set 新值
  • 流程 = 加锁 → 改库 → 写缓存 → 解锁

实例

// 锁的 key(每个商品一个锁)
String lockKey = "lock:product:" + productId;

// 1. 加分布式锁(阻塞等待,自动防死锁)
RLock lock = redissonClient.getLock(lockKey);
lock.lock();

try {
    // 2. 更新数据库
    productMapper.updatePrice(productId, 200);

    // 3. 直接把新数据写入缓存(不是删除!)
    redisTemplate.opsForValue().set("product:" + productId, 200, 5, TimeUnit.MINUTES);

} finally {
    // 4. 释放锁
    lock.unlock();
}
分布式锁 + 改库 + 同步更新缓存 标准流程
请求进来 → 尝试获取分布式锁
   ↓
【拿到锁】→ 更新数据库 → 直接写入最新数据到缓存 → 释放锁
   ↓
【没拿到锁】→ 阻塞等待,直到锁被释放再执行

场景 1:正常串行执行(无并发)

时间节点 线程 A(更新操作) 缓存状态 数据库状态
T0 发起更新,获取锁 ✅ 100 100
T1 执行 SQL,数据库更新为 200 100 200
T2 缓存直接写入新值 200 200 200
T3 释放锁 200 200

结果:缓存、数据库完全一致。

场景 2:核心场景【读写并发】(不加锁 VS 加锁 对比)

前置说明

  • 线程 A:更新操作(100 → 200)
  • 线程 B:查询操作(读价格)
  • 两个线程同一时间触发,模拟线上高并发

🔴 情况 1:不加分布式锁(会产生脏数据

T0  线程A:开始更新数据库
     线程B:同时查询 → 缓存=100,直接返回旧数据
T1  线程A:数据库更新完成 → 库=200
T2  线程A:更新缓存 → 缓存=200

最终状态:
数据库=200,缓存=200
但线程B在中间读到了旧数据,短暂数据不一致

🟢 情况 2:加上分布式锁(强一致,无脏数据)

这是重点,分步拆解时间线:

全局锁Key:lock:product:1001
时间节点 线程 A(更新) 线程 B(查询) 缓存 数据库
T0 抢占锁 → 获取锁成功 抢占锁 → 获取失败,进入阻塞等待 100 100
T1 执行更新 → 数据库改为 200 持续等待 100 200
T2 写入缓存 → 缓存改为 200 持续等待 200 200
T3 释放分布式锁 锁释放,开始执行查询 200 200
T4 执行完毕 查询缓存,拿到 200(最新值),返回结果 200 200

关键结论:

  1. 锁生效后,读写线程无法并行执行,必须排队;
  2. 只有更新线程把「库 + 缓存」全部改成新值、释放锁后,查询线程才能执行;
  3. 全程不会读到旧数据,实现强一致性

场景 3:写写并发(多个线程同时更新)

两个更新线程同时改同一条数据,演示锁的互斥效果

  • 线程 A:100 → 200
  • 线程 C:200 → 300
T0  线程A:拿到锁 ✅
    线程C:抢锁失败 → 阻塞等待
T1  线程A:库→200,缓存→200
T2  线程A:释放锁
T3  线程C:获取锁 ✅
T4  线程C:库→300,缓存→300
T5  线程C:释放锁

效果:多个写操作串行执行,不会出现并发覆盖、数据错乱。

补充:和「先删缓存 / 双删」的核心区别图示

1. 延迟双删(最终一致,允许短暂不一致)

删缓存 → 改数据库 → 延迟等待 → 再删缓存
存在间隙:删缓存 ~ 二次删缓存 之间,可能被读线程写入旧缓存
适用:普通业务

2. 分布式锁方案(强一致,无间隙)

加锁 → 改数据库 → 直接写新缓存 → 解锁
全程读写互斥,没有数据不一致的时间窗口
适用:支付、订单、余额等核心业务

分布式锁更新缓存 完整链路

用户发起更新请求
        ↓
┌─────────────┐
│ 获取分布式锁 │───失败→ 阻塞等待 → 重试抢锁
└──────┬───────┘
       ↓(抢锁成功)
  更新MySQL数据库
       ↓
  Redis写入最新数据
       ↓
  释放分布式锁
       ↓
请求结束

Redis 命令用于在 redis 服务上执行操作。

要在 redis 服务上执行命令需要一个 redis 客户端。

Redis 客户端的基本语法为:

开启Redis服务后

$ redis-cli      //打开终端并输入命令 redis-cli,该命令会连接本地的 redis 服务。

连接到本地的 redis 服务并执行 PING 命令,该命令用于检测 redis 服务是否启动。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐