news 2026/10/1 10:47:39

手写Redis分布式锁:原理、常见坑与工程实践选型指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写Redis分布式锁:原理、常见坑与工程实践选型指南

“手写Redis分布式锁”这几个字,放在招聘JD里是常规操作,放在面试题里是必考题,放在我实际写的代码里,却是一段反复推翻重来的血泪史。我最早接触分布式锁还停留在SETNX一把梭的时代,后来被线上事故教育了几次,才发现这门手艺远不是一行命令那么简单。这篇内容不是教科书,也不是 Redisson 的源码分析,而是我自己踩过坑之后的整理,想聊清楚三件事:分布式锁到底在锁什么,手写过程中会踩到哪些细节坑,以及最后我如何在手写和现成框架之间做选择。如果你正准备自己实现一把 Redis 分布式锁,或者想在面试里把这个问题说透,这篇文章可以直接拿来当参考。

1. 为什么我要手写一把Redis分布式锁

1.1 分布式环境下锁到底锁的是什么

先从一个最基础的问题说起。单体应用时代,多个线程并发操作共享变量,我们直接上synchronized或者ReentrantLock,JVM 会帮我们保证同一时刻只有一个线程进入临界区。但一旦应用从单机变成多实例部署,每个实例都有自己的 JVM 锁,所谓“互斥”就只在一个进程内部成立。用户请求负载均衡到三台机器上,三台机器同时执行同一段扣库存代码,JVM 锁谁也拦不住谁,这就是分布式锁要解决的核心问题:跨进程、跨实例的互斥。

这里很多人有个误区,以为分布式锁锁的是某段代码。其实严格来说,锁的是“资源标识”。扣库存锁的是商品 ID,防重提交锁的是订单号,定时任务锁的是任务名。不同业务共用同一个锁 key 没有意义,真正要做的是让所有操作同一个资源的实例,在 Redis 里对同一个 key 达成互斥。记住这一点,后面看锁的设计思路会清晰很多。

1.2 哪些场景必须用分布式锁

我列一下实际项目里最常见的四类场景,基本覆盖了绝大部分用法。

第一类是库存扣减。秒杀、抢购场景下,库存是共享资源,减库存操作必须互斥。很多人会拿原子自增INCR说事,但真实业务往往不是简单减一,而是要先校验库存充足、再记录扣减流水、最后更新库存,这个完整流程必须一把锁包住。第二类是定时任务。比如每天晚上做数据对账,如果部署了三台实例,三个定时任务都会触发,不加锁就会重复处理。第三类是接口防重。用户双击提交订单、支付回调重复推送,这类场景如果下游接口没有幂等逻辑,就需要锁来保证同一笔业务只处理一次。第四类是缓存重建。热点 key 缓存过期后,大量请求同时打到数据库,用分布式锁保证只有一个请求去查数据库并重建缓存,其余请求等待或者直接返回旧值。

1.3 为什么偏偏是Redis,数据库锁不香吗

有人会问,数据库不是也能实现分布式锁吗?SELECT ... FOR UPDATE锁行、version字段做乐观锁,这些都是方案。数据库锁的问题在于成本和耦合。悲观锁依赖数据库连接和事务,一个锁操作要占住一条连接,锁等待超时会直接影响数据库连接池,在高并发下容易拖垮数据库本身。乐观锁虽然轻量,但冲突多的时候要反复重试,CPU 和磁盘开销都不小。而且数据库不是所有团队都愿意拿出来做协调器,毕竟它还承担着核心数据存储的职责。

Redis 的优势在于纯内存操作,单次加锁的延迟通常在毫秒级别,性能远高于数据库。再加上 Redis 本身的可用性架构,绝大多数业务场景下它都能把锁玩得很稳。更重要的是,SET key value NX EX seconds一条命令就能完成加锁,加上 Lua 脚本可以完成原子释放,整个实现逻辑很直观,这也是我后来坚持手写一版的原因——与其背 Redisson 的源码,不如先把它背后的原理捋明白。

2. 手写第一版:看似简单,其实处处是坑

2.1 天真版本:setnx加expire两步走

我第一次写分布式锁,代码大概长这样:

Boolean locked = jedis.setnx("lock:order:123", "1"); if (locked) { jedis.expire("lock:order:123", 30); // 业务逻辑 } else { // 获取锁失败 }

当时觉得逻辑没毛病:setnx 只有 key 不存在时才能设置成功,成功就加锁,接着设个过期时间防止死锁。直到后来我仔细想了一个问题:如果setnx执行成功,但expire还没执行,进程突然崩溃、或者 Redis 连接断开了,这个 key 就会永远留在 Redis 里,锁再也释放不了。这不是理论上的假设,Redis 客户端超时、JVM 发生OOM、发布过程中机器被 kill,都可能让两条命令中间断掉。

后来有人用setnx加锁之后,用一个独立线程去补expire,甚至用定时任务扫描“没有过期时间的锁 key”再补上过期时间。这些方案都能解决一部分问题,但都是补偿思路,治标不治本,而且代码复杂度会越写越高。

2.2 加锁必须原子化:SET key value NX EX

正确的姿势是用 Redis 从 2.6.12 版本开始支持的SET命令扩展参数,把加锁和设置过期时间合并成一个原子操作:

String lockKey = "order:lock:123456"; String requestId = UUID.randomUUID().toString(); String result = jedis.set(lockKey, requestId, "NX", "EX", 30); if ("OK".equals(result)) { // 加锁成功,执行业务 try { // doSomething } finally { // 释放锁 } }

这里有两个重点。第一,NX表示只有 key 不存在时才写入,EX 30表示自动过期时间 30 秒,两个参数配合设置,Redis 内部是同一个命令、同一个原子操作,从根本上消除了两步操作之间的窗口期。第二,value 绝对不能写死成"1",必须是一个全局唯一的requestId,这个标识会在释放锁时用来确认“这把锁是不是我的”。这一点我当时没当回事,结果后面踩了一个很大的误删锁的坑,下面细说。

2.3 释放锁要校验唯一标识,不能无条件del

如果释放锁只是简单执行jedis.del(lockKey),会有一个非常隐蔽的问题:线程 A 拿到锁,设置了 30 秒过期时间,业务却跑了 40 秒。30 秒时锁自动过期,线程 B 趁虚而入拿到了锁。线程 A 跑完业务后执行del,删掉的其实是线程 B 的锁。这时候线程 C 可能已经拿到锁了,B 的业务还在执行,互斥彻底失效。

解决办法是释放锁之前先比较 value 是否等于自己当初写入的requestId,相等才删除:

String value = jedis.get(lockKey); if (requestId.equals(value)) { jedis.del(lockKey); }

但这里又有一个新问题:get和del是两个独立命令,中间依然存在时间窗口。假如线程 A 刚get完确认 value 是自己的,线程 B 也get完发现锁还是 A 的,此时锁过期了,B 加锁成功,然后 A 再执行del,照样把 B 的锁删了。所以校验和删除也必须做成原子操作,这就是 Lua 脚本出场的原因。

2.4 为什么释放锁必须用Lua脚本

Redis 的 Lua 脚本功能允许我们把多条 Redis 命令打包成一个脚本交给服务端执行,整个脚本执行期间不会插入其他命令,天然具备原子性。释放锁的脚本写起来非常简单:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

用 Jedis 调用的伪代码:

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(requestId));

这段脚本的意思很简单:如果 key 里的 value 等于我手里的requestId,才执行删除,否则什么都不做。因为get和del在脚本内部执行,中间不会被任何其他客户端命令打断,误删锁的问题就算从根本上解决了。

注意:手写锁时,加锁用SET NX EX,释放锁用 Lua 脚本,这是一对不可拆分的组合。前半段不原子会死锁,后半段不原子会误删,两个坑踩一次就够了。

3. 给锁加上续期机制,避免业务没跑完锁就过期

3.1 过期时间设多长才算合理

写到这里,锁好像能用了,但另一个问题浮现了:过期时间到底该设多大?设 10 秒,业务稍微慢一点锁就自动释放了,另一个线程进来,对共享资源的互斥保护形同虚设。设 100 秒,如果持有锁的进程真的挂了,其他所有线程都得在原地干等 100 秒,这个等待时间用户根本受不了。本质上,过期时间是一个兜底保障,它应该大于业务正常执行时间,又要尽量小,以缩短故障恢复时间。

业务正常执行时间其实很难拍脑袋定。一次库存扣减要查库存、写流水、更新缓存,正常 50 毫秒,GC 一停顿可能就到 5 秒。更极端的情况是 JVM 发生长Full GC,整个应用冻结十几秒,如果过期时间短,锁就会在业务还没结束的时候自动释放。所以单纯把过期时间调大不是办法,更务实的做法是:设一个“相对合理但偏短”的默认值(比如 30 秒),然后给锁配一个续期机制,让持有锁的线程主动延长过期时间,一直续到业务跑完为止。

3.2 一个简单的看门狗怎么实现

续期机制在 Redisson 里叫 WatchDog,直译过来就是看门狗。原理不复杂:加锁成功后,启动一个守护线程,每隔一段时间(比如每 10 秒)去 Redis 里把锁的过期时间重置为初始值(比如重新设为 30 秒)。只要业务还在跑,看门狗就一直续期;业务结束后在finally里释放锁,同时把看门狗停掉。

一个最简的 Java 实现思路如下:

public class RedisLock { private static final long DEFAULT_EXPIRE = 30_000L; private static final long WATCHDOG_INTERVAL = 10_000L; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private volatile boolean running = false; public void lock(String lockKey, String requestId) { String result = jedis.set(lockKey, requestId, "NX", "EX", DEFAULT_EXPIRE / 1000); if ("OK".equals(result)) { running = true; scheduler.scheduleAtFixedRate(() -> renew(lockKey, requestId), WATCHDOG_INTERVAL, WATCHDOG_INTERVAL, TimeUnit.MILLISECONDS); } } private void renew(String lockKey, String requestId) { // 续期的时候也必须先校验锁还是不是自己的 String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end"; jedis.eval(lua, Collections.singletonList(lockKey), Arrays.asList(requestId, String.valueOf(DEFAULT_EXPIRE))); } public void unlock(String lockKey, String requestId) { running = false; String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(requestId)); } }

这段代码有几个细节值得注意。第一,续期的间隔不能太大,一般取过期时间的三分之一,过期 30 秒就每 10 秒续一次,这样即使某一次续期失败,锁也不会立刻过期。第二,看门狗线程必须在释放锁的时候停掉,否则业务结束了,守护线程还在不停续期,锁可能永远不释放。第三,renew也必须用 Lua 脚本校验持有者,如果在续期瞬间锁已经不是自己的了,不能把别人的锁错误续期。

3.3 进程卡顿、GC停顿、网络分区:续期也救不了的情况

看门狗能解决业务执行时间长的问题,但有几个场景是它也无能为力的。第一个是 JVM 长 GC。看门狗线程和应用业务线程跑在同一个 JVM 里,JVM 全局停顿的时候,业务线程停了,看门狗线程同样也停了,续期全部卡住,锁到期自动释放。业务恢复后,继续操作共享资源,而此时锁可能已经被别人拿走了,这是分布式锁和应用自身的宿命矛盾。

第二个是网络分区。客户端和 Redis 之间的网络断开,看门狗自然续不了期,锁自动释放。如果业务进程还在正常运行,它就会在“没有锁”的状态下继续操作共享资源。第三个是运维层面的问题,比如 Redis 主节点宕机触发主从切换,新的主节点可能没有从旧主节点同步到锁 key,锁瞬间就“消失”了。这些问题说明一个事实:任何基于 Redis 的分布式锁都不是绝对安全的,它只是把出问题的概率降到了一个业务可接受的范围。是否需要为了让极端场景更安全而引入 RedLock 或者 ZooKeeper 这类强一致方案,要结合业务的重要程度来判断,不能一刀切。

4. 更进一步:可重入锁与阻塞等待

4.1 可重入到底解决什么问题

到目前为止的锁是“不可重入”的:同一个线程如果已经持有了锁,再次调用加锁,会被当成一个陌生线程请求而失败。实际业务里这种情况不少见。比如一个服务方法内部调用了另一个同样需要加锁的方法,或者一个递归结构在每一层都尝试加锁,一旦走到第二次加锁,就必然失败。如果加锁失败的策略是阻塞等待,那就会变成自己等自己释放锁,直接死锁;如果策略是快速失败,则业务直接异常。

JDK 的ReentrantLock用同一个线程持有锁的数量来做可重入判断,这个思路完全可以平移过来。区别在于,单机锁的状态保存在 JVM 内存里,分布式锁的状态保存在 Redis 里,所以计数器也得放到 Redis 里。

4.2 用哈希结构设计可重入锁

可重入锁的经典做法是用 Redis 哈希结构来保存持有者信息和重入次数。key 是锁资源标识,field 是持有者的requestId,value 是重入计数。加锁时先判断 key 是否存在,不存在就写入field=requestId, value=1;存在则判断 field 是否为自己的requestId,是的话对 value 做INCR,否则加锁失败。释放锁时先DECR,计数减到 0 才删除 key,没减到 0 说明还有外层锁存在,不能删。

这里有个容易搞混的点:用哈希结构之后,原来那个SET NX EX的直接写法就不能用了,得借助一段更复杂的 Lua 脚本来实现。因为加锁过程包含“判断 key 是否存在”和“写入 field/value”两步,只有 Lua 脚本能保证它们是原子的。写出来的脚本大概长这样:

-- 加锁 if redis.call("exists", KEYS[1]) == 0 then redis.call("hset", KEYS[1], ARGV[1], 1) redis.call("pexpire", KEYS[1], ARGV[2]) return 1 end if redis.call("hexists", KEYS[1], ARGV[1]) == 1 then redis.call("hincrby", KEYS[1], ARGV[1], 1) redis.call("pexpire", KEYS[1], ARGV[2]) return 1 end return 0
-- 释放锁 if redis.call("hexists", KEYS[1], ARGV[1]) == 0 then return 0 end local count = redis.call("hincrby", KEYS[1], ARGV[1], -1) if count > 0 then redis.call("pexpire", KEYS[1], ARGV[2]) return count end redis.call("del", KEYS[1]) return 0

我在实际项目里其实很少用到可重入,因为业务上会刻意规避同一个线程重复加锁的设计。但面试中这块几乎是必考点,而且理解了哈希结构的计数逻辑,对理解 Redisson 的底层也有帮助。

4.3 拿不到锁的时候:自旋等待还是订阅通知

手写锁到现在,加锁失败的线程怎么处理还没讨论清楚。最简单的策略是快速失败,拿不到锁直接返回失败,由上层业务决定是重试还是换一种处理方式。但很多业务场景需要“排队等待”,比如扣库存接口,用户等了 100 毫秒拿不到锁就直接报错,体验很差。这时候就要引入阻塞等待机制。

阻塞等待的粗暴实现是自旋:用一个循环不断尝试加锁,每次失败后Thread.sleep(50)再试。自旋的问题在于浪费 CPU。尤其当几十个线程同时等同一把锁时,每个线程都在高频度地请求 Redis,对 Redis 的访问压力成倍增加,即使它们大部分请求都是徒劳的。更关键的是,锁一旦释放,所有等待线程在下一个时间点同时冲上去抢锁,形成“惊群效应”,瞬间加锁压力飙升。

更好的做法是让等待线程先“睡”,等锁释放时收到通知再醒来抢锁。Redis 的 keyspace 通知机制可以做到这一点:给 Redis 开启notify-keyspace-events参数,让它监听某个 key 的删除事件,锁释放时向频道发送一条消息,等待锁的线程订阅这个频道,收到消息后再尝试加锁。这种模型的CPU开销远小于自旋,线程间也有天然的错峰效果。不过手写这套发布订阅逻辑并不轻松,要考虑消息丢失、频道命名、连接管理等问题,这也是很多团队最终选择 Redisson 的原因——Redisson 把发布订阅、信号量这些机制都封装好了。

4.4 自旋锁的坑:等待超时和CPU飙升

如果在代码里直接写一个while (true)去尝试加锁,很快你就会发现四个问题。第一,没有等待超时限制,锁永远不被释放时,线程会无限循环,连接池被耗尽,整个应用线程阻塞。第二,Thread.sleep(50)看似无害,但大量线程并发自旋时,Redis 客户端的请求量会暴涨,有时你没把业务压力打垮,先被自己的自旋把 Redis 打到了高延迟。第三,锁刚释放的一瞬间,多个线程同时加锁,只有一个能成功,其他线程白忙一轮,CPU 空转非常严重。第四,自旋间隔设置不合理,太短会加剧压力,太长则加锁等待时间不可控。

我给自旋加了两个约束:一个是用long endTime = System.currentTimeMillis() + acquireTimeout控制整个获取锁的时限,超时就放弃,不再无限等下去;另一个是退避时间从一个随机区间取值而不是固定值,比如 50 到 150 毫秒之间随机,让各个线程错开尝试时间,缓解惊群。这套办法在并发量不高的时候够用,但并发一上去,还是得靠发布订阅模型才能把问题解决好。

5. 常见问题排查实录

手写锁上线之后的排查和维护,才是这门手艺真正考验人的地方。我把实际过程中遇到过的、以及给团队排查过的问题整理成一张速查表,再挑几个典型展开说一下。

现象根因解决方案
锁被误删,多个线程同时进入临界区释放锁没有校验持有者唯一标识释放锁走 Lua 脚本,先比较 value 再删除
业务执行完之前锁过期,并发请求穿墙过期时间设置过短且没有续期机制增加看门狗线程自动续期
主从切换后锁丢失,互斥失效Redis 主从复制是异步的,新主节点可能没有锁 key对一致性要求高的场景考虑 RedLock 或改用 ZooKeeper
同一线程嵌套加锁直接死锁普通锁不可重入用哈希结构记录线程持有次数
锁等待导致接口响应超时自旋等待时间过长,或锁未释放设置获取锁超时时间,完善释放锁的 finally
Redis 连接池被占满自旋循环中高频创建连接或等待线程过多复用连接池,改用发布订阅模型

5.1 误删别人刚获取的锁

这个问题我在 2.3 里讲过原理,这里说说排查过程。当时线上监控发现偶尔有两条请求同时对同一个订单做写操作,加了锁还互斥失效。排查第一步是看 Redis 里锁 key 的 value 变化。我们当时用MONITOR命令跟踪了某个 key 的写入和删除记录,发现删除操作来自一个已经超时结束的请求。到这里原因就清楚了:我们释放锁的代码写的是无条件del,没有校验 value。修复方式就是改成 Lua 脚本,这也让我彻底理解了为什么锁的 value 必须唯一。这里提醒一句:线上慎用MONITOR,它会显著增加 Redis 的负载,量大的时候可能把 Redis 压到告警。

5.2 业务执行时间超过锁过期时间

这个问题有两个典型表现。一种是锁自动过期,第二个线程进来了,第一个线程日志里却看不出任何异常,只在最终数据上发现被写入了两次。另一种是看门狗线程写的续期逻辑有 bug,把过期时间设置成了固定值而不是“当前时间+过期时长”,导致锁的实际 TTL 越续越小,最终很快过期。我排查这类问题的方法比较简单粗暴:在获取锁和释放锁的地方打日志,记录拿到锁的时间、释放锁的时间,以及看门狗每次续期的 Redis 时间,把这些日志对齐到按请求维度去查,就能看出锁的生命周期到底发生了什么。一般业务流程稍微复杂一点的系统,锁自带续期是必须的,否则任何一次慢 SQL 或外部调用超时都会成为定时炸弹。

5.3 主从切换对锁的影响

很多团队用 Redis 主从架构保证高可用,但主从之间的复制是异步的。客户端在主节点上写入锁 key,如果数据还没复制到从节点,主节点就宕机了,哨兵会发现并从节点提升为主节点,这时候新的主节点上根本没有锁 key。所有客户端都能成功加锁,互斥失效。这个问题的本质是 Redis 的复制机制不保证强一致,任何基于普通 Redis 的分布式锁都有这个盲区。

要解决它,业界最知名的方案是 RedLock 算法:同时向多个独立的 Redis 节点加锁,超过一半成功才算加锁成功。但 RedLock 也有争议,连 Redis 官方都曾经承认它在极端时钟漂移下并不绝对安全。我在实际业务里的态度是:先去问这个锁守护的数据到底重要到什么程度。如果只是防重复提交、限流、保护缓存重建,普通 Redis 锁加看门狗就够了;如果涉及资金类强一致操作,建议直接用 ZooKeeper 或者 etcd 这类本身就是为一致性设计的协调服务,而不是在 Redis 上硬凑。

5.4 嵌套调用导致自己等自己

有一个印象很深的排查案例:一个方法加了分布式锁,方法内部又调用了另一个同样加锁的方法。因为当时用的是不可重入锁,第二次加锁直接失败,代码又没有做失败处理,结果业务抛异常了。排查时翻日志只看到“lock acquire failed”,完全没有方向。最后逐层打了断点才发现问题出在内部调用上。

这类问题在代码设计上最好直接规避。优先避免在同一个线程里重复拿同一把锁,比如把需要锁保护的公共逻辑拆成独立的内部方法,外层统一加锁。如果确实有可重入需求,就按 4.2 里的哈希结构来做,不要心存侥幸。

5.5 获取锁超时和接口响应超时

还有一类常见问题是接口整体的响应时间变长。打开日志发现很多请求卡在“获取锁”这一步。这种情况通常是持锁线程执行时间过长,或者持锁线程异常退出,看门狗没有正常停止却把锁续期了。排查时先看锁的 TTL 是否被异常拉长,再看业务线程是否长时间占用。我建议所有手写锁都提供两个基础参数:获取锁超时时间和锁自动过期时间,并且默认值要合理。获取锁超时设 3 到 5 秒,过期时间设 30 秒,大部分业务是够用的。

6. 手写锁 vs Redisson,以及我的最终建议

6.1 Redisson 比我手写多做了哪些事情

我不止一次想过,既然 Redisson 功能全面,为什么还要自己写?答案是:为了搞懂原理,但搞懂之后不代表生产环境一定要手写。Redisson 的锁默认提供 30 秒的看门狗续期,每 10 秒续一次,锁释放后自动关闭续期线程;它内部用发布订阅模型实现等待唤醒,而不是靠 CPU 自旋;它还实现了可重入锁、公平锁等多种模式。我手写的这些方案,Redisson 基本都内置了,而且工程化程度更高,容错处理更完善。

比如 Redisson 的解锁逻辑里考虑到了“解锁时如果看门狗还在续期,要先取消续期再解锁”这种细节,手写代码时很容易漏掉。再比如 Redisson 对 Redis 连接断开的处理、对命令超时的重试策略,都是一整套成熟的设计。单纯从代码数量上看,Redisson 的锁模块就有上万行,这是经过大量生产环境检验的代码,靠手写很难在短期内达到同等质量。

6.2 什么时候手写,什么时候必须用框架

我现在的决策原则是分层处理。小项目、内部系统、并发量不高、锁守护的逻辑很简单,手写一把 30 行的锁完全够用,毕竟少引一个依赖,维护成本更低。中大型项目,特别是对锁的可靠性有要求、业务链路又长又复杂的场景,直接用 Redisson 这类成熟框架,不要自己造轮子。我在团队里一直强调:手写锁最大的价值不是替代 Redisson,而是让你在排查线上问题、评估框架源码时有底气。你见过SET NX EX,就不会把 Redisson 当黑盒;你写过 Lua 释放脚本,遇到锁误删的问题就能第一时间想到 value 校验。

6.3 我最后留下的几个习惯

聊到收尾,分享几个我坚持了很久的实操习惯。

第一,锁 key 的命名一定要规范,我常用的格式是业务域:lock:资源ID,比如order:lock:123456。命名规范不仅方便排查问题,也方便在 Redis 里通过SCAN去统计锁的使用情况。第二,requestId用UUID.randomUUID().toString()或者“IP+线程ID+时间戳”的组合,保证全局唯一,尽量不要复用。第三,释放锁必须放在finally块里,任何异常路径都不能绕过。第四,锁的过期时间、续期间隔、获取锁超时时间,三个参数都要显式配置,不要写死魔法数。

还有一个我每次写锁都会想起的教训:锁不是一个孤立的技术点,它和你对业务并发模型的理解深度绑定。你越清楚临界区里到底在保护什么,就越明白过期时间、续期、可重入这些机制为什么存在。如果只是照着网上的代码背下来,线上迟早会用一个故障帮你复习一遍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:47:22

设备追溯数据为何失效?时间同步与温湿度传感器校准是关键

设备追溯数据看着全,关键时候却调不出来,这种问题我在好几个元器件厂都撞上过。有一回去一家做电源模块的厂子做系统回访,可靠性工程师翻出三个月前某批次产品的追溯档案,发现AOI记录显示某块板子过完测试的时间,居然比…

作者头像 李华
网站建设 2026/10/1 10:46:46

Jenkins插件安装教程:依赖管理、离线部署与Java Web自动部署

1. 插件机制的底层逻辑:为什么 Jenkins 离开插件寸步难行刚接触 Jenkins 的人容易有一个错觉:装完 war 包、打开 8080 端口,这工具就能自动部署了。实际用下来你会发现,裸装的 Jenkins 除了能跑一个最简单的自由风格任务、执行几条…

作者头像 李华
网站建设 2026/10/1 10:46:29

SpringBoot2+Vue3+MySQL8.0疫情防控管理系统开发实战

接手过不少学生项目和内部管理系统,看到“Java Web 疫情防控管理系统”这个标题,第一反应是亲切——SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0,标准得不能再标准的技术栈组合。这类项目在毕设、课设、技能培训中出现的频率非常高&#xff0…

作者头像 李华
网站建设 2026/10/1 10:45:56

C盘爆红不用愁:垃圾软件清理与卸载工具全攻略

C盘又爆红了吧?屏幕底部的磁盘条红得像报警,点哪里都卡一下。我先说个结论:绝大多数C盘“撑爆”的问题,根本不需要重装系统,也不是什么玄学,只要你把看不见的系统临时垃圾清理掉、把那些“请神容易送神难”…

作者头像 李华
网站建设 2026/10/1 10:45:26

交通标志识别毕设实战:YOLOv5数据构建与部署全流程

简介:本资源是一套完整的YOLOv5交通标志识别检测实战项目,专为计算机视觉初学者与本科毕业设计、课程设计、期末大作业学生打造,解决目标检测入门难、数据集匮乏、模型调优无从下手等实际问题。压缩包共266个文件,含53个Python训练…

作者头像 李华
网站建设 2026/10/1 10:44:17

基于SpringBoot的中西诊所管理系统:从数据库到并发控制的毕设全攻略

又到了一年一度挣扎毕业设计的季节。前阵子群里又有人问"什么题目好做",我给出的答案一直很明确:找个业务边界清晰、需求味很足、技术栈主流的方向下手。中西诊所管理系统就是很典型的这种题目,它基于SpringBoot做后端,…

作者头像 李华