redis实现秒杀超卖问题的解决方案:(仅限于单体项目)

秒杀实现通过乐观锁控制超卖问题

通过悲观锁控制每个用户只能下一单,避免用户多次点击,发送的多次下单请求(即多个线程)都成功,避免恶意攻击

                        每个请求访问Tomcat时,就会分配一个线程处理请求

业务逻辑:

注*以下逻辑中报错也可以改为给前端返回错误信息

1.检查数据库查看库存是否>0,不满足直接报错

2.当数据库库存>0,检查该用户对本商品的订单是否存在,如果已存在说明下过单,报错

3.如果订单也不存在,对数据库数据进行减库存,这里容易出现超卖,使用乐观锁

4.减库存后创建订单,这个过程和2步骤有关联,应该使用悲观锁控制2-4步骤的访问

乐观锁(本质sql的where语句验证):即修改数据库前查看数据前后是否一致,不一致说明中间有其他线程修改数据,则放弃修改。

update product set sale=slae-1 where sale=?
//?为检查数据库库存是否>0时查询的值,这样在修改数据时通过where在验证一遍数据是否被修改

缺点:高并发场景下修改数据库成功率太低!

优化:宽松的乐观锁,严格来说不算乐观锁了,就是条件更改

update product set sale=slae-1 where sale>0

将条件变为sale>0哪怕中间有其他线程进行减库存,只要数量依然>0依然允许修改,这样就可以完美符合我们减库存的预期(可以使用Jmeter工具进行高并发测试)

悲观锁(本质synchronized同步机制):避免同一个用户对创建订单接口访问进行多次请求时,多次请求都创建订单成功。如果不是同一用户,允许异步访问数据库创建多个不同用户订单

1.应该将2-4步骤抽离出单独放到一个方法里面,因为减库存和创建订单是一个事务,应该将其放到同一事务中执行

2.同步锁不建议直接加到2-4步骤的方法上面,因为方法同步锁,锁对象都是this,如果该类中有多个锁的对象都是this,容易照成线程的过度阻塞,导致程序反应变慢

所以对象锁不应该绑定this而是和userID绑定,因此就要使用同步代码块进行上锁,锁对象都为和userID挂钩的同一种对象即可-----这里使用userID.toString().intern()来指定锁对象

2.1为什么使用userID.toString().intern()来充当锁对象:

如果直接使用Integer类型的userID参数对象,来作为锁对象毫无意义,因为不同请求传递的参数都会在堆中new出不同的对象,这样哪怕是多个线程(请求)访问接口,传递的同一userID同步锁也无法限制他们进行同步访问代码块,因为锁对象根本不是同一对象

直接使用this也不行,因为我们只需要限制参数为同一userID的请求访问代码块时要同步,不同userID的请求之间不需要使用同一把锁限制他们。而是使用多把锁,限制同一userID的请求进行同步访问代码块2-4步骤,多把锁允许不同userID的请求可以异步访问访问代码块2-4步骤。如果使用了this,所有请求访问代码块2-4步骤时都会同步访问,不符合我们预期

3.这个同步代码块不应该封装到2-4步骤的事务方法中去,而是封装到调用该方法的位置

如果封装到2-4步骤的事务方法中,同一userID线程虽然访问代码块被同步限制了,但是除了代码块后事务还没有真正提交,这时其他同一userID的线程又进入代码块中,查询数据库中有无订单时,可能没有订单,因为方法没执行完毕,事务未提交,数据库中还没有订单信息。

所以为了避免这种情况,代码块应该封装到调用该事务方法的地方,将该事务方法放到代码块中执行

4.@Transaction事务失效

上面我们说应该在其他方法中调用2-4步骤的事务方法,但是同一类中调用事务方法,事务不会生效,只有通过spring创建的代理对象,引用事务方法时事务才会生效.

所以在引用事务方法时要通过spring框架的ApplicationContext对象的getBean(类名.class)来获取代理对象调用事务方法,这样事务才会生效!!

代码实现:(仅作参考,结合业务逻辑食用)

    @Autowired
    ApplicationContext context;

    //模仿秒杀减库存,创建订单
    @Override
    public Boolean killInSecond(Integer userID,Integer productID){
        //检查库存是否>0
        Product product = pm.selectByPrimaryKey(productID);
        if(product.getSales()<=0){
            throw new MyExceptionHandler("库存不足");
        }
        //调用2-4步骤方法
        Boolean result=false;
        synchronized (userID.toString().intern()){
            //使用代理对象调用事务方法
            ProductServiceImpl bean = context.getBean(ProductServiceImpl.class);
            result=bean.ProductAndOrder(userID,productID);
        }
        return result;
    }


    @Autowired
    OrderMapper om;

    @Autowired
    RedisIdIncrement redisId;//redis全局唯一ID生成工具,想省事可以直接uuid

    //创建订单,减库存操作
    @Transactional
    public Boolean ProductAndOrder(Integer userID,Integer productID){
        //检查数据库中书否存在该用户订单
        Integer orderCount = om.selectOrderByUserIdAndProductId(userID, productID);
        if(orderCount>0){
            throw new MyExceptionHandler("用户已下单");
        }

        //订单不存在减库存,宽松乐观锁
        Integer result = pm.updateProductBysale(productID);
        if(result!=1){
            throw new MyExceptionHandler("库存不足");
        }

        //创建订单
        //获取redis唯一ID
        Long orderId = redisId.getRedisID("order");
        //封装订单
        Order order=new Order(orderId.toString(),userID,"","",productID,"",null,1,0,null,null,null,null,new BigDecimal(100));
        result = om.insertCompleteOrder(order);
        if(result!=1){
            return false;
        }
        return true;
    }

mybatisXML文件的乐观锁sql语句

<!--  对单个商品减库存-->
  <update id="updateProductBysale">
    update product set sales=sales-1 where id=#{productId} and sales>0
  </update>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值