【黑马点评-3秒杀优惠券】四、分布式锁toy 解决一人一单

1 单体synchronized的缺陷

synchronized是JVM内部锁

两者都进入了锁的内部,这个synchronized锁形同虚设,这是由于synchronized是本地锁,只能提供线程级别的同步,每个JVM中都有一把synchronized锁,不能跨 JVM 进行上锁,当一个线程进入被 synchronized 关键字修饰的方法或代码块时,它会尝试获取对象的内置锁(也称为监视器锁)。如果该锁没有被其他线程占用,则当前线程获得锁,可以继续执行代码;否则,当前线程将进入阻塞状态,直到获取到锁为止。

而现在我们是创建了两个节点,也就意味着有两个JVM,所以synchronized会失效!

改进:分布式锁

  • 前面sychronized锁失效的原因是由于每一个JVM都有一个独立的锁监视器,用于监视当前JVM中的sychronized锁,
  • 所以无法保障多个集群下只有一个线程访问一个代码块。所以我们直接将使用一个分布锁,

2 分布式锁解决一人一单问题

1.工具package内 定义ILock锁接口

package com.hmdp.utils;

public interface ILock {
    boolean tryLock(long timeoutSec);

    void unLock();
}

2. 在SimpleRedisLock.java 实现 分布式锁的逻辑。

2.1 锁属性包括 锁name+ spring类StringRedisTemplate

2.2 tryLock方法

  1. 获取线程id
  2. redis的key为锁name
  3. redis的value为线程ID

2.3 unLock方法

让redis叫做锁name的key过期。

package com.hmdp.utils;

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

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {
    private final StringRedisTemplate stringRedisTemplate;
    private final String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        String id = Thread.currentThread().getId() + "";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock:" + name, id, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }

    @Override
    public void unLock() {
        stringRedisTemplate.delete("lock:" + name);
    }
}

3. 使用分布式锁

  1. 根据传入的voucherId,查询秒杀券
  2. 判断秒杀券是否合法
    • 与MySql交互,判断开始时间,结束时间和当前时间的对比
    • 判断Stock库存是否<1
  3. 从TheradLocal中取出用户usrId
  4. 调用分布式锁加锁,加锁,锁名为usrId, 并设置一个过期时间
  5. try前面的create订单操作,finnaly释放锁。
/**
 * 用Redis做分布式锁,前置判断合法与之前一样。
 *
 * @param voucherId
 * @return
 */
@Transactional
public Result seckillVoucherRedis(Long voucherId) {
    // 1. 查询秒杀券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    // 2. 判断秒杀券是否合法
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        // 秒杀还没开始
        return Result.fail("秒杀还没开始");
    }
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束");
    }
    if (voucher.getStock() < 1) {
        // 没券了
        return Result.fail("秒杀券已抢空");
    }
    // 3. 使用分布式锁创建订单
    Long userId = UserHolder.getUser().getId();
    SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
    boolean isLock = lock.tryLock(1200);
    // 获取锁失败
    if (!isLock) {
        return Result.fail("一人一单");
    }
    try {
        // 获取锁成功,创建代理对象,使用代理对象调用第三方事务方法
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(userId, voucherId);
    } finally {
        lock.unLock();
    }
}

注意点
try...finally...确保发生异常时锁能够释放,注意这给地方不要使用catch,A事务方法内部调用B事务方法,A事务方法不能够直接catch,否则会导致事务失效

posted @ 2025-04-15 17:00  kuki'  阅读(60)  评论(0)    收藏  举报