小言_互联网的博客

聊聊优惠卷秒杀的下单功能

331人阅读  评论(0)

前言

案例来自黑马程序员视频:https://www.bilibili.com/video/BV1cr4y1671t/?spm_id_from=333.999.0.0

案例分析

  • 下单时需要判断两点:
    • 秒杀是否开始或者结束,如果尚未开始或者已经结束则无法下单;
    • 库存是否充足,不足则无法下单
  • 具体流程就是:最开始,我们会提交优惠卷信息,然后去查询优惠卷信息,判断秒杀是否开始,如果否,则结束流程;然后判断秒杀是否结束,如果是,则结束流程;然后判断库存是否充足,如果不足,则结束流程;否则的话,就执行扣减库存;如果库存失败,则结束流程;然后创建订单,返回订单;

但是,在多线程环境中,会出现一个典型问题:超卖问题。针对这个问题,我们的常见解决方案就是加锁

  • 悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如:synchronized、Lock都属于悲观锁;
  • 乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据;如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常;

关键代码:扣减库存的时候判断stock>0

boolean update = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();      // 乐观锁 where id = ? and stock > 0

重复下单问题

解决方案:使用分布式锁

  • 什么是分布式锁
    分布式锁:满足分布式锁或集群模式下多进程可见并且互斥的锁。它具有以下几个特点:①高可用;②安全性;③多进程可见;④互斥;⑤高性能…

  • 基于redis实现的分布式锁

  • 代码案例

业务代码

// 分布式锁
// 创建锁对象
 SimpleRedisLock redisLock = new SimpleRedisLock("orderId:" + userId, stringRedisTemplate);
 // 获取锁
 boolean isLock = redisLock.tryLock(1200);
 if (!isLock) {
   
     // 获取锁失败,返回错误或重试
     return Result.fail("不允许重复下单");
 }
 try {
   
     IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
     return proxy.createVoucherOrder(voucherId);
 } finally {
   
     // 释放锁
     redisLock.unlock();
 }

 

锁对象

public class SimpleRedisLock implements ILock {
   

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID() + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
   
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    }

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

    @Override
    public boolean tryLock(long timeoutSec) {
   
        // 获取线程提示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 拆箱有可能会有空指针异常
        return Boolean.TRUE.equals(success);
    }

    // 释放锁
    @Override
    public void unlock() {
   
        // 调用lua脚本 保证原子性
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

    //    @Override
//    public void unlock() {
   
//        // 获取线程提示
//        String threadId = ID_PREFIX + Thread.currentThread().getId();
//        // 获取锁中的标识
//        String lockValueId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//        // 判断标识是否一致
//        if (StringUtils.equals(threadId, lockValueId)) {
   
//            // 释放锁
//            stringRedisTemplate.delete(KEY_PREFIX + name);
//        }
//    }

}

public interface ILock {
   
    /**
    * 尝试获取锁
    * @param timeoutSec 锁持有的超时时间,过期后自动释放
    * @return true 代表获取锁成功;false代表获取锁失败
    * */
    boolean tryLock(long timeoutSec);

    /*
    * 释放锁
    * */
    void unlock();
}

 

lua脚本

--比较线程标识与锁中标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    --释放锁,del key
    return redis.call('del', KEYS[1])
end
return 0
  • 实现思路总结:
    • 利用 setnx ex 获取锁,并设置过期时间,保存线程标识;
    • 释放锁时先判断线程标识是否与自己一致,一致则删除锁;
  • 特性
    • 利用 setnx 满足互斥性
    • 利用 setex 保证故障时锁依然能释放,避免死锁,提高安全性;
    • 利用Redis集群保证高可用和高并发特性

转载:https://blog.csdn.net/qq_42582773/article/details/128362657
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场