前言:最近两天集中死磕秒杀高并发核心难点,从 Spring 事务失效、AOP 代理陷阱,到悲观锁 + 乐观锁联用保证秒杀安全,再到 Redis 分布式锁与原子性问题,全程踩坑 + 深挖原理 + 落地代码,这一篇把企业级秒杀逻辑彻底讲透
目录
- 秒杀场景的四大经典坑
- Spring AOP 代理与事务失效真相
- 秒杀双锁架构:悲观锁防重 + 乐观锁防超卖
- 分布式锁:从单机锁到 Redis 锁演进
- 分布式锁致命 bug:误删别人锁
- Lua 脚本保证原子性(终极解决方案)
- 完整可运行代码汇总
- 核心知识点总结(面试必背)
一、秒杀场景的四大经典坑
做秒杀业务,几乎人人都会踩这些坑:
@Transactional注解加了,事务却莫名失效- 高并发下库存超卖、用户重复下单
synchronized锁在集群环境直接失效- Redis 分布式锁用了,却出现误删别人锁的诡异问题
本文从原理到代码,一次性全部搞定。
二、Spring AOP 代理与事务失效真相
1. JDK 动态代理只认接口,不认实现类
Spring 默认使用 JDK 动态代理,生成的代理对象:
- 实现业务接口,不是继承实现类
- 和实现类是 “兄弟关系”,只共享接口
- 只能强转为接口类型,不能强转为实现类
// 正确 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); // 错误:类型转换异常 VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();2. 为什么
this.方法()会导致事务失效?@Transactional public Result creatVocherOrder(Long voucherId) { ... } public void a() { // this 是原始对象,不是代理对象 → 事务直接失效 this.creatVocherOrder(voucherId); } this:当前真实对象,没有被 Spring 增强- 代理对象:才包含事务、AOP 增强逻辑结论:内部方法调用不走代理 → 事务失效
3. 正确写法:获取代理对象调用
synchronized (userId.toString().intern()) {
// 获取当前代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
// 走代理 → 事务正常生效
return proxy.creatVocherOrder(voucherId);
}
4. 关键细节
被代理调用的方法,必须定义在接口里,否则编译报错。
三、秒杀双锁架构:悲观锁 + 乐观锁
你的代码是企业级标准秒杀双锁模型,完美解决两大问题:
1. 悲观锁(synchronized):防用户重复下单
锁对象是 userId.toString().intern()
- 锁的是用户,不是库存
- 同一个用户串行执行,不同用户互不影响
- 锁必须加在事务外面,保证:先加锁 → 执行业务 → 提交事务 → 释放锁
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.creatVocherOrder(voucherId);
}
2. 乐观锁(数据库 CAS):防库存超卖
利用 SQL 原子判断 stock > 0,高并发安全且性能高。
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0) // 核心:库存大于0才扣减
.update();
3. 双锁总结
- 悲观锁锁用户:防止同一用户并发下多次下单
- 乐观锁锁库存:防止高并发下超卖
- 事务保证:扣库存 + 生成订单 原子性
四、分布式锁:从单机锁到 Redis 锁
1. synchronized 致命缺陷
synchronized 是 JVM 级别单机锁:
- 单服务器没问题
- 集群多实例部署 → 锁直接失效
- 必须使用 Redis 分布式锁
2. 手写 Redis 分布式锁结构
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
// 构造方法:new 对象时传入锁名称和 Redis 工具
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
// 加锁
@Override
public boolean tryLock(long timeoutSec) {
String key = "lock:" + name;
String value = Thread.currentThread().getId() + "";
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
// 解锁(有问题版本)
@Override
public void unlock() {
String key = "lock:" + name;
Long threadId = Thread.currentThread().getId();
String value = stringRedisTemplate.opsForValue().get(key);
if (value != null && threadId == Long.parseLong(value)) {
stringRedisTemplate.delete(key);
}
}
}
五、分布式锁致命 Bug:误删别人锁
经典事故场景
- 线程 A 加锁,执行业务阻塞,锁超时过期
- Redis 自动删除 key
- 线程 B 使用同名 key 加锁成功
- 线程 A 恢复执行,直接
delete(key) - 线程 A 误删线程 B 的锁
根本原因
- 锁 key 相同,只是 value(线程 ID)不同
- Java 代码中
get → 判断 → delete是三步非原子操作 - 判断与删除之间存在时间差,导致安全漏洞
六、Lua 脚本保证原子性(终极方案)
Redis 执行 Lua 脚本是单线程原子性,可以把判断 + 删除合为一步。
1. unlock.lua 脚本
-- 比较线程标识与锁中的标识是否相同
if(redis.call('get',KEYS[1]) == ARGV[1]) then
-- 相同则删除锁
return redis.call('del',KEYS[1])
end
-- 不相同则返回0
return 0
2. Java 中加载并执行脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
// 最终安全解锁
@Override
public void unlock() {
String key = "lock:" + name;
String threadId = Thread.currentThread().getId() + "";
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(key),
threadId
);
}
3. 关键点说明
KEYS[1]:不是固定数字 1,是传入的第一个 keyARGV[1]:传入当前线程 ID,用于判断锁归属- 脚本原子执行,彻底避免误删锁
七、完整可运行代码汇总
1. 秒杀核心业务类
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已结束");
}
// 4.判断库存
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
// 锁用户,保证一人一单
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.creatVocherOrder(voucherId);
}
}
@Transactional
@Override
public Result creatVocherOrder(Long voucherId) {
// 一人一单校验
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经购买过一次!");
}
// 扣库存(乐观锁)
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!success) {
return Result.fail("库存不足!");
}
// 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}
2. 分布式锁接口
public interface ILock {
boolean tryLock(long timeoutSec);
void unlock();
}
3. Redis 分布式锁实现
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public boolean tryLock(long timeoutSec) {
String threadId = Thread.currentThread().getId() + "";
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
Thread.currentThread().getId() + ""
);
}
}
八、核心知识点总结(面试必背)
-
事务失效原因类内部
this.方法()调用不走代理,必须用AopContext.currentProxy()获取代理对象。 -
JDK 代理特点基于接口实现,只能强转为接口类型,方法必须定义在接口中。
-
秒杀双锁设计
- 悲观锁(synchronized 锁用户 ID):防重复下单
- 乐观锁(数据库 CAS 判断 stock>0):防超卖
-
分布式锁为什么需要 Lua Java 代码
get + 判断 + delete非原子,会出现误删锁;Lua 脚本原子执行,保证安全。 -
锁设计核心
- key 相同:实现互斥
- value 存线程 ID:区分锁持有者,避免误删

381

被折叠的 条评论
为什么被折叠?



