实际工程问题:AI生成Redis分布式锁的原子性缺陷导致库存超卖

这是AI辅助开发中「表面功能可用,边界条件全漏」的典型代表,电商、库存、支付等强一致性场景极易触发线上资损。

  1. 场景背景

某电商系统开发「商品扣减库存」接口,开发者借助AI快速生成Redis分布式锁代码,目标是防止并发下单导致超卖。AI给出的实现基于SETNX + EXPIRE组合,单测串行/低并发场景均表现正常,大促活动上线后出现库存超卖,且部分商品库存被扣为负数。

  1. 问题现象

• 大促高峰时段部分热门商品出现超卖,实际下单量超过商品库存总量。

• 监控显示部分锁key长期未过期,占用Redis内存;部分请求拿到已过期的锁,出现并发执行。

• 异常日志中偶发「删锁失败」「锁不存在」报错,但业务逻辑仍正常执行。

  1. 根因分析

AI生成的代码仅实现了「加锁-执行业务-解锁」的基础流程,完全遗漏了分布式锁的核心工程约束,存在4个致命缺陷:

  1. 加锁非原子操作

AI代码分两步执行:先setnx占锁,成功后再expire设置过期时间。若占锁成功后服务宕机/进程崩溃,过期时间未设置,该锁会永久存在,后续所有请求都无法拿到锁,形成死锁。

  1. 解锁不校验持有者

解锁时直接执行del key,未校验锁的归属。若线程A业务执行超时,锁自动过期释放,线程B成功加锁;此时线程A执行完毕执行解锁,会直接删掉线程B持有的锁,导致线程C又能加锁成功,锁彻底失效。

  1. 锁过期时间无续期机制

AI写死过期时间(如30s),但业务逻辑(库存校验、订单创建、扣减库存)在数据库慢查询、GC卡顿等场景下执行时长可能超过30s,锁自动过期后并发请求直接穿透,引发超卖。

  1. 解锁非原子操作

解锁逻辑是「先get判断值是否相等,再del」,两步操作非原子。高并发下判断相等后、执行删除前,锁可能刚好过期并被其他线程重新加锁,同样会误删他人的锁。

  1. 解决方案

(1)紧急止血

线上临时切换为数据库乐观锁(版本号机制)兜底,先止住超卖,再修复分布式锁实现。

(2)根本修复:原子性分布式锁标准实现

基于Redis单命令原子性 + Lua脚本原子解锁,修复所有边界缺陷,核心代码如下:

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.stereotype.Component;

import java.util.Collections;

import java.util.UUID;

import java.util.concurrent.TimeUnit;

@Component

public class RedisDistributedLock {

private final StringRedisTemplate stringRedisTemplate;

// 解锁Lua脚本:原子性判断持有者+删除

private static final String UNLOCK_SCRIPT =

"if redis.call('get', KEYS1) == ARGV1 then " +

" return redis.call('del', KEYS1) " +

"else " +

" return 0 " +

"end";

private final DefaultRedisScript<Long> unlockScript;

public RedisDistributedLock(StringRedisTemplate stringRedisTemplate) {

this.stringRedisTemplate = stringRedisTemplate;

this.unlockScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);

}

/**

* 原子加锁:SET key value NX PX expireTime

* NX=不存在才设置,PX=毫秒级过期时间,单命令保证原子性

* @param lockKey 锁key

* @param expireTime 过期时间

* @param timeUnit 时间单位

* @return 锁唯一标识(解锁凭证),加锁失败返回null

*/

public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {

// 生成唯一锁标识,区分不同线程/请求的锁

String lockValue = UUID.randomUUID().toString().replace("-", "");

long expireMs = timeUnit.toMillis(expireTime);

Boolean success = stringRedisTemplate.opsForValue()

.setIfAbsent(lockKey, lockValue, expireMs, TimeUnit.MILLISECONDS);

return Boolean.TRUE.equals(success) ? lockValue : null;

}

/**

* 原子解锁:Lua脚本保证「判断+删除」原子执行

* @param lockKey 锁key

* @param lockValue 锁唯一标识

* @return 是否解锁成功

*/

public boolean unlock(String lockKey, String lockValue) {

if (lockValue == null) return false;

Long result = stringRedisTemplate.execute(

unlockScript,

Collections.singletonList(lockKey),

lockValue

);

return Long.valueOf(1).equals(result);

}

}

业务使用示例(配合try-finally保证解锁必执行):

public void deductStock(Long productId, int quantity) {

String lockKey = "stock:lock:" + productId;

String lockValue = redisLock.tryLock(lockKey, 30, TimeUnit.SECONDS);

if (lockValue == null) {

throw new BusinessException("系统繁忙,请稍后再试");

}

try {

// 校验库存、扣减库存、生成订单等核心业务

stockService.doDeduct(productId, quantity);

} finally {

// 无论业务成功/异常,必须解锁

redisLock.unlock(lockKey, lockValue);

}

}

(3)生产级进阶方案:引入Redisson

对于长耗时业务、高可用要求高的场景,直接使用Redisson分布式锁,自带看门狗自动续期、可重入、联锁、红锁等能力,避免手动实现的遗漏:

• 锁默认30s过期,业务未执行完时,看门狗线程每10s自动续期为30s;

• 服务宕机后看门狗停止,锁自动过期,不会死锁;

• 完全兼容上述原子加锁/解锁逻辑,开箱即用。

(4)配套优化

• 锁粒度细化:按商品ID加锁,而非全商品统一锁,降低锁竞争;

• 增加锁等待重试机制:加锁失败后自旋重试指定次数,提升用户体验;

• 数据库兜底:库存字段增加unsigned约束 + 乐观锁版本号,作为最后一道防线。

  1. AI辅助开发避坑总结

  2. 并发/锁类代码必须人工核查原子性:AI生成的分布式锁、缓存更新等代码,重点检查「操作是否原子」「异常分支是否兜底」「并发场景下的边界行为」,禁止直接复制上线。

  3. 警惕「分步实现」的陷阱:Redis、数据库等中间件操作,凡是需要「先判断再执行」的逻辑,必须用原子命令/Lua脚本/事务保证原子性,两步操作必然存在并发漏洞。

  4. 资损场景强制双校验:涉及库存、金额、优惠券等资损场景,分布式锁之外必须增加数据库层面的兜底校验,不能把一致性完全寄托在缓存锁上。

相关推荐
软件技术NINI1 小时前
基于SpringBoot和Vue的新能源汽车销售推荐系统
java·vue.js·spring boot·后端·汽车·毕业设计·毕设
Flynn Liang1 小时前
LangChain4j 生产级工程骨架:12 个模块 + Docker 一键启动
java·spring boot·langchain·rag·langchain4j
噢,我明白了3 小时前
JAVA中的一些基础应用知识点
java·开发语言
SL_staff3 小时前
私有化低代码不是妥协,而是企业级数字化的自主可控主航道
java
装不满的克莱因瓶3 小时前
【SpringAI Alibaba】ReactAgent结构化输出OutputType与OutputSchema实战
java·人工智能·ai·springai·springaialibaba
孔令飞3 小时前
把 Agent 搬进飞书:飞书 Agent 机器人演示
云原生·go·ai编程
时间的拾荒人4 小时前
Redis 入门详解:核心特性、重要文件与全局命令实战
开发语言·redis·面试
ZzT4 小时前
Anthropic 选模型指南拆解:先用 low 跑、失败的再用 high 重跑,通过率 97% 高于全程 high
ai编程·claude
秋天的怀念_David4 小时前
算法一千零一夜 · 第二十八夜 注册
java·程序人生·算法