尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Redis缓存穿透雪崩击穿与分布式锁三代实现
缓存这套东西写得再熟真到了线上出问题的时候往往还是那几个老面孔在作祟某个不存在的 ID 被打了几万次、零点一过一批 key 集体失效、热点商品下架瞬间数据库连接池拉满、两个人同时改同一个配置互相覆盖。这些问题单独看都不复杂难的是它们经常结伴出现而且现象互相伪装——你以为是缓存击穿实际是并发竞争把值写脏了你以为是 Redis 挂了实际是雪崩把请求全推到了数据库。这篇就把Redis缓存这一类问题从头捋一遍穿透、雪崩、击穿、双写不一致、并发竞争再顺着并发竞争这条线聊到分布式锁的三代实现watch乐观锁、setnx、Redisson。适合已经用过 Redis、但还没被线上问题真正捶打过的同学也适合准备面试时想听点人话版原理的同行。1. 缓存穿透的真正形态不是查不到是有人故意让你查不到穿透这个词听着抽象落到代码里其实特别具体一个请求进来先查缓存没命中转去查数据库数据库也没有于是什么都不会被写进缓存。下一次同样的请求打进来重复一遍上述流程。单次请求没什么但当这个 key 被高频重复访问数据库就成了一道没有闸门的门所有流量原样灌进去。1.1 一次空结果被反复放大的完整链路很多人对穿透的理解停留在缓存没命中这不准确。缓存没命中叫 miss是正常现象穿透的关键在于这次 miss 之后没有任何东西被沉淀下来。正常的查询会把结果写回缓存下次再来就命中了而查不存在的数据结果为空如果代码里没有对空结果做特殊处理那就等于每次都要重新走一遍数据库。我见过最典型的一个场景是商品详情页。前端路由里带着?id12345商品下架之后数据库里删掉了但外部页面还留着旧链接搜索引擎、爬虫、用户收藏夹、别人转发的分享卡片全都在往外喷这个已经不存在的 id。请求量不大但特别持久——一天二十四小时不间断地打。数据库里select * from item where id 12345走主键索引单次很快但一天几十万次连接池就被这种无效查询占住了。还有一种更恶劣的参数没做校验。接口GET /user/{id}id 是字符串类型攻击方直接传1 or 11或者-1、999999999999。前者在 MyBatis 里如果用了${}拼接就是注入后者是纯粹的数字越界。这类请求在缓存里永远查不到在数据库里也永远查不到但每一次都要消耗一次数据库连接和一次 SQL 解析。提示排查穿透时不要只看 QPS要看数据库的Com_select增长曲线和慢查询里有没有大量主键查空的记录。慢查询日志里出现执行时间极短、返回行数为 0 的 SQL 高频重复基本就是穿透的指纹。1.2 空值缓存、布隆过滤器、参数校验的取舍解决穿透的路子有三条代价和收益差别很大不能无脑全上。空值缓存是最省事的。查到数据库为空时往缓存里写一个带特殊标记的值TTL 设短一点比如 60 秒。这样同一个不存在的 id 在一分钟内只会打一次数据库。它的优点是实现成本几乎为零缺点是会污染缓存空间——如果有大量随机 id 攻击你等于用攻击方的参数把缓存撑爆把真正有用的热点数据挤出去。所以空值缓存必须配一个前置条件参数合法性校验。id 必须是正整数、长度在合理区间、符合业务编号规则比如订单号有固定前缀不合法直接返回连缓存都不用查。这一步能挡掉绝大部分随机参数攻击。布隆过滤器是更彻底的方案适合数据集合相对固定、能提前全量预热的场景。原理是用一个很长的位数组加多个哈希函数把存在的 key 映射到若干位上查询时只要有一位是 0就百分之百不存在全是 1 则可能存在。注意是可能因为不同 key 的哈希可能撞到同一位上这就是误判。参数怎么定给个实际算例假设业务数据量 n 1000 万我们能接受 1% 的误判率 p 0.01。位数组长度 m -n·ln(p) / (ln2)² ≈ 1.44 × n × log₂(1/p)代入得约 9600 万位也就是约 11.4 MB。哈希函数个数 k (m/n)·ln2 ≈ 6.6取 7 个。11 MB 内存换 1% 的误判误判时仍然会去查数据库只是概率低这个账是划得来的。方案实现成本内存开销适用场景主要缺陷参数校验极低无所有接口只能挡住明显非法参数空值缓存低与攻击量正相关数据集合不可预知恶意随机 key 会撑爆缓存布隆过滤器中与数据量线性相关集合可全量预热不支持删除误判无法归零1.3 我在实际项目里的组合拳单用一种都不够。我一般的做法是三层入口做参数格式校验挡掉明显不合法的中间加布隆过滤器挡掉集合外的最后加空值缓存兜底挡住布隆过滤器那 1% 的误判。三层叠下来打到数据库的无效请求基本能压到千分之一以下。布隆过滤器有个坑要提前想清楚它不支持删除。商品下架、用户注销你没法把对应 key 从位数组里抹掉。如果业务上删除很频繁位数组会逐渐被占满误判率飙升最后退化成形同虚设。这时候要么换 Counting Bloom Filter用计数器代替单 bit能删除但要几倍内存要么干脆老老实实只用空值缓存。还有一个容易被忽略的细节空值缓存写进去的那个空序列化方式要和正常值区分开。我们用 Jackson 序列化正常对象反序列化成 POJO空值我写的是字符串NULL读取时先判断类型再判断内容。曾经有同事直接往里面塞了个null结果 Redis 里存的是\xac\xed\x00\x05...这种 Java 序列化头读出来反序列化报错整个接口 500排查了半天才发现是缓存了 null这件事本身没被设计过。2. 缓存雪崩过期时间撞车比 Redis 宕机更常见雪崩这个词容易让人联想到 Redis 整个挂掉但实际上绝大多数雪、是人造的——批量预热、统一 TTL、定时刷新都会让一批 key 在同一秒集体失效。那一瞬间所有请求同时穿透到数据库数据库连接池瞬间打满然后所有依赖它的服务一起卡死。2.1 为什么整点过期的 key 会形成雪崩想象一下这个场景运营在后台配置了 200 个首页楼层代码里统一写死expire 3600全部在凌晨 2 点整点通过定时任务预热。到了 3 点整这 200 个 key 在同一秒到期首页的请求全部落空全部去查数据库。首页 QPS 平时是 5000这时候 5000 个请求同时查库每个 SQL 要 join 三张表就算单条 20ms5000 并发需要的连接数也远超连接池的 50。更麻烦的是连锁反应。数据库被压住之后不光是首页查不动所有连同一个库的业务全部排队超时时间一到线程池里的线程全被占满网关开始 504用户刷新请求量进一步上升形成正反馈。这里有个反直觉的点要强调雪崩的杀伤力主要不来自数据库变慢而来自连接池和线程池的占满。数据库其实没死它只是排队但你的应用层线程都在等数据库响应一分钟内就被耗光然后连正常的健康检查都过不去容器编排平台判定实例不健康开始重启重启之后缓存是空的又来一次全量回源雪上加霜。2.2 随机过期窗口该开多大最直接的对策是给 TTL 加随机抖动让过期时间散开。但抖动范围开多大是有讲究的。一个经验公式抖动范围取基础 TTL 的 10% 到 20%。基础 TTL 是 1 小时3600 秒抖动就取 0 到 600 秒之间的随机数。这样 200 个 key 的过期时间会摊在 10 分钟内平均每分钟只失效 20 个数据库完全吃得消。实际 TTL 基础 TTL random(0, 基础 TTL × 0.2)如果抖动开得太小比如只加 60 秒那 200 个 key 还是挤在 1 分钟内等于没解决。如果开得太大比如基础 1 小时抖动 1 小时那有些 key 可能 2 小时才过期数据新鲜度会受影响对时效性要求高的业务比如库存、价格就不能这么干。对于库存、价格这种强时效数据我的做法是不用抖动改用主动刷新在 key 快要过期前的 1 到 5 分钟内由一个后台任务异步重建缓存重建成功后再延长 TTL。这样缓存永远不会自然过期到空而是旧值撑着新值后台算。2.3 多级缓存与降级开关的实际配法抖动只解决了同时过期解决不了Redis 集群整体不可用。后者要靠多级缓存和降级。多级缓存的典型结构是本地缓存Caffeine 远程缓存Redis。本地缓存扛高频只读的热点数据命中率往往能到 60% 以上即使 Redis 全挂这部分请求也不会打穿到数据库。代价是一致性本地缓存更新有延迟多个实例之间不同步。我的做法是给本地缓存设一个很短的 TTL比如 3 秒同时订阅 Redis 的失效通道__keyevent0__:expired收到消息立刻清本地。3 秒是兜底防止消息丢失导致本地缓存长期脏着。3 秒的延迟对大部分读多写少的场景商品详情、文章内容、配置项完全可接受但对库存这种绝对不能忍库存我就不放本地缓存。降级开关是最后一道闸。用 Sentinel 或类似组件在数据库前面挡一层当数据库的慢 SQL 比例或连接池使用率超过阈值直接对非核心接口返回兜底数据比如空列表、默认推荐、静态文案保证核心链路下单、支付的连接不被抢走。// 降级开关的伪代码示意阈值要按实际连接池容量反推 if (dbConnectionPoolUsedRate 0.85) { degradeSwitch.on(); // 打开降级 return Collections.emptyList(); // 非核心查询直接返回空 }这里有个经验降级阈值不能拍脑袋定。连接池 50 个连接如果单条 SQL 平均 20ms那理论吞吐是 2500 QPS。降到 85% 使用率时大约是 2100 QPS此时响应已经开始变慢再有 10% 的流量增长就会雪崩。所以阈值应该从最大安全吞吐量倒推而不是从连接数快满了这个直觉出发。3. 缓存击穿热点 key 失效那一秒慢查询会被放大成风暴击穿和雪崩的区别在粒度雪崩是一大批 key 同时失效击穿是单个热点 key 失效。听起来范围小但杀伤力不见得低——因为热点 key 的访问量可能占整个系统的 30%它失效的那 1 秒钟就是 1500 个并发同时去查同一条数据。3.1 互斥重建和逻辑过期的本质差别解决击穿有两大流派本质区别在于要不要让读请求等待。互斥重建谁发现缓存空了谁去抢一把锁抢到的那个去查数据库、写缓存没抢到的等待或重试。好处是数据一定是最新的坏处是等待期间接口响应变慢如果重建耗时 200ms那这 200ms 内所有请求都在等用户体验是有感知的。逻辑过期缓存里存的不是业务对象本身而是一个包装体里面带着真正的数据和它的逻辑过期时间。缓存物理上永不过期或者设一个很长的 TTL每次读出来先判断逻辑时间没过期就直接用过期了也不阻塞——先返回旧数据同时提交一个异步任务去重建。好处是几乎没有等待坏处是会有短暂的脏数据窗口。维度互斥重建逻辑过期数据新鲜度强一致重建完就是新值最终一致有秒级脏窗口响应延迟冷启动时有尖刺平稳无尖刺实现复杂度低一把分布式锁即可中需要异步线程池 包装体失效场景锁服务不可用时会退化成穿透异步任务失败时长期返回旧值适用业务价格、库存、权限商品详情、文章、推荐位选择标准很朴素你能不能忍几秒钟的旧数据。商品详情能忍价格不能忍文章内容能忍登录态不能忍。3.2 用 SETNX 做重建锁的完整写法互斥重建的核心是只有一个线程去重建。用 Redis 的SET key value NX PX ttl就能实现但细节很多写错了就是给自己埋雷。public String getWithMutex(String key) { // 1. 先查缓存 String cached redis.get(key); if (cached ! null) { return cached; } String lockKey lock:rebuild: key; String lockValue UUID.randomUUID().toString(); // 必须是唯一值 // 2. 尝试加锁30 秒自动过期防止死锁 Boolean locked redis.set(lockKey, lockValue, NX, PX, 30000); if (Boolean.TRUE.equals(locked)) { try { // 3. 双重检查拿锁期间可能别人已经重建完了 cached redis.get(key); if (cached ! null) { return cached; } // 4. 查库并回写 String dbValue loadFromDb(key); redis.set(key, dbValue, PX, 3600_000); return dbValue; } finally { // 5. 用 Lua 脚本比对 value 后再删除防止误删别人的锁 String lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(lockValue)); } } else { // 6. 没抢到锁短暂休眠后重试不要死循环 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getWithMutex(key); // 生产环境建议改成有限次重试 } }这份代码里有三个不能省的细节。第一是lockValue用 UUID不能用固定值否则释放锁时无法区分是我的锁还是别人的锁。第二是释放必须用 Lua 脚本做原子比对get再del中间有个窗口锁可能刚好过期被别人拿走你这一del就把别人的锁删了。第三是重试要有限次上面的写法在极端情况下可能栈溢出实际项目里我会用一个 for 循环限定最多重试 3 次超过就返回兜底值。3.3 逻辑过期方案里的时间窗设计逻辑过期的实现要点是包装体设计。public class LogicalExpireWrapperT { private T data; // 真正的业务数据 private long expireAt; // 逻辑过期时间戳毫秒 public boolean isExpired() { return System.currentTimeMillis() expireAt; } }写入时expireAt设成当前时间 业务 TTL同时把缓存本身的物理 TTL 设成一个远大于它的值比如逻辑 5 分钟、物理 2 小时。这样即使异步重建失败最多 2 小时内还能拿到旧数据不至于全空。读取时的逻辑是命中且未过期直接返回命中且已过期返回旧值同时提交异步重建任务未命中物理过期了走互斥重建。异步重建的线程池必须隔离不能和业务线程池共用否则重建任务排队会拖慢正常业务。我给重建任务单独配了一个ThreadPoolExecutor核心线程 2、最大 4、队列容量 1000、拒绝策略用DiscardPolicy——队列满了说明重建积压严重这时候多丢一个任务无所谓反正下一次读还会再触发。还有个坑重建任务要按 key 去重。同一个过期 key 在 1 秒内可能被读几千次如果不做去重几千个重建任务同时提交等于自己造了一次小型的击穿。我的做法是用一个并发的Set记录正在重建中的 key提交任务前先addadd失败就跳过任务结束后remove。这个 Set 是单机内存里的多实例之间还是可能重复但重复度从几千降到实例数可以接受。4. 双写不一致删缓存和写库的顺序决定了不一致的概率缓存和数据库是两套存储两个写操作不可能原子完成所以不一致是必然存在的我们能做的是把不一致的窗口压到最小、把不一致的持续时间压到最短。4.1 四种更新顺序的推演理论上就四种组合逐个推一遍就知道为什么大家最后都选了先更新库、再删缓存。顺序并发场景结果结论先删缓存再更新库删完、更新前读请求进来回填旧值缓存里是旧值长期不一致不推荐先更新库再删缓存删缓存前读请求拿旧值回填极端并发下短暂不一致主流方案先更新库再更新缓存两个写并发后提交的可能先写缓存缓存被覆盖成旧值不推荐先删缓存再更新库延迟再删延迟期内读请求回填旧值第二次删除把旧值清掉可用但复杂第一种先删缓存的问题最严重删完之后、数据库还没更新这几十毫秒里任何一个读请求都会把旧值重新写进缓存而数据库更新完之后不会再管缓存于是缓存就长期是旧的直到 TTL 到期。这个窗口概率不低因为是并发场景。第二种先更新库再删缓存的问题窗口非常小只有读请求恰好查到旧值、还没来得及写缓存、写请求已经把库更新完并删了缓存、读请求再把旧值写进去这四个动作严格按这个顺序发生概率大概是毫秒级窗口里的竞争。理论上存在实际很难复现。第三种更新缓存问题最大两个写请求并发A 先提交数据库但后写缓存B 后提交数据库但先写缓存结果缓存里是 A 的值旧值数据库里是 B 的值新值从此长期不一致。所以永远不要用更新缓存代替删除缓存。第四种延迟双删是第一和第二种的折中本质是想弥补第一种的缺陷先删一次更新数据库再延迟删一次。延迟时间要大于一次读请求从查库到回写缓存的耗时通常设 500ms 到 1s。但它引入了新的不确定性——延迟时间没法精确估而且第二次删除失败就没兜底了。我的实践结论是绝大多数业务用先更新库、再删缓存加 TTL 兜底就够了别为了追求理论一致把系统搞复杂。真正不能容忍不一致的场景比如账户余额根本不该用缓存方案应该直接读库或者用专门的强一致存储。4.2 删除失败的补偿与 binlog 兜底先更新库再删缓存也有一个必须处理的问题删缓存失败怎么办。Redis 网络抖动、集群切换del命令是有可能失败的。缓存如果不删TTL 又设得长那不一致会持续很久。常规的补偿手段是重试。但重试不能直接在业务线程里循环会拖慢响应。我的做法是第一次删除失败把 key 丢进一个本地队列或消息队列由消费者做二次删除失败则按指数退避重试1s、2s、4s、8s最多 5 次5 次都失败记一条告警日志人工介入或者依赖 TTL 兜底。更彻底的办法是订阅数据库的 binlog。数据库的每次变更都会写 binlog用一个中间件比如 Canal伪装成从库去订阅解析出变更的表和主键然后异步删缓存。这个方案的好处是把删缓存从业务代码里彻底剥离业务只需要写库缓存删除由数据管道负责不会漏。坏处是引入了一个新的组件复杂度上升小团队维护成本不小。我的建议是分阶段单表数据量不大、写入不频繁的业务用删缓存 重试 TTL就够写 QPS 上千、涉及多张表级联更新的再考虑上 binlog 方案。别一上来就搞数据管道最后运维成本把自己压垮。4.3 TTL 兜底不是偷懒是最后一道保险很多同学觉得加了 TTL 就是承认自己方案不完美。我反倒认为 TTL 是必须的因为它兜住的是所有你没想到的异常路径代码 bug 漏删了、网络分区导致删除丢了、有人绕过应用直接改了库、上游服务回滚了数据。这些情况你没法在代码里一一枚举只能靠过期时间来强制对齐。TTL 设多长是个业务问题没有标准答案。我的经验区间数据特性TTL 建议理由商品详情、文章内容10 分钟 ~ 1 小时可接受长一点减少回源用户资料、配置项5 ~ 30 分钟变更不频繁库存、价格10 ~ 60 秒宁可多回源不能错榜单、推荐位1 ~ 5 分钟本身就是准实时关键是要有人对 TTL 负责。我们团队的做法是在缓存写入的地方统一走一个工具类TTL 必须从枚举里取不允许直接写数字。这样 review 的时候一眼就能看出这个地方为什么用这么短的 TTL而不是半年后谁都不知道 3600 这个数是哪来的。5. 数据并发竞争锁加错对象比不加锁更危险前面说的都是缓存和数据库之间的问题这一段说的是多个客户端同时操作同一个 key。最典型的两个场景库存扣减和配置更新。5.1 读-改-写序列里的丢失更新考虑这个流程两个线程都要给某个商品的库存减 1。线程 AGET stock:1001 → 10 线程 BGET stock:1001 → 10 线程 ASET stock:1001 9 线程 BSET stock:1001 9最终库存是 9但实际卖出了 2 件应该是 8。这就是经典的丢失更新。Redis 的命令是原子的但GET和SET是两条命令中间可以被其他客户端插进来。有人会说Redis 是单线程的不会有并发问题。这句话只对了一半Redis 处理单条命令是串行的但你的业务逻辑不是一条命令是多个命令组成的序列序列之间完全可能被其他客户端穿插。解决方案按成本从低到高排第一条路是原子命令。能用INCR、DECR、HINCRBY、SETNX解决的就别加锁。库存扣减完全可以用DECRBY stock:1001 1原子、高效。但要注意DECRBY不会在库存变成负数时自动拦截需要配一个 Lua 脚本做判断 扣减-- KEYS[1] stock key, ARGV[1] 扣减数量 local current tonumber(redis.call(GET, KEYS[1])) if current nil or current tonumber(ARGV[1]) then return -1 -- 库存不足 end redis.call(DECRBY, KEYS[1], ARGV[1]) return current - tonumber(ARGV[1])这个脚本在 Redis 里是原子执行的判断和扣减之间不会被插入。第二条路是乐观锁。如果逻辑比扣个数复杂比如要先算价格再扣库存Lua 脚本写起来就费劲了这时候可以用WATCH配合事务。WATCH会监视一个 key如果在EXEC之前这个 key 被其他客户端改过整个事务就会失败返回 nil业务侧需要重试。WATCH的缺点很明显冲突高的时候重试次数会爆炸。100 个线程抢同一个 key平均只有一个能成功其余 99 个都要重试重试的 99 个里又只有一个成功……这就是典型的活锁。所以WATCH只适合冲突率很低的场景比如读配置、改配置这种平时没什么人动的操作。第三条路是分布式锁。加锁把事情串行化虽然吞吐下降但语义最清晰。这也是下一节要展开的内容。这三条路的选型判断其实很简单问自己冲突概率有多高。低冲突用WATCH或者乐观重试中高冲突用分布式锁能用原子命令解决的永远优先用原子命令别上锁。5.2 加了锁还出问题通常是锁的粒度错了我见过一个案例某系统为了防止库存超卖在服务入口加了一把分布式锁锁的 key 是lock:order所有下单请求串行执行。结果压测时 QPS 从 3000 掉到 80。问题不在锁本身在锁的粒度。所有订单共用一把锁等于把整个下单链路串行化跟没做并发设计一样。正确的粒度应该是lock:stock:{itemId}按商品维度加锁不同商品之间互不影响同一商品的并发被串行化——这才是业务真正需要的。粒度错误还有一个反面锁得太细。比如按userId加锁但业务是全局限量 100 件那用户的锁根本挡不住超卖因为不同用户之间的竞争没被覆盖。判断粒度是否正确的标准是锁保护的临界区里面的读-改-写序列是不是都在操作同一份共享资源。是就用这把锁不是就说明锁的范围和资源不匹配。6. 分布式锁的三代实现watch、setnx、Redisson 各自解决了什么顺着并发竞争往下走绕不开分布式锁。这一块的历史演进特别有意思每一代方案都是为了补上一代的漏洞理解这个演进链条比死记 API 有用得多。6.1 WATCH 乐观锁适合低冲突但 ABA 和重试是硬伤WATCH是 Redis 原生事务的一部分。用法是先WATCH key然后MULTI开启事务把要执行的命令入队最后EXEC提交。如果被 watch 的 key 在EXEC之前被别人改过EXEC返回 nil事务整体不执行。WATCH lock:config # 读取当前值 GET lock:config # 业务判断... MULTI SET lock:config new-value EXEC # 返回 nil 说明被改过需要重试整个流程它最大的价值是不加锁也能实现互斥语义对 Redis 的侵入最小。但三个问题绕不过去第一重试成本高。冲突时整个业务逻辑要重跑如果业务逻辑里有耗时操作重跑代价很大。第二ABA 问题。WATCH判断的是key 有没有被改过如果 A 改成 B 又改回 AWATCH是感知不到的。好在 Redis 的WATCH实现是基于版本号/客户端修改标记的实际上比单纯的值比较要严格一些但如果你自己实现 CAS比如自己GET出来比较再SETABA 就是实打实的问题。第三它不能跨客户端串行化。WATCH事务失败后所有客户端都在重试大量无效计算。在写多读少或者冲突频繁的场景性能会塌。所以WATCH的实际定位是读多写少、冲突极低的配置类数据。它不适合用来做业务上的分布式锁。6.2 SETNX 锁的四个必须处理的细节SETNX是很多人手写分布式锁的起点。早期写法是SETNX lock 1加上EXPIRE lock 30但这两条命令不是原子的如果SETNX成功后客户端崩了EXPIRE没执行锁就永远不释放。后来 Redis 2.6.12 提供了SET的扩展参数一条命令搞定SET lock:order:1001 uuid-xxxx NX PX 30000NX表示 key 不存在才设置PX 30000表示 30 秒后自动过期。这一条命令是原子的解决了死锁问题。但要做到生产可用还有四个细节第一value 必须唯一。用 UUID 或者实例 IP 线程 ID。如果 value 是固定值释放锁的时候你没法判断这把锁是不是自己加的。第二释放必须用 Lua 比对。前面击穿那一节的代码已经写过GET再DEL不是原子的中间锁可能过期被抢走。第三超时时间要估准。锁的 TTL 必须大于业务执行时间否则业务没跑完锁就过期了另一个线程进来两个线程同时操作共享资源互斥失效。但 TTL 设太长客户端崩了之后锁要等很久才释放。这个矛盾无解——除非你引入续期机制。第四不可重入。同一线程第二次加同一把锁会失败如果业务里有嵌套调用就会自己把自己锁死。我见过最惨的一次线上事故就是超时时间估短了锁 TTL 设了 10 秒某次数据库抖动业务执行了 15 秒锁在第 10 秒过期第二个线程进来两个线程同时更新同一个订单状态最终状态错乱人工排查了两天才定位到。6.3 Redisson 看门狗解决了什么又没解决什么Redisson 是把手写锁的坑基本填平的一个库。它的核心机制有两块看门狗续期和可重入计数。看门狗的原理是加锁成功后启动一个后台任务每隔锁超时时间 / 3的时间检查一次如果业务还在执行锁还被当前线程持有就把 TTL 重置回默认值30 秒。默认情况下每 10 秒续一次。这样业务执行多久锁就自动续多久彻底解决了超时时间估不准的问题。但看门狗有个前提Redisson 客户端必须还活着。如果整个进程崩了看门狗也跟着没了锁在 30 秒后自然过期这反而是我们想要的——崩掉的进程本来就不该继续持锁。可重入的实现是把 value 从字符串换成 Hash 结构key: lock:order:1001 field: 客户端UUID:线程ID value: 重入次数加锁时如果发现 field 已经存在就把 value 加 1释放时减 1减到 0 才真正删除 key。这样同一线程多次加锁不会死锁。RLock lock redissonClient.getLock(lock:order: orderId); try { // 最多等 3 秒拿到锁后由看门狗自动续期 boolean acquired lock.tryLock(3, TimeUnit.SECONDS); if (!acquired) { throw new BizException(操作太频繁请稍后再试); } // 业务逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }注意tryLock的写法只传等待时间不传租期leaseTime才会启看门狗。如果写成tryLock(3, 30, TimeUnit.SECONDS)就固定了 30 秒租期看门狗不生效又回到了超时时间估不准的老问题。这个坑非常多人踩。那 Redisson 没解决什么它解决不了 Redis 自身的高可用问题。主从架构下主节点加锁成功后还没同步到从节点就宕机了从节点升主锁就丢了。Redisson 提供了 RedLock 算法向多个独立的 Redis 实例加锁超过半数成功才算成功但 RedLock 的争议一直很大因为它的正确性依赖于各实例的时钟不发生大幅漂移网络分区时也可能出问题。我的态度比较务实如果你的业务不能容忍锁丢失那就别用 Redis 锁用支持一致性协议的组件比如基于 ZooKeeper 或者关系库的行锁。Redis 锁的定位是高性能、允许极低概率失效的场景比如防重复提交、任务调度去重。真正涉及钱的场景——扣库存、扣余额——最终的兜底应该是数据库层面的乐观锁版本号或者唯一索引Redis 锁只是第一道拦截网不是唯一防线。7. 把这几件事串起来我在实际项目里的分层防御单独看每个问题方案都很清楚。但它们在实际系统里是交织的需要有个整体的分层思路。我的分层大致是这样的第一层入口校验。参数格式、长度、取值范围在网关或者 Controller 层挡掉。这一层不涉及任何存储成本最低收益最大。第二层本地缓存。Caffeine 存最热的那一小部分数据TTL 3 到 5 秒扛住极高并发的只读请求。这一层挡掉的是击穿和一部分雪崩的冲击。第三层Redis 缓存。统一走工具类写入TTL 从枚举取热点 key 用逻辑过期普通 key 用随机抖动。这一层是主力。第四层分布式锁。只用在写路径和有竞争的读路径上粒度按业务资源划分用 Redisson 的tryLock加看门狗。锁的 key 命名规范统一为lock:业务:资源ID方便排查。第五层数据库兜底。乐观锁版本号、唯一索引、行锁这些是真正的最后防线。缓存可以错锁可以丢但数据库的约束不能破。第六层监控告警。缓存的命中率、Redis 的慢查询、锁的等待时间、数据库连接池使用率这四个指标我是必看的。命中率突然从 95% 掉到 70%说明有大量 key 失效或者被穿透锁等待时间突然拉长说明有热点资源被争抢连接池使用率超过 80%说明缓存层没扛住。有个小技巧分享一下我在每个缓存写入的地方都埋了一个来源标记记录这个 key 是正常回源写的还是预热任务写的。排查雪崩的时候看一眼标记就知道失效的 key 是定时任务批量过期造成的还是业务逻辑里 TTL 设错了。这个字段就存在 value 的包装体里几乎不增加成本但省了很多排查时间。还有一个反直觉的体会缓存问题的大部分根因不在 Redis在业务代码的写法。TTL 写死、删缓存漏写、锁粒度搞错、空值没处理这些都是代码层面的疏忽。所以与其花时间研究 Redis 的高级特性不如先把这几个基础模式在团队里统一成工具类用代码规范把它们固化下来。工具类里该做的判断都做了业务代码想写错都难——这比写多少文档都管用。
RELATED

相关推荐

Payload混淆技术:编码转换与协议特性实战解析

Payload混淆技术:编码转换与协议特性实战解析

1. 项目概述:Payload混淆技术的本质与价值 在网络安全攻防对抗的永恒博弈中,Payload作为攻击载荷的载体,其隐蔽性直接决定了攻击能否突破防御系统。传统基于特征码的检测机制催生了各种混淆技术,而今天我们要探讨的正是融合编码转…

📅 2026/10/27 12:55:16
OpenCV多角度多尺度模板匹配技术详解

OpenCV多角度多尺度模板匹配技术详解

1. 多角度多尺度模板匹配技术解析 在工业视觉检测、医学影像分析等领域,我们经常需要从复杂背景中定位特定目标物体。传统模板匹配方法存在两大痛点:一是目标可能出现任意旋转角度,二是目标尺寸可能发生缩放变化。这正是多角度多尺度模板匹配…

📅 2026/10/27 12:50:14
Windows本地Flink CDC实现MySQL实时同步实战指南

Windows本地Flink CDC实现MySQL实时同步实战指南

1. 为什么要在 Windows 上折腾 Flink 与 MySQL 同步 很多做数据处理的同行一听到“Flink 跑在 Windows 上”,第一反应是皱眉头——生产环境清一色 Linux,本地搞 Windows 图什么?我最初也是这个态度,直到实际带过几个项目之后才发现…

📅 2026/10/27 12:50:14
MORE NEWS

更多资讯

📰

ponytail插件实战:样式聚合与CSS变量自动化

1. 先搞清楚 ponytail 是什么玩意儿"ponytail"这名字我第一次看到的时候,还以为是哪个做发饰的品牌来搞前端工具了。后来真在项目里用上才发现,它和"扎头发"这个动作确实有异曲同工之妙——把散落在项目各个角落、看起来毫无头绪的样…

📰

Ponytail 聚合插件:将散落代码片段按序合并为单文件

"ponytail"这个词本身就很有画面感——一条马尾辫,把满脑袋散头发用皮筋扎成一根。我最早接触它,不是在发型教程里,而是在项目的发版流程里。当时维护一个中型前端系统,样式文件散落在十几个地方:全局变量、…

📰

ISTQB认证与实战能力:软件测试工程师的发展平衡之道

1. 软件测试行业的认证与实战之争 在软件测试行业摸爬滚打十几年,我见过太多人纠结于"考证还是练手"这个问题。ISTQB(国际软件测试资格认证委员会)的证书确实金光闪闪,但简历上写满项目经验的测试工程师同样吃香。这就像…

📰

2026年AI人才市场趋势与高薪技术岗位解析

1. 2026年AI人才市场的真实图景最近和几个头部科技公司的HRD聊了聊,发现一个有趣的现象:2023年还在裁员的大厂,现在都在疯狂抢AI算法人才。某自动驾驶公司给3年经验的LLM工程师开出了150万年薪股票,而这个人选手里同时握着5个offe…

📰

Flink流批一体作业全生命周期管控平台设计与实践

1. 这不是又一个“调度页面”,而是一套真正能管住Flink作业的生产级中枢“Flink流批一体作业管理平台”——光看标题,很多人第一反应是:哦,又一个带Web界面的Flink任务提交工具?点点按钮、填填JAR包、选个集群&#xf…

📰

2023年AI技术发展:大模型、多模态与生成式应用

1. AI技术发展现状全景扫描 2023年无疑是AI技术发展的分水岭之年。作为一名跟踪AI领域十年的技术观察者,我亲眼见证了从实验室研究到产业落地的完整演进过程。当前AI发展呈现出三个显著特征:大模型参数规模突破万亿级、多模态理解能力显著提升、生成式AI…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬