秒杀实现通过乐观锁控制超卖问题
通过悲观锁控制每个用户只能下一单,避免用户多次点击,发送的多次下单请求(即多个线程)都成功,避免恶意攻击
每个请求访问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)来获取代理对象调用事务方法,这样事务才会生效!!
代码实现:(仅作参考,结合业务逻辑食用)
@AutowiredApplicationContext context;//模仿秒杀减库存,创建订单@Overridepublic Boolean killInSecond(Integer userID,Integer productID){//检查库存是否>0Product 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;}@AutowiredOrderMapper om;@AutowiredRedisIdIncrement redisId;//redis全局唯一ID生成工具,想省事可以直接uuid//创建订单,减库存操作@Transactionalpublic 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唯一IDLong 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>