前期准备:
设计表结构:
商品表
购买记录表
dao设计
<!--获取产品--> <select id="getProduct" parameterType="long" resultType="product"> select id,product_name as productName, stock,price,version,note from t_product where id=#{id} </select> <!--减少库存--> <update id="decreaseProduct"> update t_product set stock = stock - #{quantity} where id = #{id} </update>
<!--插入购买记录--> <insert id="insertPurchaseRecord" parameterType="purchaseRecord"> insert into t_purchase_record( user_id,product_id,price,quantity,sum,purchase_date,note) values(#{userId},#{productId},#{price},#{quantity}, #{sum},now(),#{note}) </insert>
service层设计
@Transactional public boolean purchase(Long userId, Long productId, int quantity) { // 获取产品 ProductPo product = productMapper.getProduct(productId); // 比较库存与购买量 if (product.getStock()<quantity){ return false; } // 扣减库存 productMapper.decreaseProduct(productId,quantity); // 创建购买记录 PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity); // 插入购买记录 purchaseRecordMapper.insertPurchaseRecord(pr); return true; } private PurchaseRecordPo initPurchaseRecord(Long userId, ProductPo product, int quantity) { PurchaseRecordPo pr = new PurchaseRecordPo(); pr.setNote("购买日志,时间:" + System.currentTimeMillis()); pr.setProductId(product.getId()); pr.setPrice(product.getPrice()); pr.setQuantity(quantity); double sum = product.getPrice() * quantity; pr.setSum(sum); pr.setUserId(userId); return pr; }
controller层设计
@GetMapping("/purchase") public String purchase(){ return "purchase"; } @PostMapping("/purchase") @ResponseBody public Result purchase(Long userId,Long productId,Integer quantity){ boolean success = purchaseService.purchase(userId,productId,quantity); String message = success? "抢购成功":"抢购失败"; Result result = new Result(success,message); return result; } class Result{ private boolean success; private String message; public Result() { } public Result(boolean success, String message) { this.success = success; this.message = message; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
前端页面调用
<script type="text/javascript" src="/js/jquery-1.8.3.js"></script> <script type="text/javascript"> for(var i=0;i<50000;i++){ var params = { userId:1, productId:1, quantity:1 }; $.post("/purchase/purchase",params,function (result) { //alert(result.message); }) } </script>
五万人抢两万件商品结果如下
可看出出现超发现象,卖出去了20003件商品,库存变为-3
那么这样的问题应该如何来解决呢?当前企业中提出了悲观锁、乐观锁、redis等解决方案
一、悲观锁
<!--获取产品--> <select id="getProduct" parameterType="long" resultType="product"> select id,product_name as productName, stock,price,version,note from t_product where id=#{id} for update </select>
数据库事务执行的过程中 就会锁定查询出来的数据 其他的事务将不能再对其进行读写,这样就避免了数据的不 单个请求直至数据 事务完成,才会释放这个锁,其他的请求才能重新得 这个锁
结果正确,但第一次我耗时们耗时51秒,这次我们一共花费了一分零5秒,花费了更多的时间,那如何减少需要花费的时间呢?
二、乐观锁
<!--减少库存--> <update id="decreaseProduct"> update t_product set stock = stock - #{quantity}, version = version + 1 where id = #{id} and version = #{version} </update>
别忘记去掉for update,并同步修改dao减少库存接口加上第三个参数
@Transactional public boolean purchase(Long userId, Long productId, int quantity) { // 获取产品 ProductPo product = productMapper.getProduct(productId); // 比较库存与购买量 if (product.getStock()<quantity){ return false; } // 获取当前版本号 int version = product.getVersion(); // 尝试扣减库存 int result = productMapper.decreaseProduct(productId, quantity, version); if (result == 0) { return false; } // 创建购买记录 PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity); // 插入购买记录 purchaseRecordMapper.insertPurchaseRecord(pr); return true; }
结果如下:卖出5447,剩余14553件产品
这次时间是快了,但是却剩余了大量的产品,为什么呢,因为并发操作时好多操作都被判定失败了。
改进方法:
使用时间戳限制重入的乐观锁
public boolean purchase(Long userId, Long productId, int quantity) { // 当前时间 long start = System.currentTimeMillis(); // 循环尝试直到成功 while(true){ // 循环时间 long end = System.currentTimeMillis(); if (end - start>100){ return false; } // 获取产品 ProductPo product = productMapper.getProduct(productId); // 比较库存与购买量 if (product.getStock()<quantity){ return false; } // 获取当前版本号 int version = product.getVersion(); // 尝试扣减库存 int result = productMapper.decreaseProduct(productId, quantity, version); if (result == 0) { continue; } // 创建购买记录 PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity); // 插入购买记录 purchaseRecordMapper.insertPurchaseRecord(pr); return true; } }
结果如下: 吐血,可能由于本人电脑问题,剩余商品反而更多了,剩余18698,卖出1302,耗时1分27秒
使用限定次数重入的乐观锁
@Transactional public boolean purchase(Long userId, Long productId, int quantity) { for(int i=0;i<3;i++){ // 获取产品 ProductPo product = productMapper.getProduct(productId); // 比较库存与购买量 if (product.getStock()<quantity){ return false; } // 获取当前版本号 int version = product.getVersion(); // 尝试扣减库存 int result = productMapper.decreaseProduct(productId, quantity, version); if (result == 0) { continue; } // 创建购买记录 PurchaseRecordPo pr = this.initPurchaseRecord(userId,product,quantity); // 插入购买记录 purchaseRecordMapper.insertPurchaseRecord(pr); return true; } return false; }
结果如下:这次还好,剩余6550,卖出13450,耗时1分16秒
三、那有没有更好的方法呢,有的那就是使用redis
采用lua脚本在内存中操作数据进行抢购,再通过定时任务的方式将其写入到数据库中,总用时15秒,非常快。