多种方案解决超卖问题(分布式锁、单机锁、行锁,文末附带demo源码)

什么是超卖?

当商品库存为1,用户A和用户B同时提交了该商品订单,此时两个用户同时读取库存为1,并发进行内存扣减之后,进行更新数据库,导致库存最终更新为-1,产生卖。

我们先看一段会产生超卖问题的代码

@Transactional(rollbackFor = Exception.class)
    public Integer createOrder(int purchaseProductId,int purchaseProductNum) throws BaseException{

        Product product = productMapper.selectById(purchaseProductId);
        if (product==null){
            throw new BaseException("购买商品:"+purchaseProductId+"不存在");
        }

        //商品当前库存 & 校验库存
        Integer currentCount = product.getCount();
        log.info(Thread.currentThread().getName() + "库存数" + currentCount);
        if (purchaseProductNum > currentCount){
            throw new BaseException("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
        }

        //计算剩余库存
        Integer leftCount = currentCount -purchaseProductNum;
        product.setCount(leftCount);
        product.setTimeModified(new Date());
        productMapper.updateById(product);

       return orderCommonService.createOrder(product,purchaseProductNum);
    }

在上面的代码中,我们先去查询商品的库存数,然后判断库存数是否充足,如果不足,就抛出异常,如果充足,就创建订单。上面这段代码,在并发下单的情况下,就会产生超卖问题,我们将库存设置成1,然后创建五个线程并发请求一下看看结果

这是输出结果,可以看到,我们库存只有1个,但是下了五个单,这样就发生了超卖。解决超卖问题,我们需要使用锁

1、基于数据库行锁解决超卖问题

 如果我们需要使用数据库锁解决超卖问题,一般有两种方案,悲观锁乐观锁

乐观锁:乐观锁并不是数据库锁机制,而是一种cas思路,一般会在表中添加一个version版本字段,更新数据的时候同时更新版本字段

悲观锁:悲观锁是基于InnoDB引擎的行锁机制(select ... for update)

优点:数据库既实现存储又实现锁,无需额外中间件。若基于乐观锁实现,能实现高效并发。

缺点:基于悲观锁机制,锁存续期间,其他线程无法对数据进行更改,在读请求压力大时,会造成大量线程堵塞,DB压力变高,影响其他正常业务,适合“写多读少”的场景。基于乐观锁机制,获得的锁为非阻塞,但没有获得锁的服务并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作,频繁的更新version也会严重影响数据库性能,适合“写少读多”的场景。

下面我们使用悲观锁的方式解决超卖问题

@Transactional(rollbackFor = Exception.class)
    public Integer createOrder(int purchaseProductId,int purchaseProductNum) throws BaseException {

        Product product = productMapper.selectByIdForUpdate(purchaseProductId);
        if (product==null){
            throw new BaseException("购买商品:"+purchaseProductId+"不存在");
        }

        //商品当前库存 & 校验库存
        Integer currentCount = product.getCount();
        log.info(Thread.currentThread().getName() + "库存数" + currentCount);
        if (purchaseProductNum > currentCount){
            throw new BaseException("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
        }

        //在数据库中完成减量操作
        productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());

        return orderCommonService.createOrder(product,purchaseProductNum);
    }

这里我们只需要改一行代码,就是查询商品信息productMapper.selectByIdForUpdate(purchaseProductId)的sql,我们改成

select * from demo_product where id = #{id} for update

我们将库存设置为2,创建5个线程调用一下

这里我们可以看到只有两个订单创建成功,超卖问题解决 

2、基于synchronized关键字解决超卖问题

/**
     * 这里不能使用 @Transactional(rollbackFor = Exception.class) 声明式事务,aop会导致synchronized锁失效
     */
    public synchronized Integer createOrder(int purchaseProductId,int purchaseProductNum) throws BaseException {
        TransactionStatus ts = null;
        try {
            ts = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
            Product product = productMapper.selectById(purchaseProductId);
            if (product == null) {
                throw new BaseException("购买商品:" + purchaseProductId + "不存在");
            }

            //商品当前库存 & 校验库存
            Integer currentCount = product.getCount();
            log.info(Thread.currentThread().getName() + "库存数" + currentCount);
            if (purchaseProductNum > currentCount) {
                throw new BaseException("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");
            }

            //在数据库中完成减量操作
            productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());
            Integer order = orderCommonService.createOrder(product, purchaseProductNum);
            dataSourceTransactionManager.commit(ts);
            return order;
        }catch (Exception e) {
            if(ts!=null) {
                dataSourceTransactionManager.rollback(ts);
            }
            throw e;
        }
    }

synchronized使用简单,是最常用的java锁关键字,这里就不多介绍

这里需要注意一点,synchronized不能跟声明式事务@Transactional注解一起使用,原因是声明式事务使用aop处理事务问题,会出现锁释放了但是事务没提交导致锁失效的问题

3、基于ReentrantLock可重入锁解决超卖问题

private Lock lock = new ReentrantLock();

    public  Integer createOrder(int purchaseProductId,int purchaseProductNum) throws BaseException {
        TransactionStatus ts = null;
        try{
            lock.lock();
            ts = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
            Product product = productMapper.selectById(purchaseProductId);
            if (product==null){
                throw new BaseException("购买商品:"+purchaseProductId+"不存在");
            }

            //商品当前库存 & 校验库存
            Integer currentCount = product.getCount();
            if (purchaseProductNum > currentCount) {
                throw new BaseException("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");
            }

            ///在数据库中完成减量操作
            productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());
            Integer order = orderCommonService.createOrder(product, purchaseProductNum);
            platformTransactionManager.commit(ts);
            return order;
        }catch (Exception e) {
            if(ts!=null) {
                dataSourceTransactionManager.rollback(ts);
            }
            throw e;
        }finally {
            lock.unlock();
        }
    }

ReentrantLock内部定义了一个final 的抽象类 Sync RLock很多操作需要借助Sync类完成。

Sync类继承AQSAbstractQueuedSynchronizer),因此锁的操作实际是借助AQS进行实现的。

AbstractQueuedSynchronizer先进先出FIFO等待队列的阻塞锁定 + 相关同步器(信号量&事件)

4、使用redis分布式锁解决超卖问题

原理是使用setnx命令获取锁

private final String ORDER_KEY = "order_key";

    public  Integer createOrder(int purchaseProductId,int purchaseProductNum) throws BaseException, InterruptedException {
        boolean lock;
        String key = ORDER_KEY+purchaseProductId;
        while (true) {
            lock = RedisLockUtil.lock(key);
            if (lock) {
                try {
                    Product product = productMapper.selectById(purchaseProductId);
                    if (product == null) {
                        throw new BaseException("购买商品:" + purchaseProductId + "不存在");
                    }

                    //商品当前库存
                    Integer currentCount = product.getCount();
                    log.info(Thread.currentThread().getName() + "库存数" + currentCount);
                    //校验库存
                    if (purchaseProductNum > currentCount) {
                        throw new BaseException("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");
                    }

                    //在数据库中完成减量操作
                    productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());
                    return orderCommonService.createOrder(product, purchaseProductNum);
                } finally {
                    RedisLockUtil.releaseLock(key);
                }
            }else {
                Thread.sleep(100);
            }
        }
    }
@Component
public class RedisLockUtil {

    private static RedisTemplate redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate redisTemplate) {
        RedisLockUtil.redisTemplate = redisTemplate;
    }

    private final static String VALUE = UUID.randomUUID().toString();

    public static Boolean lock(String key){
        RedisCallback<Boolean> redisCallback = redisConnection -> {
            //表示set nx 存在key的话就不设置,不存在则设置
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(30);
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            byte[] redisValue = redisTemplate.getKeySerializer().serialize(VALUE);
            return redisConnection.set(redisKey,redisValue,expiration,setOption);
        };

        //获取分布式锁
        return (Boolean)redisTemplate.execute(redisCallback);
    }

    public static Boolean releaseLock(String key){
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Collections.singletonList(key);

        return (Boolean) redisTemplate.execute(redisScript,keys,VALUE);
    }
}

5、使用redission分布式锁解决超卖问题

redis官网推荐实现分布式锁的三方类库。其功能非常强大,对各种锁都有实现,使用非常简单,使用者能够将更多的关注点放在业务逻辑上。

可以解决redis锁不可重入、锁过期等问题

加锁&释放锁:基于RLock#lock()&RLock#unlock()方法操作即可

线程并发获取,当一个线程获取到锁,其他线程则获取不到,并且内部会不断尝试获取锁,当持有锁的线程将锁释放后,其他线程则会继续去竞争锁

存在问题 通常,业务执行多久无法确定一个准确值。lock() 虽可设置超时时间,但锁超时后,锁就会自动释放若此时业务仍在执行,且后续线程又获取到了新的锁,在解锁时候就会出现异常造成死锁因为加锁时的唯一标识与解锁时的唯一标识发生了改变。

看门狗机制 - 不对锁key设置超时时间,当超时时间为-1时,启动一个定时任务,在业务释放锁之前,会一直不停的增加这个锁的有效时间,从而保证在业务执行完毕前,这把锁不会被提前释放掉。

开启看门机制:将RLock#lock()替换为RLock#tryLock()可,默认每隔30秒进行一次续期。

private final String ORDER_KEY = "order_key";

    public  Integer createOrder(int purchaseProductId,int purchaseProductNum) throws BaseException {
        String key = ORDER_KEY+purchaseProductId;

        RLock rlock = redissonClient.getLock(key);
        rlock.lock(30, TimeUnit.SECONDS);
        try {
            Product product = productMapper.selectById(purchaseProductId);
            if (product==null){
                throw new BaseException("购买商品:"+purchaseProductId+"不存在");
            }

            //商品当前库存
            Integer currentCount = product.getCount();
            //校验库存
            if (purchaseProductNum > currentCount){
                throw new BaseException("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
            }

            //在数据库中完成减量操作
            productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());
            return orderCommonService.createOrder(product, purchaseProductNum);
        }finally {
            rlock.unlock();
        }
    }

demo源码:cache-demo: 常见各种缓存demo用来超卖问题

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值