1 問題描述
最近有小夥伴在做商品抽獎活動時,在對獎品庫存進行扣減,有線程安全的問題,遂加鎖synchronized進行同步,
但發現加鎖後并沒有控制住庫存線程安全的問題,導緻庫存仍被超發。
先簡單介紹下,各層的技術架構:
中間層架構:Spring 4.1.0
持久層: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,産生庫存扣減超發問題】
2 排查問題
排查問題開始之前,簡單說下自己排查問題的幾個原則(僅供參考):
問題重制:一定要先重制問題,任何重制不了的問題,都不是問題。同理,任何存在的問題,都必然能再次重制。
由近及遠:先确認自己的代碼無問題,然後再去确認外部代碼無問題(如:架構代碼,第三方代碼等)。
由外到内:程式就是一個IPO,有輸入Input(如:參數、環境等)也有輸出Out(如:結果、異常等),輸出Out是問題的表象,先确定外部因素Input無問題,再确認程式代碼邏輯無問題。
由淺入深:其實就是由易到難、自上向下,先從上層應用排查問題,如:上層API、應用層、HTTP傳輸等,然後再确認底層應用排查問題,如:底層API、網絡層、系統層、位元組碼、JVM等;
确定synchronized關鍵字是否起作用;
【建議:盡量慎用synchronized關鍵字,非常影響程式性能】根據多線程并發測試,
可以确認多線程之間是同步執行synchronized代碼塊,确認synchronized同步執行沒問題。
确定Spring事務是否送出成功;檢視Spring 事務配置:
<!-- 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鎖的範圍大于事務控制的範圍。
@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)盡量減小鎖的代碼範圍;