加锁方式来防止超卖问题的一些见解
本文最后更新于7 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com

超卖问题一直是面试的常见问题之一

这里刚刚好系统性的学习了

如何通过加锁的方式来防止超卖问题的发生

总而言之就是在业务逻辑方法内部用相对宽松的乐观锁

而方法调用时用严格的悲观锁

来实现乐观与悲观的相互结合协调


下面先给出一段代码示例:

Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()){
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            //拿到当前对象的代理对象,即实现的接口的代理对象
        return proxy.createVoucherOrder(voucherId);}
    }


    @Transactional//此处业务逻辑牵扯到了两张表,所以最好加上事务注解
    public  Result createVoucherOrder(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("一人仅限一单哦~");
            }}
        /**
         * gt加了一个乐观锁,设置的条件比较宽松只要stock大于0
         * 如果设置stock=stock,也就是确认stock没有被其他线程修改
         * 会导致过于严格而导致大多数都会失败
         */
//扣减库存
        boolean success = seckillVoucherService.update()// 1. 开始构建更新操作     相当于    update seckill_voucher
                .setSql("stock=stock-1")// 2. 设置要更新的SQL片段相当于set stock=stock-1
                .eq("voucher_id", voucherId).gt("stock", 0) // 3. 设置更新条件  where voucher_id=#{voucherId},stock大于0
                .update(); // 4. 执行更新  .update() 的返回值是true
        if (!success) {
            return Result.fail("库存不足");
        }

想必你也会有诸多疑问,比如:

  • 悲观锁为什么是加在这?
  • 乐观锁为什么是这样设置的?
  • proxy代理对象是为什么?
  • intern()是什么东西?

下面逐一说说吧:

悲观锁:

首先悲观锁加在这肯定不是乱加的,有说法的。

悲观锁认为所有线程都是互相不可信的,都是不安全的

如果想当然的直接在createVoucherOrder加上 synchronized关键字

那的确是在整个业务方法上确确实实的上了悲观锁

但是会导致不同ID的UserID都需要等待前一位锁的释放(而我们的目的是阻止相同ID的用户多买)

进而导致整个方法都是串行执行的,是十分严重的性能问题

所以这边应该把悲观锁锁在UserID中

这样才能阻止相同ID的用户进入逻辑执行

//识别userId,若ID相同就卡在这里,等待这个用户的上个请求被执行完毕。
synchronized (userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
     //而进入方法中会检测你的购买数是不是大于1,然后叫你滚蛋
        return proxy.createVoucherOrder(voucherId);}

乐观锁:

乐观锁认为所有线程都是比较安全的,不会被其他线程入侵

即使发现数据被修改也只需要更新数据就可以了

boolean success = seckillVoucherService.update()// 1. 开始构建更新操作     相当于    update seckill_voucher
                .setSql("stock=stock-1")// 2. 设置要更新的SQL片段相当于set stock=stock-1
                .eq("voucher_id", voucherId).gt("stock", 0) // 3. 设置更新条件  where voucher_id=#{voucherId},stock大于0
                .update(); // 4. 执行更新  .update() 的返回值是true
        if (!success) {
            return Result.fail("库存不足");
        }

在上面的代码实现中,这里的乐观锁其实就是加了个gt(“stock”,0)。

就是判断库存是否大于0,为True的话才能购买,这是一种更为宽松的锁形式

也能判断库存值是否等于道歉库存值,也就是stock=getStock();

这种是更为严格的锁形式,高并发状态下会导致阻止过多购入

所以无论哪种,单一的乐观锁无法解决超买超卖的问题,都需要配合悲观锁才行


proxy代理对象是为什么?

首先因为业务方法中设置了事务注解,只有动态代理对象才能应用于事务注解

正常来说代码应该是这样的无动态代理

return this.createVoucherOrder(voucherId);

但是这里的this是一个实例化对象,实例化对象无法应用于事务

这也是Spring事务失效的几种原因之一

当然事务注解是不能删去的,不然会导致大问题

所以这里我们必须引入动态代理对象

也就是这一句:

IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();

AopContext.currentProxy();将会拿到当前对象的代理对象

也就是当前类所实现的接口的代理对象

Idea出于甩锅考虑,所以会默认给一个最为保守的Object类

但是我们知道所实现的类是IVoucherOrderService,所以我们就强转过来。


intern()是什么东西?

简单来说就是Java自带的toString()方法

其实是new了一个新的对象出来

即使名称相同,但是内存地址是截然不同的,所以其实还是不同的对象

但是悲观锁是一个高冷的总裁大人,他管你这的那的

他不关心你的名字是什么,也不关心你的是不是那个对象

反正不是同一个人,就直接放行

所以为了防止高傲的总裁大人任性,就有了intern()方法的出现

他会严格按照字符串来去转换,而不是创建一个新的对象

总裁大人依旧不管这那,看见了名称是一样的UserID,就卡死

所以在这里intern()是必要的。


还有一些唧唧歪歪的东西:

值得留意的是return上面有一个:Long userId = UserHolder.getUser().getId();

而后续的方法体中也有一个:Long userId = UserHolder.getUser().getId();

就会让人留意:后面重新拿了一个新的user,不会拿错吗?

实际上是不会的,这就是LocalThread的特性

一次业务,代码从上到下执行一次,都是单独的线程池,都是单独的线程

拿也是拿的刚刚进去的

就这样吧,后面有新的知识再补充吧

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇