redis
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)类型存储。
存储方式
把图片文件读取为二进制数据
以
key-value形式存入 Redis 的 string 类型读取时再把二进制数据还原成图片
为什么不推荐用 Redis 存图片?
1. 内存成本极高
- Redis 是内存存储,内存价格是硬盘的几十倍
- 一张 1MB 的图片,10 万张就是 100GB 内存,成本爆炸
2. 浪费 Redis 核心性能
- Redis 设计用来做缓存、计数器、会话、消息队列等高频小数据操作
- 存大体积图片会挤占内存,拖慢其他核心业务
3. 大 key 风险(Redis 大忌)
Redis 官方建议:单个 value 不超过 10KB
图片动辄几十 KB~ 几 MB,属于大 key
导致 Redis 阻塞、卡顿
主从同步、集群迁移极慢
容易引发线上故障
什么场景可以用 Redis 存图片?
只有极小、极高频的图片才适合:
极小头像(<5KB)、图标
验证码图片(一次性、体积小)
临时二维码(短期缓存)
并且必须配合:
设置过期时间(不要永久存储)
控制大小(<10KB)
标准架构
图片上传到 专业对象存储
阿里云 OSS、腾讯云 COS、七牛云、MinIO(自建)把图片 URL存入 Redis
业务读取 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持久化方案
- 做了数据操作的命令后,输入指令bgsave就持久化了
- rdb方案
- aof方案
redis 高可用方案
1. 主从复制
- 主节点写,从节点读
- 分担读压力
2. 哨兵(sentinel)
- 自动监控主节点
- 主节点挂了自动切换从节点为主
3. redis Cluster(集群)
- 数据分片存储(多主多从)
- 支持海量数据 + 高并发
Redis 为什么快?
内存操作 + 单线程避免锁 + IO 多路复用
缓存常见问题?
-
缓存穿透、击穿、雪崩
- 穿透:查不存在的数据 → 布隆过滤器
- 击穿:热点 key 过期 → 互斥锁 / 永不过期
- 雪崩:大量 key 同时过期 → 随机过期时间、集群
Redis 分布式锁怎么实现?
-
SET key value NX PX 3000
-
Redis 淘汰策略 volatile-lru(最常用:删除最近最少使用的过期 key)
数据结构实现?
- ZSet 底层用什么实现? 压缩列表(ziplist)+ 跳表(skiplist)
- Hash 底层? ziplist / hashtable
- List 底层? quicklist(双向链表 + 压缩列表)
- 为什么 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)
解决缓存穿透神器
布隆过滤器 = 一个超级省空间的 “存在性判断工具”,作用:快速判断一个元素 一定不存在 / 可能存在
为什么要用?(解决缓存穿透)
缓存穿透问题
用户查一个根本不存在的数据:
- 先查 Redis → 没有
- 再查数据库 → 也没有
- 大量这种请求 → 数据库压力爆炸 → 宕机
布隆过滤器就是用来挡住这些无效请求的!
核心特点
✅ 如果判断不存在 → 一定真的不存在(100% 准确)
✅ 如果判断存在 → 可能存在,也可能不存在(有误判率)
✅ 不存真实数据,只存哈希标记 → 极省空间
✅ 不能删除元素(这是硬伤)
原理
- 准备一个全是 0 的 bit 数组
- 一个元素进来,用 多个哈希函数 算出多个位置
- 把这些位置的 bit 改成 1
- 判断时:只要有一个位置是 0 → 一定不存在
- 全部是 1 → 可能存在
多个哈希是为了降低误判率
优点 & 缺点
优点
- 空间极小(百万数据只占几 MB)
- 查询速度极快
- 抗缓存穿透神器
缺点
- 有误判率(可以调小,但不能消除)
- 不支持删除
1. 布隆过滤器为什么不能删?
因为多个元素可能共用同一个 bit 位,删一个会影响其他元素。
2. 怎么解决删除问题?
用计数布隆过滤器,或直接重建过滤器。
3. 缓存穿透怎么解决?
布隆过滤器 + 空值缓存
总结
- 布隆过滤器 = 判断存在性的高效工具
- 核心作用:防缓存穿透
- 判断规则:不存在必真,存在不一定真
(二)缓存击穿
定义:单个热点 Key过期,瞬时大量并发请求直接打到数据库。
成因:秒杀、爆款、首页等高访问热点数据统一过期。
解决方案:
分布式互斥锁:仅一个请求查询数据库并更新缓存,其余请求等待;
热点 Key 永不过期:代码异步定时刷新数据,规避过期问题;
逻辑过期:存储逻辑过期时间,过期后异步更新缓存,旧数据正常返回。
(三)缓存雪崩
定义:大量缓存数据同时失效,海量请求全部压到数据库,把数据库拖垮、服务瘫痪,瞬时大量并发请求直接打到数据库。
成因:
-
首页、活动爆款、秒杀商品这类热点数据,缓存设置了相同过期时间。时间一到,这批缓存集体失效。 瞬间成千上万的请求拿不到缓存数据,全都直奔数据库查询,数据库压力暴增,轻则查询卡顿,重则宕机,整个业务受影响。
- 缓存服务整体宕机,Redis / 缓存集群挂了,所有请求都无法走缓存,全部击穿到数据库,同样引发雪崩。
缓存雪崩 两类场景
场景 1:大量缓存 Key 同时过期(最常见)
场景 2:缓存集群整体宕机 / 不可用
解决方案:
1. 过期时间加随机值(最简单、优先使用)
原理:给缓存过期时间增加随机偏移量,打散过期时间,避免批量 Key 同一时刻失效。
2. 热点 Key 永不过期
原理:热点数据不设置主动过期,通过定时任务异步更新缓存,从根源杜绝过期。
3.分布式互斥锁(防并发击穿)
原理:缓存失效瞬间,只允许一个请求查询数据库并更新缓存,其余请求等待重试 / 直接读旧缓存。 常用:Redis 分布式锁、ZooKeeper 锁。
流程
-
缓存失效,请求尝试获取锁;
-
拿到锁 → 查库 → 更新缓存 → 释放锁;
-
没拿到锁 → 短暂休眠后重试读缓存。
优点
严格控制数据库请求量,数据强一致。
缺点
部分请求会等待,高并发下有少量性能损耗。
适用
要求数据一致性较高的业务。
快速区分口诀
穿透:查不存在的数据 → 无效请求打 DB
击穿:单个热点 Key 失效 → 高并发单点打 DB
雪崩:批量 Key 失效 / 缓存宕机 → 全量流量打 DB
- 缓存雪崩:大量 key 同时失效 / 缓存整体挂掉,请求集体打向 DB(范围大)
- 缓存击穿:单个热点 key 失效,并发打向 DB(只针对一个 key)
- 缓存穿透:查询根本不存在的数据,缓存永远不命中,请求一直访问 DB
缓存更新方案
一、基础概念:两种常见错误更新方案
方案 1:先改数据库 → 后删缓存(主流普通方案,有瑕疵)
执行流程:更新数据库 → 删除缓存
并发问题: 线程 A 更新库(未删缓存)→ 线程 B 查询,读取旧缓存返回 → 线程 A 再删除缓存
结果:查询线程短暂读到旧数据,存在短时不一致。
方案 2:先删缓存 → 后改数据库(严重缺陷,不推荐)
执行流程:删除缓存 → 更新数据库
并发问题: 线程 A 删除缓存(缓存为空)→ 线程 B 查询,查库拿到旧数据并写入缓存 → 线程 A 才更新数据库
结果:缓存永久保存旧数据,数据库为新数据,彻底数据不一致。
二、方案一:缓存延迟双删策略(最终一致性,通用业务首选)
1. 核心流程
- 第一次删除缓存
- 更新数据库
- 延迟几百毫秒~1 秒,第二次删除缓存
2. 解决的问题
专门清除「删缓存、改库间隙中,查询线程写入的旧缓存」,规避方案 2 的致命缺陷。
3. 搭配兜底方案
设置缓存过期时间
- 操作:写入缓存时,统一配置 TTL(读多写少设 5 分钟,写多设 1~3 分钟)
- 作用:极端场景下双删失效时,缓存超时自动刷新,避免永久脏数据
4. 优缺点
- 优点:实现简单、代码改动小,大幅降低脏数据概率
- 缺点:存在短暂数据不一致窗口,不适合强一致性业务
5. 适用场景
普通业务、读多写少、允许短暂数据不一致的场景。
三、方案二:分布式锁 + 同步更新缓存(强一致性,核心业务首选)
1. 核心思想
利用分布式锁实现读写互斥,所有请求排队执行,彻底杜绝并发导致的数据不一致。
2. 标准执行流程
- 尝试获取分布式锁(同一数据共用一把锁)
- 加锁成功 → 更新数据库
- 直接向缓存写入最新数据(不删除缓存)
- 释放分布式锁
- 抢锁失败的请求,阻塞等待锁释放后再执行
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% 数据一致的核心业务。
四、三大方案对比 & 选型总结
| 方案 | 数据一致性 | 实现难度 | 性能 | 适用场景 |
|---|---|---|---|---|
| 先改库后删缓存 | 短暂不一致 | 低 | 高 | 大部分普通业务 |
| 延迟双删 | 最终一致 | 低 | 高 | 读多写少,可容忍短时不一致 |
| 分布式锁 + 同步更缓存 | 强一致 | 高 | 中等 | 支付、订单、金融等核心业务 |
选型口诀
- 普通业务 → 延迟双删 + 缓存过期兜底
- 核心敏感业务 → 分布式锁 + 同步更新缓存
- 禁止使用:单纯「先删缓存、后改数据库」方案
缓存双删策略(先删缓存 → 改数据库 → 再删缓存)
核心目的:解决「读写并发」导致的缓存脏数据、数据不一致问题。
缓存双删 = 延迟双删,标准流程:
- 先删除 Redis 缓存
- 更新 MySQL 数据库
- 延迟一小段时间(几百 ms~1s),再删除一次缓存
1. 先看为什么普通方案会出问题
方案 1:先改库 → 后删缓存(最常用,但有漏洞)
并发场景:
- 线程 A:更新数据库(还没删缓存)
- 线程 B:查询数据 → 读到旧缓存,直接返回
- 线程 A:再删除缓存
结果:B 把旧数据留在缓存里,造成脏数据。
方案 2:先删缓存 → 后改库(漏洞更大)
- 线程 A:删缓存
- 线程 B:查询 → 缓存空,查旧库数据写入缓存
- 线程 A:再更新数据库
结果:缓存永久存旧数据,彻底不一致。
三、双删策略怎么解决问题
- 第一次删缓存:清空旧缓存
- 更新数据库(新数据落地)
- 延迟二次删缓存: 专门干掉并发读线程中途写入的旧缓存。
延迟时间只要大于数据库查询 + 写入缓存的耗时,就能彻底清掉脏缓存。
#一般配合缓存过期时间使用:就算双删漏了,超时自动刷新,就算双删策略出了小 bug,缓存也#不会永远脏数据,到期自动刷新。为什么要这么做?双删策略是尽量保证一致, 过期时间是兜底#保险,极端情况下缓存脏了,最多几分钟就自动恢复
适用
- 读多写少、允许短暂缓存不一致的业务
优点
- 实现简单,代码改动小
- 大幅降低并发下缓存脏数据概率
缺点
- 有短暂不一致窗口,强一致性业务不能用
- 依赖延迟时间设置,时间太长 / 太短都失效
- 极端高并发仍有极小概率出问题
强一致性场景(不能用双删)怎么做?
适用场景
- 金融、订单、余额、支付
- 必须 100% 一致,不能有一毫秒脏数据
双删为什么不能用?
双删有短暂不一致窗口,强一致业务不允许
强一致性方案:
步骤 1:加分布式锁
更新数据库 + 分布式锁 + 同步更新缓存
- 要更新数据前,先加锁(Redisson 最常用)
- 加锁成功才能继续更新
- 防止并发更新、并发读写交叉
步骤 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 元旧价格 除非缓存过期,否则永远不一致!
用一句话先删缓存后改库 的坑
你删缓存的瞬间 + 你更新数据库之前 只要有人查询,就会把旧数据重新塞回缓存 你后面更新数据库也没用了!
为什么双删能解决这个坑?
双删 =
- 删缓存
- 更新数据库
- 延迟几百毫秒,再删一次缓存
第三步就是专门干掉: 线程 B 刚刚写入缓存的那个旧数据!
先删缓存后改库最大的问题?如何补救?
- 问题:删缓存后,旧数据会被重新写回缓存
- 问题:写回缓存发生在 “删缓存” 和 “更新库” 之间
- 补救:双删的第二次删除,就是把这个 “偷偷写回去的旧缓存” 删掉
总结
- 先删缓存 → 再改库 = 坑!缓存会变旧,永远不一致
- 先删缓存 → 改库 → 延迟再删缓存 = 双删(解决坑)
强一致性场景里「分布式锁 + 更新数据库 + 同步更新缓存」
分布式锁是干嘛的?
就是让 “写操作” 和 “读操作” 不能同时跑,必须排队! 谁先拿到锁,谁先执行,没拿到的必须等着。
为什么要用分布式锁?(不锁会发生什么)
还是用商品价格 = 100 元 → 改成 200 元举例
不加锁的危险并发(脏数据根源)
- 线程 A 开始更新:删缓存
- 线程 B 突然来查询 → 缓存空 → 查库 100 → 写回缓存 100
- 线程 A 才更新库成 200
- 结果:缓存 100,库 200 → 脏数据!
加锁后会发生什么?
线程 A 拿到锁 → 别人都必须等!
- 线程 A:更新库 → 直接把缓存改成 200 → 释放锁
- 线程 B:等锁释放后再查 → 读到最新 200
完美不脏数据!
分布式锁 强一致性方案:完整操作步骤
步骤 1:更新数据前,先抢锁
谁抢到锁,谁才能执行更新,别人必须排队等
步骤 2:更新数据库(改成最新值)
直接 update 数据库,确保数据是最新的
步骤 3:直接把新数据 set 进缓存(不是删除!)
这是关键! 不是删缓存,是直接把新数据写进 Redis
步骤 4:释放锁
让排队的请求可以继续执行
演示
商品价格:100 → 要改成 200
线程 A(更新价格)
- 抢锁成功 → 别人都要等
- 更新数据库 → 200
- set Redis → 200(直接写新值,不删)
- 释放锁
线程 B(查询)
- 来的时候,锁被 A 拿着 → 必须等
- A 释放锁后,B 才执行
- 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 |
关键结论:
- 锁生效后,读写线程无法并行执行,必须排队;
- 只有更新线程把「库 + 缓存」全部改成新值、释放锁后,查询线程才能执行;
- 全程不会读到旧数据,实现强一致性。
场景 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 服务是否启动。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)