全站資源開放下載,感謝廣大網友的支持
鏈接失效請移步職業司平臺
非盈利平臺

非盈利平臺

只為分享一些優質內容

Java幫幫-微信公眾號

Java幫幫-微信公眾號

將分享做到極致

微信小程序

微信小程序

更方便的閱讀

職業司微信公眾號

職業司微信公眾號

實時動態通知

安卓APP

安卓APP

我們從此不分開

程序員生活志-公眾號

程序員生活志-公眾號

程序員生活學習圈,互聯網八卦黑料

支付寶贊助-Java幫幫社區
微信贊助-Java幫幫社區

從構建分布式秒殺系統聊聊Lock鎖使用中的坑

9
發表時間:2018-11-08 13:16來源:Java幫幫-微信公眾號


前言

在單體架構的秒殺活動中,為了減輕DB層的壓力,這里我們采用了Lock鎖來實現秒殺用戶排隊搶購。然而很不幸的是盡管使用了鎖,但是測試過程中仍然會超賣,執行了N多次發現依然有問題。輸出一下代碼吧,可能大家看的比較真切:

@Service("seckillService")publicclassSeckillServiceImplimplementsISeckillService{    /**     * 思考:為什么不用synchronized   * service 默認是單例的,并發下lock只有一個實例     */private Lock lock = new ReentrantLock(true);//互斥鎖 參數默認false,不公平鎖@Autowiredprivate DynamicQuery dynamicQuery;    @Override@Transactionalpublic Result  startSeckilLock(long seckillId, long userId){         try {            lock.lock();            //這里、不清楚為啥、總是會被超賣101、難道鎖不起作用、lock是同一個對象            String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";           Object object =  dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});           Long number =  ((Number) object).longValue();            if(number>0){                nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";                dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});                SuccessKilled killed = new SuccessKilled();                killed.setSeckillId(seckillId);                killed.setUserId(userId);                killed.setState(Short.parseShort(number+""));                killed.setCreateTime(new Timestamp(new Date().getTime()));                dynamicQuery.save(killed);            }else{                return Result.error(SeckillStatEnum.END);            }        } catch (Exception e) {            e.printStackTrace();        }finally {            lock.unlock();        }        return Result.ok(SeckillStatEnum.SUCCESS);    }}

代碼寫在service層,bean默認是單例的,也就是說lock肯定是一個對象。感覺不放心,還是打印一下 lock.hashCode(),輸出結果沒問題。由于還有其他事情要做,最終還是帶著疑問提交代碼到碼云。

追蹤

如果想分享代碼并使大家一起參與進來,一定要自薦,這樣才會被更多的人發現。當然,如果有交流群一定要留下聯系方式,這樣討論起來可能更方便。項目被推薦后,果然加群的小伙伴就多了。由于項目配置好相應參數就可以測試,并且每個點都有相應的文字注釋,其中有心的小伙伴果然注意到了我寫的注釋<這里、不清楚為啥、總是會被超賣101、難道鎖不起作用、lock是同一個對象>,然后提出了困擾自己好多天的問題。

碼友zoain說,測試了好久終于發現了問題,原來lock鎖是在事物單元中執行的。看到這里,小伙伴們有沒有恍然大悟,反正我是悟了。這里,總結一下為什么會超賣101:秒殺開始后,某個事物在未提交之前,鎖已經釋放(事物提交是在整個方法執行完),導致下一個事物讀取到了上個事物未提交的數據,也就是傳說中的臟讀。此處給出的建議是鎖上移,也就是說要包住整個事物單元。

AOP+鎖

為了包住事物單元,這里我們使用AOP切面編程,當然你也可以上移到Control層。

自定義注解Servicelock:

@Target({ElementType.PARAMETER, ElementType.METHOD})    Retention(RetentionPolicy.RUNTIME)    @Documentedpublic@interface Servicelock {      String description()default "";}

自定義切面LockAspect:

@Component@Scope@AspectpublicclassLockAspect{   /**      * 思考:為什么不用synchronized    * service 默認是單例的,并發下lock只有一個實例      */privatestatic  Lock lock = new ReentrantLock(true);//互斥鎖 參數默認false,不公平鎖  //Service層切點     用于記錄錯誤日志@Pointcut("@annotation(com.itstyle.seckill.common.aop.Servicelock)")      publicvoidlockAspect(){            }        @Around("lockAspect()")    public  Object around(ProceedingJoinPoint joinPoint){         lock.lock();        Object obj = null;        try {            obj = joinPoint.proceed();        } catch (Throwable e) {            e.printStackTrace();        } finally{            lock.unlock();        }        return obj;    } }

切入秒殺方法:

@Service("seckillService")publicclassSeckillServiceImplimplementsISeckillService{    /**    * 思考:為什么不用synchronized    * service 默認是單例的,并發下lock只有一個實例    */private Lock lock = new ReentrantLock(true);//互斥鎖 參數默認false,不公平鎖@Autowiredprivate DynamicQuery dynamicQuery;    @Override@Servicelock@Transactionalpublic Result startSeckilAopLock(long seckillId, long userId){        //來自碼云碼友<馬丁的早晨>的建議 使用AOP + 鎖實現        String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";        Object object =  dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});        Long number =  ((Number) object).longValue();        if(number>0){            nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";            dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});            SuccessKilled killed = new SuccessKilled();           killed.setSeckillId(seckillId);           killed.setUserId(userId);           killed.setState(Short.parseShort(number+""));           killed.setCreateTime(new Timestamp(new Date().getTime()));            dynamicQuery.save(killed);        }else{           return Result.error(SeckillStatEnum.END);       }        return Result.ok(SeckillStatEnum.SUCCESS);   }}

所有的工作完成以后,我們來測試一下代碼,意料之中,再也沒有出現超賣的現象。然而,你以為就這么結束了么?細心的碼友IM核米,又提出了以下問題:Spring 里的切片在未指定排序的時候,兩個注解是隨意執行的。如果事務在加鎖前執行的話,是不是就會產生問題?

首先,由于自己實在沒有時間去取證,最終還是碼友IM核米完成了自問自答,這里引用下他的解釋:

我說的沒錯,但 @Transactional 切片是特殊情況

1)多 AOP 之間的執行順序在未指定時是 :undefined ,官方文檔并沒有說一定會按照注解的順序進行執行,只會按照 @ Order 的順序執行。

可參考官方文檔: 可以在頁面里搜索 Command+F「7.2.4.7 Advice ordering」

2)事務切面的 default Order 被設置為了 Ordered.LOWEST_PRECEDENCE,所以默認情況下是屬于最內層的環切。

可參考官方文檔: 可以在頁面里搜索 Command+F「Table 10.2. tx:annotation-driven/ settings」

總結

經驗真的很重要,踩的坑多了也變走成了路
不要吝嗇自己的總結成果,分享交流才能夠促使大家共同進步
最好不要懷疑久經考驗的Lock鎖同志,很有可能是你使用的方式不對



Java幫幫學習群生態

Java幫幫學習群生態

總有一款能幫到你

Java學習群

Java學習群

與大牛一起交流

大數據學習群

大數據學習群

在數據中成長

九點編程學習群

九點編程學習群

深夜九點學編程

python學習群

python學習群

人工智能,爬蟲

測試學習群

測試學習群

感受測試的魅力

Java幫幫生態承諾

Java幫幫生態承諾

一直堅守,不負重望

初心
勤儉
誠信
正義
分享
友鏈交換:加幫主QQ2524138991 留言即可 24小時內答復  
業司
教育資訊
會員登錄
獲取驗證碼
登錄
登錄
我的資料
留言
回到頂部