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

前言:最近两天集中死磕秒杀高并发核心难点,从 Spring 事务失效、AOP 代理陷阱,到悲观锁 + 乐观锁联用保证秒杀安全,再到 Redis 分布式锁与原子性问题,全程踩坑 + 深挖原理 + 落地代码,这一篇把企业级秒杀逻辑彻底讲透

目录

  1. 秒杀场景的四大经典坑
  2. Spring AOP 代理与事务失效真相
  3. 秒杀双锁架构:悲观锁防重 + 乐观锁防超卖
  4. 分布式锁:从单机锁到 Redis 锁演进
  5. 分布式锁致命 bug:误删别人锁
  6. Lua 脚本保证原子性(终极解决方案)
  7. 完整可运行代码汇总
  8. 核心知识点总结(面试必背)

一、秒杀场景的四大经典坑

做秒杀业务,几乎人人都会踩这些坑:

  • @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 致命缺陷

synchronizedJVM 级别单机锁

  • 单服务器没问题
  • 集群多实例部署 → 锁直接失效
  • 必须使用 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:误删别人锁

经典事故场景

  1. 线程 A 加锁,执行业务阻塞,锁超时过期
  2. Redis 自动删除 key
  3. 线程 B 使用同名 key 加锁成功
  4. 线程 A 恢复执行,直接 delete(key)
  5. 线程 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,是传入的第一个 key
  • ARGV[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() + ""
        );
    }
}

八、核心知识点总结(面试必背)

  1. 事务失效原因类内部 this.方法() 调用不走代理,必须用 AopContext.currentProxy() 获取代理对象。

  2. JDK 代理特点基于接口实现,只能强转为接口类型,方法必须定义在接口中。

  3. 秒杀双锁设计

    • 悲观锁(synchronized 锁用户 ID):防重复下单
    • 乐观锁(数据库 CAS 判断 stock>0):防超卖
  4. 分布式锁为什么需要 Lua  Java 代码 get + 判断 + delete 非原子,会出现误删锁;Lua 脚本原子执行,保证安全。

  5. 锁设计核心

    • key 相同:实现互斥
    • value 存线程 ID:区分锁持有者,避免误删
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员萤火

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值