mysql增加version字段實現樂觀鎖,實現高並發下的訂單庫存的並發控制,通過開啟多線程同時處理模擬多個請求同時到達的情況
=============================================================
完整的代碼請到GIthub查看:https://github.com/AngelSXD/swapping
多個線程處理完后再做事情:https://www.cnblogs.com/sxdcgaq8080/p/9456006.html
=============================================================
先說說同一個事務中使用一個樂觀鎖的情況:
核心功能點:
1.先做查詢 【查詢時候把version帶出來】
<select id="findByUid" parameterType="String" resultType="com.sxd.swapping.domain.GoodsStock"> select uid uid, version version, sale_num saleNum, stock stock from goods_stock where uid = #{uid} </select>
2.再做更新【更新的時候判斷version是不是查出來時候的version,如果是,則更新,更新時順便version+1即可。否則不更新】
<update id="updateStock" parameterType="com.sxd.swapping.domain.GoodsStock"> update goods_stock set <if test="stock != -1"> stock = stock - #{buyNum}, </if> sale_num = sale_num + #{buyNum}, version = version + 1 where uid = #{uid} and version = #{version} </update>
=============================================================
1.實體對應數據表
/** * 低配版本的 商品庫存表 */ @Entity @Table @Getter @Setter public class GoodsStock extends BaseBean { private String goodsName;//商品名稱 private String goodsPrice;//商品價格 private Long buyNum;//購買數量 private Long saleNum;//銷售量 private Long stock;//商品庫存 庫存為-1 代表無限量庫存 private Integer version;//版本號 @Transient private Integer threadCount;//模擬並發訪問的線程數量 實際業務中不用這個字段 僅用作本次測試接口使用 }

2.mybatis的mapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sxd.swapping.dao.mybatis.GoodsStockMapper"> <update id="updateStock" parameterType="com.sxd.swapping.domain.GoodsStock"> update goods_stock set <if test="stock != -1"> stock = stock - #{buyNum}, </if> sale_num = sale_num + #{buyNum}, version = version + 1 where uid = #{uid} and version = #{version} </update> <select id="findByUid" parameterType="String" resultType="com.sxd.swapping.domain.GoodsStock"> select uid uid, version version, sale_num saleNum, stock stock from goods_stock where uid = #{uid} </select> </mapper>
mybatis的mapper.java
package com.sxd.swapping.dao.mybatis; import com.sxd.swapping.domain.GoodsStock; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface GoodsStockMapper { int updateStock(GoodsStock goodsStock); GoodsStock findByUid(@Param("uid") String uid); }
3.serviceImpl層代碼
@Autowired GoodsStockMapper mapper; /** * 數據庫加 version 版本號 * * 實現 數據庫樂觀鎖 * * 實現高並發下庫存的並發控制機制 * * 要保證事務一致性,要么都使用mybatis 要么都使用jpa * @param map * @param entity * @param threadNum * @return */ @Override @Transactional public void updateStock(Map<Integer,String> map, GoodsStock entity, Integer threadNum) { String uid = entity.getUid(); Long buyNum = entity.getBuyNum(); String msg = ""; //判斷庫存是否足夠 GoodsStock old = mapper.findByUid(uid); Long stock = old.getStock(); System.out.println("線程"+threadNum+"---------->正在工作"); if (stock >= buyNum){ old.setBuyNum(buyNum); if (mapper.updateStock(old) > 0 ){ msg = "庫存扣除成功,剩余庫存數量:"; }else { msg = "庫存扣除失敗,剩余庫存數量:"; } Long nowStock = mapper.findByUid(uid).getStock(); msg +=nowStock; }else { msg = "庫存不足,剩余庫存數量:"+stock; } map.put(threadNum,msg); }
4.controller層代碼:
/** * uid代表 同一時間 大家都來買這一件東西 * threadCount代表 同時會有多少人在操作 * buyNum代表 同一個人的一次購買量 * @param entity * @return */ @RequestMapping(value = "/concurrentStock",method = RequestMethod.POST) public UniVerResponse<Map<Integer,String>> concurrentStock(@RequestBody GoodsStock entity){ UniVerResponse.checkField(entity,"uid","threadCount","buyNum"); UniVerResponse<Map<Integer,String>> res = new UniVerResponse<>(); String uid = entity.getUid(); GoodsStock old = service.findByUid(uid); if (old != null){ //設置一個線程安全的Map記錄各個線程是否成功執行 Map<Integer,String> map = new ConcurrentHashMap<Integer, String>(); Integer threadCount = entity.getThreadCount(); //所有線程阻塞,然后統一開始 CountDownLatch begin = new CountDownLatch(1); //主線程阻塞,直到所有分線程執行完畢 CountDownLatch end = new CountDownLatch(threadCount); //開始多線程 begin.countDown(); for (Integer i = 0; i < threadCount; i++) { Runnable runnable = buyGoods(map,entity,i,begin,end); new Thread(runnable).start(); } //多個線程都執行結束 try { end.await(); res.beTrue(map); } catch (InterruptedException e) { e.printStackTrace(); res.beFalse("多線程執行失敗",UniVerResponse.ERROR_BUSINESS,null); } }else { res.beFalse("商品不存在",UniVerResponse.ERROR_BUSINESS,null); } return res; } //多線程的方法 public Runnable buyGoods(Map<Integer,String> map, GoodsStock entity, Integer threadNum,CountDownLatch begin,CountDownLatch end){ Runnable runnable = new Runnable() { @Override public void run() { try { System.out.println("線程"+threadNum+":--------------------->開始工作"); begin.await(); service.updateStock(map,entity,threadNum); end.countDown(); System.out.println("線程"+threadNum+":--------------------->結束工作"); } catch (InterruptedException e) { e.printStackTrace(); } } }; return runnable; }
5.發送請求

第一次請求:
下圖所示,僅有線程編號為3的 線程 購買成功,其他都購買失敗。


第二次請求:


第三次請求:


第四次請求:


最后一次請求:


二.再說說在同一個事務中使用多個樂觀鎖的情況
===============================================================================================
下面僅寫一段代碼舉個例子即可:
即 第一步操作,第二步 都會使用樂觀鎖
如果執行失敗有兩種情況:
1.數據庫連接斷開,sql真正的執行出錯
2.sql成功執行,但是其實update執行失敗,因為version對應不起來
所以需要注意的是 如果使用樂觀鎖執行失敗[失敗情況2],那么需要自己手動去拋出異常,去保證事務的一致性!!!
因為失敗情況1自己會拋出RuntimeException
因為下面示例代碼中的第一步操作如果失敗了會直接返回 所以並沒有去拋異常
/** * 進行兌換 * * 1.減少會員積分總數[加樂觀鎖] * * 2.減少商品庫存 增加商品銷量[加樂觀鎖] * * 3.新增兌換記錄 * * * @param entity * @return */ @Override @Transactional public boolean insert(ExchangeOrder entity,String integralUid,Integer buyIntegral) { boolean isSuccess = false; //1.減少會員積分 IntegralDetail integralDetail = integralDetailMapper.findByIntegralId(integralUid); integralDetail.setIntegralValue(buyIntegral);//sql 做減操作 isSuccess = (integralDetailMapper.deductIntegral(integralDetail) > 0); if (isSuccess){ //2.減少商品庫存 增加商品銷量 IntegralGoods integralGoods = integralGoodsMapper.findByUid(entity.getIntegralGoodsId()); //無限庫存不做修改 if (integralGoods.getStock() != -1) { integralGoods.setStock(entity.getBuyNum()); } //增加銷量 integralGoods.setSaleNum(entity.getBuyNum()); integralGoods.initUpdateDataMen(); isSuccess = (integralGoodsMapper.updateStock(integralGoods) > 0); if (isSuccess){ //3.新增兌換記錄 mapper.insert(entity); }else{ throw new RunException("銷量增加失敗,請稍后再試"); } } return isSuccess; }
