1. 程式人生 > >spring事務和synchronized鎖的一些問題

spring事務和synchronized鎖的一些問題

1 問題描述

最近有小夥伴在做商品抽獎活動時,在對獎品庫存進行扣減,有執行緒安全的問題,遂加鎖synchronized進行同步,

但發現加鎖後並沒有控制住庫存執行緒安全的問題,導致庫存仍被超發。

先簡單介紹下,各層的技術架構

中間層框架:spring 4.1.0
持久層:MyBatis 3.2.6
MVC框架:Spring MVC 4.1.0

存在問題的程式碼:

[html] view plain copy print?
  1. @Override  
  2. public void saveMemberTicket(ApplyTicketReq applyTicketReq) throws ServiceException {  
  3.     synchronized (this.class) {  
  4.         // 檢查庫存是否有剩餘  
  5.         preCheck(applyTicketReq);  
  6.         // 扣減庫存  
  7.         modifyTicketAmount(applyTicketReq);  
  8.     }  
  9. }  
@Override
public void saveMemberTicket(ApplyTicketReq applyTicketReq) throws ServiceException {
    synchronized (this.class) {
        // 檢查庫存是否有剩餘
        preCheck(applyTicketReq);
  
        // 扣減庫存
        modifyTicketAmount(applyTicketReq);
    }
}

庫存扣減超發問題具體描述:

當庫存剩餘為1時,執行緒1拿到鎖進入同步程式碼塊,扣減庫存,執行緒2等待鎖;
當執行緒1執行完同步程式碼塊時,執行緒2拿到鎖,執行同步程式碼塊,檢查到的庫存剩餘仍為1;【此時,庫存應該為0,產生庫存扣減超發問題】

2 排查問題

排查問題開始之前,簡單說下自己排查問題的幾個原則(僅供參考):

問題重現:一定要先重現問題,任何重現不了的問題,都不是問題。同理,任何存在的問題,都必然能再次重現。
由近及遠:先確認自己的程式碼無問題,然後再去確認外部程式碼無問題(如:框架程式碼,第三方程式碼等)。
由外到內:程式就是一個IPO,有輸入Input(如:引數、環境等)也有輸出Out(如:結果、異常等),輸出Out是問題的表象,先確定外部因素Input無問題,再確認程式程式碼邏輯無問題。

由淺入深:其實就是由易到難、自上向下,先從上層應用排查問題,如:上層API、應用層、HTTP傳輸等,然後再確認底層應用排查問題,如:底層API、網路層、系統層、位元組碼、JVM等;

確定synchronized關鍵字是否起作用;

【建議:儘量慎用synchronized關鍵字,非常影響程式效能】根據多執行緒併發測試

可以確認多執行緒之間是同步執行synchronized程式碼塊,確認synchronized同步執行沒問題。
確定Spring事務是否提交成功;檢視Spring 事務配置:

[html] view plain copy print?
  1. <!-- Transaction Support -->
  2. <tx:adviceid="useTxAdvice"transaction-manager="txManager">
  3.   <tx:attributes>
  4.       <tx:methodname="*remove*"propagation="REQUIRED"read-only="false"rollback-for="java.lang.Exception"no-rollback-for="com.xxx.exception.ServiceException"/>
  5.       <tx:methodname="*save*"propagation="REQUIRED"read-only="false"rollback-for="java.lang.Exception"no-rollback-for="com.xxx.exception.ServiceException"/>
  6.       <tx:methodname="*modify*"propagation="REQUIRED"read-only="false"rollback-for="java.lang.Exception"no-rollback-for="com.xxx.exception.ServiceException"/>
  7.       <tx:methodname="*update*"propagation="REQUIRED"read-only="false"rollback-for="java.lang.Exception"no-rollback-for="com.xxx.exception.ServiceException"/>
  8.       <tx:methodname="create*"propagation="REQUIRED"read-only="false"rollback-for="java.lang.Exception"no-rollback-for="com.xxx.exception.ServiceException"/>
  9.       <tx:methodname="find*"propagation="SUPPORTS"/>
  10.       <tx:methodname="get*"propagation="SUPPORTS"/>
  11.       <tx:methodname="query*"propagation="SUPPORTS"/>
  12.       <tx:methodname="page*"propagation="SUPPORTS"/>
  13.       <tx:methodname="count*"propagation="SUPPORTS"/>
  14.   </tx:attributes>
  15. </tx:advice>
  16. <!--把事務控制在Service層-->
  17. <aop:config>
  18.   <aop:pointcutid="pc"expression="execution(public * com.xxx..service.*.*(..))"/>
  19.   <aop:advisorpointcut-ref="pc"advice-ref="useTxAdvice"/>
  20. </aop:config>
<!-- 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="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鎖的範圍大於事務控制的範圍。

[html] view plain copy print?
  1. @RequestMapping(value = "applyTicket")  
  2. @ResponseBody  
  3. public void applyTicket(@FromJson ApplyTicketReq applyTicketReq) throws Exception {  
  4.     synchronized (String.valueOf(applyTicketReq.getMemberRoomId()).intern()) {  
  5.         synchronized (String.valueOf(applyTicketReq.getTicketId()).intern()) {  
  6.             service.saveMemberTicket(applyTicketReq);  
  7.         }  
  8.     }  
  9.     responseMessage(ModelResult.CODE_200,ModelResult.SUCCESS);  
  10. }  
@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)儘量減小鎖的程式碼範圍;