秒杀功能要加个分布式锁。我让 AI 写,它给了一段这个:
publicbooleantryLock(Stringkey,longtimeoutSeconds){Booleansuccess=redisTemplate.opsForValue().setIfAbsent(key,"1",timeoutSeconds,TimeUnit.SECONDS);returnBoolean.TRUE.equals(success);}publicvoidunlock(Stringkey){redisTemplate.delete(key);}写法看着挺标准。加锁、设过期、释放,一样不缺。
压测跑起来,库存超卖。
先看这套写法本身的问题
第一,value 是写死的"1"。
意味着所有客户端写进去的值都一样。那么释放锁的时候,谁都可以删。
场景:A 拿到锁,业务执行超时,锁自动过期了。B 拿到锁开始执行。A 这时候执行完了,走进unlock,把 B 的锁删了。C 立刻就能拿到锁进来。
三个线程同时在跑临界区。
第二,释放锁不判断持有者。
上面那个场景之所以能发生,就是因为delete(key)不检查这把锁是不是自己的。
第三,锁超时时间不一定够。
30 秒的锁,业务跑了 35 秒,锁先过期了。这时候另一台机器直接进来。
这个问题的难点在于:你没法准确预估业务耗时。今天 3 秒,明天数据库慢一下变 40 秒,你就中招了。
第四,也是最容易忽略的。上面那个写法里,加锁和设过期其实是原子的(setIfAbsent传了过期时间,Redis 内部是一条SET key value NX PX命令,没有原子性问题)。
但如果 AI 写成下面这样,就是真的非原子了:
// 危险:两步操作Booleansuccess=redisTemplate.opsForValue().setIfAbsent(key,"1");if(Boolean.TRUE.equals(success)){redisTemplate.expire(key,30,TimeUnit.SECONDS);// 单独一步}这两行之间如果服务挂了,那把锁永远不会过期。后面所有请求全部阻塞。
这个 bug 平时测不出来,只在你最不想出事的时候出现。
正确的写法长什么样
基础版本:加锁带过期、value 带唯一标识、释放用 Lua 比对
privatestaticfinalStringUNLOCK_LUA="if redis.call('get', KEYS[1]) == ARGV[1] then "+" return redis.call('del', KEYS[1]) "+"else return 0 end";publicbooleantryLock(Stringkey,StringrequestId,longmillis){Booleanok=redisTemplate.opsForValue().setIfAbsent(key,requestId,millis,TimeUnit.MILLISECONDS);returnBoolean.TRUE.equals(ok);}publicbooleanunlock(Stringkey,StringrequestId){Longresult=redisTemplate.execute(newDefaultRedisScript<>(UNLOCK_LUA,Long.class),Collections.singletonList(key),requestId);returnresult!=null&&result>0;}关键三点:
requestId用 UUID,每次加锁都是唯一值- 加锁一条命令,
SET NX PX,原子 - 释放用 Lua,先比对再删,比对和删除在 Redis 端是一条命令
再往上一步:自动续期
上面这个版本还是没解决“业务比锁时间长”的问题。要彻底解决,得有个看门狗,在锁快过期的时候自动续上。
Redisson 干的就是这个:
RLocklock=redissonClient.getLock("order:"+orderId);try{lock.lock(30,TimeUnit.SECONDS);// 业务逻辑}finally{lock.unlock();}它内部有个后台线程,每隔 10 秒(默认锁时间的 1/3)检查一次,业务还在跑就续期。
手写这个看门狗不是不行,但要处理的东西不少:续期线程的管理、客户端宕机的情况、续期失败怎么办。
我的建议是:能用 Redisson 就别手写。分布式锁这种基础设施,手写出来能跑通不难,跑对所有边界情况很难。
AI 为什么写不对这个
因为 AI 给你的不是“正确的并发代码”,是“看起来正确的代码”。
训练数据里分布式锁的文章,绝大多数是教学性质的。讲清楚“加锁、设过期、finally 释放”这三步就够了,没人会在教程里讲锁续期和持有者校验。
它给你的是一个能演示的版本,不是一个能上线的版本。
这两个之间的差距,就是超卖的库存数。
一句话
分布式锁这东西,最危险的状态不是“没加锁”,是“以为加了锁”。
没加锁你会老老实实做幂等、做库存校验。以为加了锁,你就把那些防线全撤了。
我把这段代码和修复版整理了一下,回头顺手查查你们项目里的锁,value 是不是写死的字符串。
你见过最离谱的分布式锁是怎么写的?说出来让我开开眼。