redis八股
java八股
redis使用场景八股解析
缓存-缓存穿透

我先来分析一下这个方案,原本按照给的图片可以看到,图片中查询的是一个正常情况,没有发生缓存击穿的情况,它是一个存在的数据,这是前提,然后通过id查redis,查到了返回结果,没查到就在DB里查,最后返回数据结果,返回之前把结果存储在redis中。在这当中一个**重要的点**就是先把数据写入redis完成回填,然后再将数据返回给调用方。
但是这又引申到一个进阶考点了——**同步模式和异步模式。**
一、同步写缓存(先写 Redis,再返回结果)
适用场景
- 读多写少、详情类查询:你现在的根据 ID 查文章、商品详情、用户信息,90% 基础缓存场景都用它
- 要求强一致性、下次请求必须命中缓存的业务
- 并发量中等,不在乎多一次 Redis 网络耗时
优点
- 逻辑简单,不易出错,第一次回写完成后,后续请求 100% 走缓存
- 无短暂数据不一致问题
缺点
- 响应时间变长,多了一次 Redis 写入的 RT (Response Time)耗时
- Redis 宕机时,如果没做容错处理,本次请求直接失败
解释: - 先查 Redis → Redis 已经宕机,连接直接失败 / 查不到
- 程序正常降级,去查询 MySQL,数据库确实无数据,拿到 null 结果
- 同步规则要求:必须执行 把null存入Redis 这一步
- 尝试和宕机的 Redis 通信、执行 set 命令,连接失败,代码抛出 IO
异常 - 没有 try-catch 容错捕获这个异常:程序直接终止,前端收到 500 服务器错误,本次请求彻底失败,用户拿不到任何返回结果。
二、异步写缓存(先返回结果,后台线程 / 消息队列异步更新 Redis)
适用场景
- 超高并发热点接口:秒杀商品首页、热门活动列表、流量爆炸的公共资讯
- 追求极致接口响应速度,能容忍极短时间(毫秒级)数据不一致
- 写缓存动作不阻塞主流程
例:百万直播间点赞属于高并发、读感优先、允许最终一致性的业务,优先采用异步写缓存 / 异步落库方案,优先保证接口响应速度和用户体验,用 Redis 承接瞬时流量,数据库做后台批量持久化即可。
优点 - 用户体感更快,DB 查询完直接返回,写 Redis 放在后台执行
- 即使 Redis 短暂故障,不影响本次用户请求正常返回
缺点
- 存在极小时间窗口的数据不一致:用户拿到数据了,但 Redis 还没写完,下一个并发请求依然会查 DB
- 代码复杂度更高,需要线程池、消息队列兜底,还要处理异步任务失败重试
面试高频问答整理
-
查询文章详情用同步还是异步?
答:同步模式。详情查询并发压力一般,优先保证缓存可靠回填,结构简单好维护,一致性优先。 -
什么场景必须用异步更新缓存?
答:大促首页、热点排行榜、百万 PV 公共页面,接口耗时敏感,允许极短暂不一致,优先保证返回速度。 -
异步写缓存如果任务失败怎么办?
答:用 MQ 做可靠投递、增加失败重试机制、定时任务兜底扫描补缓存。
解释:
如果后台异步往 Redis 写缓存的任务半路失败了,很容易出现缓存一直没补上、数据丢失的问题,处理办法分三层:
1.不用普通后台子线程跑异步,改用消息队列 MQ 来存放这个任务
普通线程崩了任务直接消失,MQ 会把这条任务记录存下来,不会随便弄丢。
2.任务执行失败后,让它自动重复尝试执行几次
比如第一次写 Redis 网络断了,不用人工操作,系统自动再试 2-3 回,大部分临时故障重试就能成功。
3.加一个定时巡检的兜底小程序
哪怕重试好几次都彻底失败,每隔一段时间自动扫描一遍数据,对比数据库和缓存内容,发现缓存空缺就主动补上,做最后一道保险。
解决方案一的意思就是本来一个你在redis和DB里面都查不到数据,直接用null强制性当作数据结果存到redis当中,保护DB。一个不存在的请求就会产生一个没有用的null结果,浪费内存。不一致问题就是本来没有数据,给id返回了null,并且存到redis中了,但是后台在mysql中新增了id的真实数据,mysql与redis内容不一致,但是数据请求先走redis,直接返回null了。
优化手段:给空 key 设置较短的过期 TTL(Time To Live)
作用:空 key 自动过期释放内存,同时缩短数据不一致的时长。

方案二的布隆过滤器,我首先想到的问题就是它和redis有什么区别,Redis 只能判断「当前缓存里有没有」;布隆过滤器能判断「数据库里从来有没有存过」,二者防护的问题完全不一样。布隆过滤器存储的是所有曾经成功写入 MySQL 的主键 ID。当它返回“不存在”时,可以 100% 确定数据库中从未有过这条数据,从而直接拦截请求,无需查询 Redis 或 DB。
- DB 新增 → 布隆主动立刻更新
- DB 新增 → Redis原地不动,等到第一次查询击穿后,才被动写入

首先给出你好这个数据,会通过k个哈希函数,图中是三个,来算出k个下标的值,把对应下标的数组的0改为1,查询的话也是一样,但是如果对应的k个下标上的二进制数组有一个不为1.则表示不存在。
布隆过滤器很难做删除数据,你好和hello两个数据用哈希函数算出结果都是2,那下标为2的这个地方存储了两个数据,如果删除任何一个数据,二进制数组1改为0,那数据就出现了误删。
优点;它是由一串二进制数组组成的一个数据,所以占的空间很小。
插入和查询的速度很快,也是因为数组的特性。时间复杂度是O(k),因为k个哈希函数个数。
本身不存储原始数据,所以安全性很好
缺点:很难做删除操作
存在误判

怎么减小误判概率?
代码中设置的误判率是能够影响误判结果的,它会和你的误判率几乎吻合。所以误判率越小,它误判的个数也就越小。但是我们就想到那把误判率设置成无限小可以吗?
误判率越小,运算结果时间越长,性能越差。误差率越小,它所占用的空间越大,并且需要的哈希函数也就越多。但是哈希函数越多需要到一个最优点,它所对应的下标就要越多,开始是误判变小,到后面数组内1占比过高误判回升。这个越多,哈希值就越多,对应的二进制数据也会增多,所以占用空间越大。
在位数组长度、存入数据总量固定的前提下:
- 哈希数量逐步增加初期,每个元素占用更多独立比特位,误判概率持续下降;
- 当哈希数量超过最优值之后,大量比特被快速标记为 1,数组内 1 的占比过高,后续不存在的数据很容易全部命中 1,误判率反而回升;
- 额外还有性能问题,哈希函数越多,插入和查询的 CPU 计算开销越大。
误判例子:
假设 bit 数组长度只有 5:[0,0,0,0,0]
插入元素 A,哈希算出位置 1、3 → 数组变为:[0,1,0,1,0]
插入元素 B,哈希算出位置 2、4 → 数组变为:[0,1,1,1,1]
现在查询完全不存在的元素 C
C 经过哈希计算,刚好命中位置 1、4,这两个位置现在都是 1
布隆判定:C 可能存在,但实际上 C 从来没存过 → 误判
缓存-缓存击穿
看到击穿定义我就想到击穿跟我前面学的穿透有什么区别?
- 缓存穿透:DB、Redis 全都没有这条数据,大量无效请求反复打数据库。
- 缓存击穿:DB 一定有数据,只是 Redis 热点 key 过期清空,并发请求同一时刻全部绕过 Redis 访问 DB,压垮库。

- 互斥锁(排队干活)
大白话:
缓存过期了,只放一个人去查数据库、更新缓存,其他所有人原地排队等着。
流程:
大家同时查 Redis,发现没数据
第一个人抢到分布式锁,去查 MySQL,查到数据回填 Redis
后面所有人拿不到锁,阻塞等待,等缓存有值了,直接读缓存
优点:数据绝对最新,一致性很好
缺点:有人排队等待,接口响应变慢;极端情况锁没释放会死锁
- 逻辑过期(不删数据,只标过期时间)
大白话:
Redis 里的数据永远不主动删除、不淘汰,额外给每条数据多存一个 “逻辑过期时间”。
哪怕真实已经到过期点了,Redis 数据还在。
流程:
查询时先看逻辑时间有没有过期
没过期:直接正常返回缓存数据
已经过期:开一个独立子线程异步去更新 DB 和缓存,当前请求直接返回旧数据,不用等更新完成
优点:所有请求几乎不用等待,性能好,高并发友好
缺点:会短暂返回旧数据,有很短时间的数据不一致

一句话对比
互斥锁:宁可大家排队等新数据,也要保证拿到最新内容,牺牲速度换一致性
逻辑过期:先凑合返回旧数据,后台悄悄更新,牺牲短暂一致性换速度
逻辑过期的 key 本身不设 TTL,不会被 Redis 主动过期删除,但 Redis 有最后一道保险:内存淘汰机制,内存塞满时自动删冷门 key 腾空间。
- 过期删除:只针对加了 expire 过期时间的 key,时间到了才会被清理。逻辑过期的热点 key完全不设
TTL,所以不会走这套过期删除逻辑,数据一直留在 Redis 里。 - 内存淘汰(逐出):Redis 配置了最大可用内存maxmemory,一旦内存撑爆,不管 key 有没有过期,直接按规则删掉一部分 key
释放内存,这是兜底手段。
线程 1 先来查询缓存
查到 Redis 里是有数据的,拆开 value 一看:里面自带的 expire 时间已经超期了(逻辑过期)。
线程 1 尝试去拿互斥锁,抢到锁了。
它不阻塞后面所有请求,自己单独开一个子新线程去干活:查 MySQL、更新最新数据、重新写回 Redis、刷新里面的逻辑过期时间。
主线程的线程 1 直接把手里这份旧的过期数据原路返回给用户,用户不用等待。
线程 2、线程 3、线程 4 几乎同一时间也来查询
它们同样查到逻辑时间过期了,也尝试抢互斥锁:
锁已经被线程 1 拿走了,全部抢锁失败;
这些线程什么等待都不用做,直接就地把 Redis 里现存的旧数据返回给用户。
后台子线程完成更新后
新数据写入 Redis,并且把 value 里的 expire 逻辑时间往后延期,然后释放互斥锁。
之后再来的请求,查到逻辑时间没过期,直接正常拿最新缓存数据返回。
3 个最关键特点:
- 全程没有任何请求阻塞排队
哪怕数据逻辑过期,所有人都能立刻拿到旧数据响应,接口速度几乎不受影响,高并发性能极好。
- 只有 1 个子线程去更新数据库
就算一万个并发同时过来,也只会有 1 个线程去操作 DB,完美防止缓存击穿大量请求打满数据库。
- 代价是短暂数据不一致
在后台线程更新完成之前,所有用户拿到的都是老版本数据,只能扛高并发,做不到强实时一致性。
一句话总结逻辑过期:
数据标了内部过期时间,但 Redis 不删数据;过期后只派一个小弟偷偷去后台更新库和缓存,前台所有人直接先拿旧数据走人,不等更新完成,用极短时间的数据新旧差,换超高并发吞吐量。
互斥锁场景:Redis 无数据 → 抢不到锁只能死等;
逻辑过期场景:Redis 常驻有数据 → 抢不到锁直接返回旧数据,零阻塞。
缓存-缓存雪崩
标准定义里,缓存击穿默认就是单个热点 key;极少数情况 2~3 个独立热点 key 刚好同时过期,业内依然会按击穿处理,不会归为雪崩。
缓存击穿:同一个 key高并发访问,刚好这个 key 过期,一堆请求全挤去查 DB。压力全部集中在数据库同一张表、同一条 SQL上。
缓存雪崩:一大堆完全不同的 key集体同时过期,各种各样的请求四面八方打向数据库,很多张表、很多 SQL 同时承压,属于大范围灾难。
那如果是 2 个爆款商品 key,刚好同一秒一起过期算什么?
实战里:两个是互相独立的热点,各自的并发请求只冲自己对应的 DB 数据,依然叫缓存击穿,算两次独立的击穿事故;
只有当几十上百个普通 key 扎堆过期,才叫缓存雪崩。
- 大量key同时过期的原因就是他们用了同一个过期时间。
解决方案:给不同的key的TTL添加随机值

方案 1:给过期时间加随机数(解决大批量 key 同时集体过期)
本来很多 key 都设 10 分钟过期,时间一到齐刷刷全部失效,直接雪崩。
做法:基础过期时间统一 10 分钟,额外随机多加上几十秒到几百秒。
大家过期时间被错开,不会同一秒集体失效,压力被平摊分散,就不会瞬间冲爆数据库。
方案 2:Redis 搞集群、哨兵高可用(解决 Redis 整个宕机废掉)
不让 Redis 只有孤零零一台机器。
做主从哨兵、Redis 集群,一台 Redis 炸了,备用机器立刻顶上继续干活,Redis 服务不会整体瘫痪,请求依旧能正常走缓存,不会全部压去数据库。
方案 3:加限流、降级(Redis 真扛不住了,硬保数据库)
哪怕缓存失效,也不能放任无数请求疯狂撞数据库。
限流:只放行规定数量的请求去查数据库,多余请求直接拒绝返回 “稍后再试”;
降级:非核心功能直接暂时关掉,优先保证下单、支付这类核心功能能用。
一般在网关层(Nginx、SpringCloud 网关)做这套控制。
方案 4:加多级别缓存(本地内存缓存兜底)
在我们 Java 程序自己的内存里,再存一份热点数据(用 Caffeine、Guava)。
查询顺序:先查本机内存 → 再查 Redis → 最后才查数据库。
就算 Redis 彻底崩了,大量请求可以直接读服务器本地内存,根本不用去碰数据库,极大减轻压力。
过期加随机:错峰过期,避免批量 key 一起失效
Redis 搞高可用:机器坏了有备胎顶替,服务不中断
限流降级:限制访问量,砍掉非必要功能,死保数据库
多级缓存:本机内存当第二道缓存,绕开故障的 Redis
哨兵模式
一堆 Redis 机器,全部数据都存在一起,只有 1 台主机器负责改数据,其他机器只做备份。
额外雇几个 “巡逻保安(哨兵)”,主机器死机了,保安自动挑一台备份机器转正当新主机,程序自动连上继续用。
缺点:所有东西挤在一起,存满了没法扩容加大空间。
Redis 集群模式
把数据拆开,分成好几堆,每一堆数据都单独一组机器看管,每组都有主管 + 备用小弟。
哪一组的主管崩了,组内备用小弟直接顶上;整体数据装不下了,直接多加一组机器就行。
不用额外雇保安,每组自己就能完成故障顶替,既能无限加地方存数据,坏机器也能自动替补。
哨兵模式和redis集群模式的区别?
核心差别就一件事:哨兵所有数据是完整一份;集群是把数据劈成很多份分开存,别的故障自动顶替只是附加功能。
哨兵(全家共用一个大箱子)
全部数据完整装在一套主从里,没有拆分,每一台从机都存了全部一模一样的数据。
只有 1 个老大负责写数据,其他全是全量备份。
老大挂了,保安(哨兵)从备份里挑一个转正。
硬伤:箱子就这么大,数据塞满了,没法通过加机器扩大容量。
Redis 集群(买了好多个小箱子,数据分装)
整体数据切成很多小块,不同小块放在不同组机器里。A 组只存用户数据、B 组只存商品数据,每组的从机只备份本组的一小块数据,不是完整全量数据。
每一组都有自己的老大和备胎,某一组老大崩了,只有这一组的备胎顶上,别的组完全不受影响。
好处:数据装不下了,直接多加一组箱子就行,容量能无限扩容。
哨兵:你所有衣服全部放 1 个大衣柜,家里好几个一模一样的完整衣柜做备份。衣柜装满衣服后,再加衣柜也不能分担衣服,只是多一份备份。
集群:衣服分开放,上衣放柜子 A、裤子放柜子 B、鞋子放柜子 C,每个柜子都有备用柜子。上衣柜子满了,单独加一个上衣柜子就行,不用动裤子、鞋子的柜子。
二者故障自动顶替的能力确实类似,本质区别:
哨兵 = 多份完整全量数据做备份,不能扩容容量;
集群 = 数据拆分多组存放,既能备份容错,还能随便加机器扩容。
方案三:
一、网关层限流(Nginx / SpringCloud Gateway,最主流)
好比商场门口的安检闸门,直接在请求刚进来的大门口拦住控制。
- 提前设定好固定阈值:比如每秒最多只放行 20 个请求 去访问数据库;
- 系统会统计单位时间内的请求总数:
没超过 20 个:正常放行,去查 Redis、查 DB;
超过 20 个:后面新来的请求直接当场返回提示:“访问人数过多,请稍后重试”,根本不让走到数据库那一步。
两种常用计数规则:
固定窗口限流:每 1 秒清零一次计数器,1 秒内最多放 20 条;
令牌桶(最常用):系统后台匀速不断生成 “通行证令牌”,请求必须拿到令牌才能通行,令牌每秒只产出 20 个,天然卡死访问上限。
二、服务内部熔断降级(Sentinel、Hystrix)
请求已经进到业务代码里了,在 Java 服务内部做管控。
单独给「查询数据库」这个接口设置上限:比如并发最多允许 10 个线程同时操作 DB;
当同时操作 DB 的线程达到 10 个上限后,新来的调用直接走兜底逻辑:
返回默认友好页面、返回缓存旧数据、直接拒绝,不再继续调用数据库。
数据库是一个只能容纳 10 人的小房间:
限流:门口保安只允许 10 个人排队进屋,外面再多的人直接劝返;
降级:实在放不进去人了,直接告诉外面人 “暂时无法查询”,绝不硬塞人挤爆小房间。
方案四为什么先查本地内存再查redis,而不是先redis再本地内存?
- 本机内存速度碾压 Redis,优先读本地极致省时间
本机 Caffeine/Guava 是进程内内存,CPU 直接读写,延迟微秒级别;
Redis 是独立服务,走网络 TCP 通信,哪怕本地部署也要发包、收包,速度差几十上百倍。
先查本地缓存:大部分热点请求直接本机搞定,完全不走网络,响应最快。
如果反过来先查 Redis:每次都先走一次网络,白白浪费性能,多级缓存完全失去意义。 - 雪崩场景的核心目标:Redis 挂了,直接绕开 Redis 保护 DB
多级缓存本来就是Redis 故障兜底方案:
设计目的就是:Redis 崩了、宕机、大面积失效时,靠本机内存扛住流量。
顺序:本地缓存 → Redis → DB
Redis 炸了之后,请求命中本地缓存,直接返回数据,根本不会去碰数据库,完美达成保护 DB 的目的。
如果反过来:Redis → 本地缓存 → DB
Redis 一旦整体宕机,所有请求第一步就要先连接故障 Redis,大量连接超时、排队阻塞,请求堵死之后才会降级走本地缓存,不仅接口超时严重,大量无效连接还会拖垮应用服务。 - 热点数据特征:高频访问数据长期驻留本地,命中率最高
热门商品会被大量重复访问,第一次查询时:本地没数据→查 Redis 拿到数据→写入本地内存。
后续几百上千次请求,全部直接命中本机内存,不用重复访问 Redis。
调换顺序的话,热点每次都先跑 Redis,本地缓存几乎很难被命中,本地缓存形同摆设。
补充一个小细节:数据一致性小取舍
本地缓存会做短暂过期(比如几十秒),就算本地数据稍微旧一点,也只是极短时间不一致,和逻辑过期思路一致,优先保可用性、保性能。
先本地后 Redis:速度最快,Redis 崩了能直接兜底绕开故障,本地缓存利用率拉满;
先 Redis 后本地:多走网络变慢,Redis 故障时大量请求先阻塞超时,本地缓存基本没用。
缓存-双写一致性
双写一致性,特指:MySQL 数据库 和 Redis 缓存 两处都要存同一份业务数据,执行修改操作之后,库里面的数据、缓存里面的数据必须保持一模一样,不能一个新、一个旧。
方案 A:先删缓存 → 再更新数据库
标准执行步骤
执行删除 Redis 缓存
执行 update 语句,更新数据库,修改 MySQL 数据
查询缓存,未命中,查询数据库,写入缓存
漏洞场景(高并发读写交叉)
并发同时进来:1 个写请求 + 1 个读请求
时序插队:
写请求:删掉 Redis 缓存
读请求立刻到达:Redis 无数据(缓存未命中),查询 MySQL,拿到还未更新完成的旧数据
读请求将旧数据重新写入 Redis
写请求此时才完成 MySQL 的更新操作

漏洞后果
MySQL 已经是新数据,Redis 被回填旧数据,永久数据不一致,只要没人手动删缓存,后续所有查询一直读旧数据,故障持续性存在。
方案 B:先更新数据库 → 再删除缓存(行业主流)
标准执行步骤
执行 update 语句,修改 MySQL 数据
执行删除 Redis 缓存
查询缓存,未命中,查询数据库
写入缓存

漏洞场景(极低概率读写交叉)
并发同时进来:1 个写请求 + 1 个读请求
时序插队:
线程1查询缓存,发现缓存过期,缓存中的数据没有了,那我缓存未命中,查询数据库,把旧数据拿出来,还没有同步到缓存之前,线程切换到线程2,更新数据库,然后删除缓存,现在缓存没有数据,所以删了没用,最后回到线程一把旧数据写到redis。

漏洞后果
仅产生一次脏缓存,MySQL 新、Redis 旧,属于短暂不一致;新的查询到来时,如果再次触发缓存未命中,会自动加载最新数据修复。
漏洞发生概率远低于方案 A。
我们发现只有线程交互进行,才会有概率出现双写不一致。
漏洞补救方案:方案 B 基础上加 延迟双删
执行步骤
更新 MySQL 数据库
立即第一次删除 Redis 缓存
等待一段延迟时间(大于业务最长查询耗时,一般几百 ms)
第二次删除 Redis 缓存
我们用完整流程拆解,初始:Redis=10,DB=10,要更新到 20
读请求发起,查询 Redis,拿到数字 10,此时确实可以直接把 10 返回给用户,用户这边请求已经结束了。
但是代码里有逻辑:哪怕已经返回结果,很多框架依然会执行异步回填 Redis的操作,也就是:用户收到结果先走了,后台子线程还在慢悠悠执行「把 10 写入 Redis」这一步。
就在这个后台子线程卡顿、迟迟没执行写入的空档,写操作完成了整套动作:
DB 更新为 20
立刻删除 Redis,Redis 变成空
卡顿的后台子线程终于恢复运行,此时 Redis 是空状态,它正常执行逻辑:把手里的旧值 10 存入 Redis。
最终就出现:DB=20,Redis=10,数据不一致。
用户体验层面
用户拿到 10 就已经收到响应、页面加载完成,感知不到任何延迟,请求主体早就结束了。
服务后台层面
刷新缓存的动作是后置异步做的,和给用户返回结果拆开了。用户先走,后台干活,后台干活卡住,就会造成脏数据回填。
为什么延迟双删能解决
我们改完数据库、删完缓存后,故意等几百毫秒。
就是专门等这些后台异步的慢写入全部执行完毕,等旧的 10 已经被强行写进 Redis 之后,我们再执行第二次删除,直接把脏数据清掉。
下一次正常查询,会去 DB 拿最新的 20,重新存入 Redis。
作用
等待慢读请求完成旧数据回填动作,之后二次删除直接清掉刚写入的脏旧数据,下一次访问自动加载最新数据,几乎杜绝数据不一致问题。
横向对比总结
方案 A 漏洞:冲突必造成永久脏数据,风险极高,基本不使用
方案 B 原生漏洞:冲突只产生单次短暂脏数据,风险低,作为基础方案
方案 B + 延迟双删:绝大多数业务最终落地形式,实现最终一致性
异步回填:把「往 Redis 存数据」这件事拆出去,单独丢给后台小线程偷偷做,主线程先立刻把数据返回给用户,用户不用等待,后台慢慢去补存 Redis。导致第一次删redis时,redis变为空的, 后续延时过后旧数据又回到redis,所以要第二次删redis。

读写锁天生默认不是写优先,写优先不是锁自带的本能,是代码人为加了一套排队规矩硬做出来的,不是锁天然就该让写先走。
拆开大白话讲:
-
原生普通读写锁:规矩很简单
多个读可以一起干活
只要还有读在干活,写就必须排队死等
逻辑出发点:查询请求量远大于修改,优先保证查询吞吐量,这是大多数业务默认需求。
所以原生是读优先,写本来就没有任何优先权。 -
我们想要 “写优先”,完全是业务需求倒逼的
什么时候必须让写插队?
比如:下单改库存、改订单状态、用户余额变动。
这些修改属于敏感操作,必须尽快生效,如果被几十上百个查询一直堵着,写迟迟执行不了,会出现:
超卖、状态延迟错乱、缓存长时间脏数据,业务故障风险远大于查慢一点。
为了业务安全,我们主动定了新规则,强行制造写优先:
只要有改数据的人开始排队了
后面新来的所有查询,一律不许进场干活,全部去后面排队
已经正在查的人,允许做完(不能强行中断,会崩事务)
正在查的全部结束后,第一个就叫醒排队的写操作先干活
写完了,才放行堆积的查询 -
总结 “凭什么写能优先”
不靠锁本身偏心,靠三条人为约束:
新查询直接限流禁止入场,不再增加正在运行的读;
不再给读新的执行机会,只消耗现存正在跑的读;
等待队列里,写的排序优先级高于后面所有排队的读。
通俗举例
食堂窗口:
普通模式:吃饭的人(读)可以一群一起站窗口吃;打包带走的(写)必须等所有人吃完。
写优先模式:一旦有人要打包:
正在吃饭的人慢慢吃完走人,外面新来吃饭的全部拦住不许进来,里面清空后,立刻先给打包的办理,打包完了再放外面吃饭的进来。
打包没有天然特权,是管理员临时加了新规,限制新食客入场,才做到打包优先处理。
基础两个锁的本职功能(底层不变)
共享锁(读锁)
作用:允许多个人同时持有、同时干活。
规则:
加了共享锁,其他人还能继续加共享锁(查询扎堆一起跑)
加了共享锁,任何人加不了排他锁(修改进不来)
排他锁(写锁)
作用:只能一个人独占,其他人啥锁都加不上。
规则:
加了排他锁,别人既不能加共享锁,也不能加排他锁
写写互斥、读写互斥
一句话:锁本身只管「能不能同时干活」,完全不管谁先来后到、谁优先级更高,优先级是排队规则额外加的,不是锁自带。
第一部分:默认读优先读写锁里,两把锁怎么配合
场景 A:现在只有查询在跑,大量线程加共享锁
共享锁叠加生效,一堆查询并行执行
此时来了一个修改操作,想去加排他锁
因为当前存在共享锁,排他锁直接被挡住,进入排队
只要持续不断有新查询过来、持续新增共享锁,排他锁会无限等下去(写饥饿)
场景 B:现在修改正在跑,持有排他锁
排他锁独占资源
新来的查询想加共享锁、新来的修改想加排他锁,全部阻塞排队
修改执行完毕释放排他锁后,排队的查询统一批量加上共享锁,一起执行
本模式下两把锁分工
共享锁负责拉高查询并发;排他锁负责独占修改;
锁只控制互斥,队列排队默认:先来先排,读请求源源不断插队排队,写永远排后面。
第二部分:写优先模式里,两把锁功能没变,只是加了准入门槛
锁本身的兼容 / 互斥逻辑一丝没变,变化的是:能不能允许新请求去加锁
核心改动:只要队列里已经有等待的排他锁(写请求),直接禁止新线程加共享锁。
分步拆解:
一批老查询正在运行,持有共享锁
一个修改到达,尝试加排他锁失败,进入等待队列,系统打上标记:有写在等
此时外面新的查询请求过来:
不是不让它加共享锁,是规则直接禁止它去申请共享锁,强制排到队伍末尾
不会再新增活跃的共享锁,正在运行的共享锁只会越来越少
等所有老查询陆续执行完、全部释放共享锁,当前无任何锁占用
系统优先唤醒排队里的写请求,成功加上排他锁,独占执行修改
修改完成释放排他锁,标记清除,才允许积压的查询重新申请共享锁,并发执行
写优先模式下两把锁分工
共享锁:依然可以多线程并发执行,但是只允许之前已经拿到锁的老读继续跑,新读禁止获取;
排他锁:互斥属性完全不变,只是在资源空闲时,队列里排他锁的唤醒顺序,强排在共享锁之前;
精简总结
共享锁、排他锁 = 负责「能不能一起干活」的权限工具,互斥逻辑固定不变;
读优先:工具正常使用,新读随便加共享锁,写的排他锁一直排队;
写优先:工具功能不变,只是人为限制新读禁止申请共享锁,耗尽现有共享锁后,优先放行排他锁执行;
优先级是调度规则决定的,不是共享锁、排他锁本身自带优先级。




缓存-持久化

看图就行


主进程进行写操作,拷贝了数据副本B,来进行写操作。避免主进程修改数据,子进程同时在读和写,会有冲突甚至脏数据。


一般everysec。


缓存-数据过期策略




缓存-数据淘汰策略
这个我前面提到过,内存淘汰用的是allkeys-Iru,可以返回看看。下面是各种淘汰规则:




redis分布式锁-使用场景
一般情况的抢劵场景


特殊场景(超卖)

线程是并发进行的,现在库存是1,两个都查询了优惠卷数量为1,并发性的扣减库存,导致超卖,库存-1。
下面是有锁流程

优点
实现简单,无额外中间件成本,快速解决超卖问题
逻辑清晰:锁范围包裹完整事务动作,原子性有保障
致命缺点(生产环境几乎不使用)
- 锁作用域局限单机
synchronized是 JVM 本地锁,只在单个 Tomcat 服务实例生效。如果项目做集群多机部署,不同服务器的线程之间无法互斥,依然会发生超卖。 - 并发性能极差
所有请求串行排队执行,秒杀高并发场景下吞吐量极低,容易出现大量请求阻塞超时。 - Redis 操作多次网络往返
一次抢购包含get、set两次 Redis 网络 IO,加锁放大了耗时,进一步降低 QPS。

分布式锁 VS 之前的 synchronized 本地锁
先回忆旧问题:本地锁为啥集群就崩了
之前用的 synchronized 是机器内部锁,只管住同一台服务器里的多个线程排队。
现在项目搭了集群:两台服务器 8080、8081 分开跑,两台机器的锁互相看不见:
8080 的线程拿了自己机器的锁
8081 的线程完全不受影响,照样同时去查 Redis、减库存
最后就出现超卖,优惠券库存变负数。
分布式锁核心本质:把锁放在公共地方,所有机器一起看
分布式锁不再把锁藏在各自服务器内存里,而是把锁存到公共中间件(这里是 Redis)。
不管请求打到 8080 还是 8081,所有服务器抢券前,都必须先去同一个公共位置抢锁,谁抢到谁执行减库存操作,没抢到的全部排队阻塞。
对照图里的流程分步白话拆解
两台服务器(8080、8081)各自的线程,同时跑去 Redis 公共位置抢分布式锁
8080 的线程 1 抢先拿到锁:
正常执行:查优惠券库存 → 判断够不够 → 扣减 Redis 库存 → 最后归还公共锁
同一台 8080 里的线程 2:拿不到锁,原地阻塞等待
8081 的线程 1 抢锁失败:
哪怕它是另一台机器的线程,公共锁被占着就必须卡住,不能往下执行查库存
直到 8080 线程 1 操作完毕、把公共锁释放之后,8081 的线程 1 才能拿到锁,再去查询库存
此时库存已经被扣完,直接返回 “抢完了”,彻底杜绝跨机器并发超卖。
最通俗比喻
本地 synchronized 锁:每间教室自己的门栓,A 教室锁上门,B 教室完全不受影响,两屋人能同时干活。
分布式锁:全教学楼只有1 把公共大门钥匙,不管你来自哪个教室,想干活必须先抢这唯一钥匙。拿到钥匙才能进门操作,操作完把钥匙放回门口,下一个人再抢。
分布式锁解决了什么、保留了什么
✅ 解决痛点:集群多服务器部署下的跨机器并发超卖问题,锁全局生效
⚠️ 保留短板:所有请求串行排队执行,高并发秒杀场景吞吐量依然偏低;同时还要额外处理锁超时、死锁、锁误释放等新问题。
redis分布式锁-实现原理(setnx,redisson)

为什么要设置超时时间?
避免死锁,业务超时或业务宕机,导致锁一直没被释放。其他线程都无法执行。
还有就是怕业务没进行完,锁的时间就到了,就有了图上的两种方法。



更多推荐



所有评论(0)