两天吃透秒杀核心:事务失效、JDK 代理、悲观 / 乐观锁、分布式锁原子性

张开发
2026/4/3 12:40:32 15 分钟阅读
两天吃透秒杀核心:事务失效、JDK 代理、悲观 / 乐观锁、分布式锁原子性
前言最近两天集中死磕秒杀高并发核心难点从 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 02. Java 中加载并执行脚本private static final DefaultRedisScriptLong 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 ServiceImplVoucherOrderMapper, 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 DefaultRedisScriptLong 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 判断 stock0防超卖分布式锁为什么需要 LuaJava 代码get 判断 delete非原子会出现误删锁Lua 脚本原子执行保证安全。锁设计核心key 相同实现互斥value 存线程 ID区分锁持有者避免误删

更多文章