我們之前講了秒殺模塊的實現,使用了sychronized互斥鎖,但是在集群模式下因為不同服務器有不同jvm,所以synchronized互斥鎖失效了。
redis實現秒殺超賣問題的解決方案:(僅限于單體項目)-CSDN博客
這時就要找到一個多臺服務器都能識別的鎖,即redis中的setNX充當互斥鎖,來控制秒殺的一人一單
在redis緩存擊穿中,使用邏輯過期就用過互斥鎖,這里原理一摸一樣,只不過這里存儲的value為UUID+線程ID
setNX互斥鎖的使用:
場景1:(會導致一個用戶創建多個訂單)
注*線程1和線程2的userID相同,所以創建的redis鎖key值相同,但是value不相同,釋放鎖時如果不進行驗證value值,很有可能會出現場景1的情況。
場景2:
注*在線程1檢查鎖后,發現自己的鎖過期了,該鎖不是自己創建的,說明其他相同userID的線程也在創建訂單,這時應該回滾,撤銷之前數據庫操作。(在調用減庫存創建訂單的方法中回滾)
代碼實現:
鎖工具:
創建后不能注冊為Bean,用的時候new對象即可,如果想注冊為Bean使用keywords和value都要作為參數傳遞,否者會出現多線程隨意修改該值的情況
public class RedisLock {StringRedisTemplate template;String keywords;String value;public RedisLock(StringRedisTemplate template,String keywords){this.keywords=keywords;this.template=template;}//嘗試創建鎖public boolean tryLock(Integer timeOutSecond){//給該線程生成唯一標識,作為valuevalue=UUID.randomUUID().toString().replace("-","")+"-"+Thread.currentThread().getId();return template.opsForValue().setIfAbsent("lock:"+keywords,value,timeOutSecond, TimeUnit.SECONDS);}//嘗試刪除鎖public void delLovk(){//釋放鎖之前先驗證鎖是否過期,是為自己的鎖String result = template.opsForValue().get("lock:" + keywords);if(result!=null || result.equals(value)){template.delete("lock:"+keywords);}}
}
之所以不將該類注冊為bean使用,是因為創建鎖時,要獲取UUID+線程的ID,刪除鎖時也需要該值,所以這個值只能使用一個全局變量來記錄。
如果注冊為bean后,所有線程的操作都使用該對象中value屬性去進行賦值和刪除操作,就會導致value被不斷修改,keywords也會被不斷修改,最終導致程序邏輯錯誤,應該一個線程使用一個獨有的value屬性,所以不能將該工具類注冊為Bean,用的時候new即可
業務邏輯代碼:有原來的synchronized改為分布式鎖控制線程創建訂單
@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);
// }//分布式鎖RedisLock redisLock = new RedisLock(template, "order:" + userID);//獲取鎖result=redisLock.tryLock(30);if(!result){throw new RuntimeException("該用戶只能創建一個訂單");}//使用代理對象調用事務方法ProductServiceImpl bean = context.getBean(ProductServiceImpl.class);result=bean.ProductAndOrder(userID,productID);//釋放鎖redisLock.delLovk();return result;}
減庫存創建訂單方法:
@AutowiredOrderMapper om;@AutowiredRedisIdIncrement redisId;//創建訂單,減庫存操作@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;}
不足:
雖然我們通過檢查鎖的value值判斷該鎖是否為本線程創建的鎖,控制了誤刪鎖的可能,但是這里依然會沒有解決多個相同userID的線程,會創建多個訂單的情況。
情況一:
在線程1檢查鎖后,發現自己的鎖過期了,該鎖不是自己創建的,說明其他相同userID的線程也在創建訂單,這時應該回滾,撤銷之前數據庫操作。(在調用減庫存創建訂單的方法中回滾)
情況2:
刪除鎖時發現自己的鎖過期了,緩存中沒有該鎖,說明
1.沒有其他相同userID用戶執行創建訂單的邏輯,不需回滾直接結束程序即可
2.有其他線程執行了操作,但是已經執行完畢,訂單也已經創建完畢,繼續執行程序即可,因為創建訂單時發現訂單已存在,自會回滾