#1 問題描述# 最近有小夥伴在作商品抽獎活動時,在對獎品庫存進行扣減,有線程安全的問題,遂加鎖synchronized進行同步,但發現加鎖後並無控制住庫存線程安全的問題,致使庫存仍被超發。java
先簡單介紹下,各層的技術架構:算法
中間層框架:Spring 4.1.0express
持久層:MyBatis 3.2.6安全
MVC框架:Spring MVC 4.1.0網絡
存在問題的代碼:多線程
@Override public void saveMemberTicket(ApplyTicketReq applyTicketReq) throws ServiceException { synchronized (this.class) { // 檢查庫存是否有剩餘 preCheck(applyTicketReq); // 扣減庫存 modifyTicketAmount(applyTicketReq); } }
庫存扣減超發問題具體描述:架構
當庫存剩餘爲1時,線程1拿到鎖進入同步代碼塊,扣減庫存,線程2等待鎖;併發
當線程1執行完同步代碼塊時,線程2拿到鎖,執行同步代碼塊,檢查到的庫存剩餘仍爲1;【此時,庫存應該爲0,產生庫存扣減超發問題】app
#2 排查問題# 排查問題開始以前,簡單說下本身排查問題的幾個原則(僅供參考):框架
問題重現:必定要先重現問題,任何重現不了的問題,都不是問題。同理,任何存在的問題,都必然能再次重現。
由近及遠:先確認本身的代碼無問題,而後再去確認外部代碼無問題(如:框架代碼,第三方代碼等)。
由外到內:程序就是一個IPO,有輸入Input(如:參數、環境等)也有輸出Out(如:結果、異常等),輸出Out是問題的表象,先肯定外部因素Input無問題,再確認程序代碼邏輯無問題。
由淺入深:其實就是由易到難、自上向下,先從上層應用排查問題,如:上層API、應用層、HTTP傳輸等,而後再確認底層應用排查問題,如:底層API、網絡層、系統層、字節碼、JVM等;
根據多線程併發測試,能夠確認多線程之間是同步執行synchronized代碼塊,確認synchronized同步執行沒問題。
<!-- Transaction Support --> <tx:advice id="useTxAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="*remove*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="*save*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="*modify*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="*update*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="create*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="fill*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="cancel*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="*chang*" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="handleLotteryResult" propagation="REQUIRED" read-only="false" rollback-for="java.lang.Exception" no-rollback-for="com.xxx.exception.ServiceException"/> <tx:method name="find*" propagation="SUPPORTS"/> <tx:method name="get*" propagation="SUPPORTS"/> <tx:method name="query*" propagation="SUPPORTS"/> <tx:method name="page*" propagation="SUPPORTS"/> <tx:method name="count*" propagation="SUPPORTS"/> </tx:attributes> </tx:advice> <!--把事務控制在Service層--> <aop:config> <aop:pointcut id="pc" expression="execution(public * com.xxx..service.*.*(..))" /> <aop:advisor pointcut-ref="pc" advice-ref="useTxAdvice" /> </aop:config>
因爲Spring事務是經過AOP實現的,因此在saveMemberTicket方法執行以前會有開啓事務,以後會有提交事務邏輯。而synchronized代碼塊執行是在事務以內執行的,能夠推斷在synchronized代碼塊執行完時,事務還未提交,其餘線程進入synchronized代碼塊後,讀取的庫存數據不是最新的
。
#3 解決問題# 將synchronized關鍵字加入到Controller層,使synchronized鎖的範圍大於事務控制的範圍。
@RequestMapping(value = "applyTicket") @ResponseBody public void applyTicket(@FromJson ApplyTicketReq applyTicketReq) throws Exception { synchronized (String.valueOf(applyTicketReq.getMemberRoomId()).intern()) { synchronized (String.valueOf(applyTicketReq.getTicketId()).intern()) { service.saveMemberTicket(applyTicketReq); } } responseMessage(ModelResult.CODE_200,ModelResult.SUCCESS); }
#4 總結問題# 根據以上的排查過程,已經很清楚的確認了事務與鎖之間存在的問題。因爲事務範圍大於鎖代碼塊範圍,在鎖代碼塊執行完成後,此時事務還未提交,致使此時進入鎖代碼塊的其餘線程,讀到的還是原有的庫存數據。
關於程序加鎖本身的一點看法:
建議程序中儘可能不要加鎖;
儘可能在業務和代碼層,解決線程安全的問題,實現無鎖的線程安全;
若是以上兩點都作不到,必定要加鎖,儘可能使用java.util.concurrent包下的鎖(由於是非阻塞鎖,基於CAS算法實現,具體能夠查看AQS類的實現);
若是以上三點仍然都作不到,必定要加阻塞鎖:synchronized鎖,兩個原則:(1)儘可能減少鎖粒度;(2)儘可能減少鎖的代碼範圍;