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获取锁成功抢锁失败,阻塞等待100100
T1更新数据库为 200持续等待100200
T2缓存写入 200持续等待200200
T3释放锁开始执行查询200200
T4执行结束读取最新缓存 200200200

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发起更新,获取锁 ✅100100
T1执行 SQL,数据库更新为 200100200
T2缓存直接写入新值 200200200
T3释放锁200200

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

场景 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抢占锁 → 获取锁成功抢占锁 → 获取失败,进入阻塞等待100100
T1执行更新 → 数据库改为 200持续等待100200
T2写入缓存 → 缓存改为 200持续等待200200
T3释放分布式锁锁释放,开始执行查询200200
T4执行完毕查询缓存,拿到 200(最新值),返回结果200200

关键结论:

  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

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐