成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專(zhuān)欄INFORMATION COLUMN

如果連鐵將軍都不再可靠--記一次排查使用分布式輪候鎖+SESSION防訂單重復(fù)仍然加鎖失效問(wèn)題經(jīng)歷

econi / 2187人閱讀

摘要:盡可能地將數(shù)據(jù)寫(xiě)入,例如創(chuàng)建設(shè)置的都會(huì)將數(shù)據(jù)立即的寫(xiě)入再來(lái)看看文檔怎么描述的看看這可愛(ài)的默認(rèn)值我們終于知道了當(dāng)我們不做任何設(shè)置時(shí),默認(rèn)采用的是方式顯而易見(jiàn),使用方式能最大限度的減少與的交互,而在大多數(shù)場(chǎng)景下都是沒(méi)有問(wèn)題的。

0.問(wèn)題背景

此次問(wèn)題源于一次挺嚴(yán)重的生產(chǎn)事故:客戶(hù)的訂單被重復(fù)生成了,而出問(wèn)題的代碼其實(shí)很簡(jiǎn)單:

// ....
redisLockUtil.lock(memberVo.getMember().getId());

String orderTmpId = orderSubmitVo.getRid();

/** 防止表單重復(fù)提交,orderTmpId只能一次有效 */
String rid = (String) request.getSession().getAttribute(GlobalContants.ORDER_TEMP_ID);
if (!Lang.isEmpty(rid) && rid.equals(orderTmpId)) {
    request.getSession().removeAttribute(GlobalContants.ORDER_TEMP_ID);
} else {
    attr.addAttribute("error", errorCode);
    attr.addAttribute("message", "訂單提交數(shù)據(jù)有誤,請(qǐng)不要重復(fù)提交");
    return "redirect:/order/orderSubmitResult";
}
//...

代碼的邏輯很簡(jiǎn)單,首先,通過(guò)redisLockUtil.lock實(shí)現(xiàn)了一個(gè)輪候鎖,每個(gè)用戶(hù)的多次請(qǐng)求是以輪候排隊(duì)形式進(jìn)行處理;其次,通過(guò)預(yù)分配并存入Session的RID,臨時(shí)訂單號(hào)防止重復(fù)提交,一切看上去是多么的健壯啊,怎么會(huì)出問(wèn)題呢!

項(xiàng)目使用了spring-session框架的RedisSession實(shí)現(xiàn)基于Redis的跨應(yīng)用的Session共享
1.初步分析

一開(kāi)始,我們并不能穩(wěn)定的重現(xiàn)問(wèn)題,總是在正常訂單中偶爾的出現(xiàn)一些重復(fù)單,在通過(guò)不斷的嘗試后,終于讓我們發(fā)現(xiàn)了一些規(guī)律:

使用QQ瀏覽器會(huì)極大的提高重現(xiàn)成功率(不要問(wèn)我為什么QQ瀏覽器總會(huì)發(fā)送兩個(gè)時(shí)間間隔極短的請(qǐng)求!ε=( o`ω′)ノ)

當(dāng)程序處理較慢時(shí)容易重現(xiàn)

接下來(lái)我們模擬了連續(xù)發(fā)送重復(fù)請(qǐng)求的場(chǎng)景進(jìn)行了測(cè)試,結(jié)果發(fā)現(xiàn)了一個(gè)有趣的情況,提交兩個(gè)連續(xù)的請(qǐng)求,會(huì)生成兩個(gè)一樣的訂單,而提交三個(gè)連續(xù)請(qǐng)求時(shí)也只會(huì)生成兩個(gè)一樣的訂單,提交4個(gè)請(qǐng)求呢,生成了3個(gè)訂單!而訂單的生成時(shí)間間隔通常都在2s到3s之間,這基本就可以排除輪候鎖的問(wèn)題了,那,難道是rid的判重出問(wèn)題了?
接下來(lái)的測(cè)試我們將主要關(guān)注rid的變化,以下是其中一組數(shù)據(jù)示意:

req1: {SESSION[TEMP_ORDER_ID]: 2018052204911}
req2: {SESSION[TEMP_ORDER_ID]: 2018052204911}
req3: {SESSION[TEMP_ORDER_ID]: null}

等等!session_rid重復(fù)了2次,怎么可能!根據(jù)代碼,在req1處理之后,session中的TEMP_ORDER_ID應(yīng)該立即被remove掉才對(duì)!
于是,我們繼續(xù)關(guān)注這個(gè)rid,發(fā)現(xiàn)存在這樣的詭異情況:

req1、req2在調(diào)用request.getSession().getAttribute(GlobalContants.ORDER_TEMP_ID)后,都可獲取到同一個(gè)rid,而req3為空

req1、req2在調(diào)用完 request.getSession().removeAttribute(GlobalContants.ORDER_TEMP_ID) 后打印Session中的ORDER_TEMP_ID,值為空

req2中可以獲取到req1中本應(yīng)被刪除的rid,而直到處理req3時(shí),SESSION中的TEMO_ORDER_ID才被正確移除!但是,每次removeAttribute后,request.getSession().getAttribute(GlobalContants.ORDER_TEMP_ID)的取值又的確為空!這怎么可能?!
因?yàn)轫?xiàng)目使用了RedisSession實(shí)現(xiàn)Session共享,冷靜下來(lái)的我又去看了看Redis中的數(shù)據(jù),結(jié)果發(fā)現(xiàn),當(dāng)req1調(diào)用完removeAttribute后,Redis上Value里的ORDER_TEMP_ID屬性根本沒(méi)置空,同樣的,也是直到req2處理完畢req3開(kāi)始處理時(shí)才變?yōu)榭?!現(xiàn)在基本可以確定就是removeAttribute沒(méi)有如我們所想的那樣去正確刪除Redis里的值導(dǎo)致了下一請(qǐng)求處理時(shí)仍然能獲取到本應(yīng)被刪除的屬性。

難道是spring-session搞的鬼?跟進(jìn)源碼看看吧...

2.抽絲剝繭

先看看RedisSession里是怎么實(shí)現(xiàn)removeAttribute的:

先在cached中移除待刪除的屬性,然后將detla中的對(duì)應(yīng)屬性至空
嗯....好像也沒(méi)什么問(wèn)題...再看看flushImmediateIfNecessary方法,這個(gè)方法應(yīng)該就是吧detla中保存的屬性寫(xiě)入Redis了吧,至少也是前置的某些步驟吧:

嗯,果然調(diào)用了saveDelta,看名字相當(dāng)直白,就是保存detla,看看具體實(shí)現(xiàn)吧

可見(jiàn),delta就是Session里的內(nèi)容,通過(guò)BoundHashOperations寫(xiě)入Redis,嗯,很Spring,很正路,應(yīng)該也沒(méi)有太多問(wèn)題...
等等,好像哪里不對(duì)
flushImmediateIfNecessary? IfNecessary?!

回顧一下之前看到的代碼,調(diào)用saveDelta前可是有個(gè)判斷的,只有配置了redisFlushMode為RedisFlushMode.IMMEDIATE時(shí)才會(huì)立即將session寫(xiě)入Redis!
那么,問(wèn)題來(lái)了,如果不設(shè)置這個(gè)配置呢?

3.真相大白

來(lái)看看RedisSession提供了什么FlushMode:

可以看到,RedisFlushMode提供了ON_SAVE跟IMMEDIATE兩種方式,根據(jù)這里的注釋?zhuān)@兩個(gè)配置的作用分別是這樣的:

ON_SAVE:  只有當(dāng)SessionRepository.save方法被調(diào)用的時(shí)候才將緩存的Session屬性寫(xiě)入Redis,而在一般的Web項(xiàng)目中,上述方法會(huì)在Http Response被提交的時(shí)候才會(huì)被調(diào)用。
IMMEDIATE: 盡可能地將數(shù)據(jù)寫(xiě)入Redis,例如創(chuàng)建Session、設(shè)置Session的Attribute都會(huì)將數(shù)據(jù)立即的寫(xiě)入Redis

再來(lái)看看API文檔怎么描述的

看看這可愛(ài)的默認(rèn)值!我們終于知道了當(dāng)我們不做任何設(shè)置時(shí),spring-session默認(rèn)采用的是ON_SAVE方式!顯而易見(jiàn),使用ON_SAVE方式能最大限度的減少與Redis的IO交互,而在大多數(shù)場(chǎng)景下都是沒(méi)有問(wèn)題的。然而我們的代碼就恰恰是在第一個(gè)請(qǐng)求還沒(méi)提交,第二個(gè)請(qǐng)求已經(jīng)進(jìn)入到Action方法并獲取Session,此時(shí)緩存中的TEMP_ORDER_ID并沒(méi)有在Redis中被設(shè)置成空,因此導(dǎo)致了這個(gè)幾乎不可能發(fā)生的“Session臟讀”事件!

4. 解決方案

目前我們采取將RedisFlushMode改為IMMEDIATE,修改方法為在@EnableRedisHttpSession注解中指定flushMode:

Configuration
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
public class WebSessionConfig {
    //...
}

如此修改后,在每次調(diào)用removeAttribure后,都能正確的觀察到Redis中相應(yīng)的屬性被置為空,問(wèn)題也就基本得到了解決。

更多的思考

到此,其實(shí)問(wèn)題已經(jīng)解決了,但是還有一個(gè)疑問(wèn):我的輪候鎖是假的么?說(shuō)好的鎖中貴族鐵將軍呢?!怎么還能有重復(fù)的請(qǐng)求進(jìn)來(lái)呢?!
讓我們?cè)俅蔚幕仡櫼幌抡w的代碼,將業(yè)務(wù)代碼去掉,我們的代碼是這樣的:

@RequestMapping(value = {"/orderSubmit", "/orderSubmit.action", "/orderSubmit.html"}, method = RequestMethod.POST)
public String orderSubmit(OrderSubmitVo orderSubmitVo, Map model, HttpServletRequest request, RedirectAttributes attr) {
    MemberVo memberVo = loginService.findMemberVo(request);
    try {
        //同一用戶(hù)排隊(duì)下單
        redisLockUtil.lock(memberVo.getMember().getId());
        String orderTmpId = orderSubmitVo.getRid();
        /** 防止表單重復(fù)提交,orderTmpId只能一次有效 */
        String rid = (String) request.getSession().getAttribute(GlobalContants.ORDER_TEMP_ID);
        if (!Lang.isEmpty(rid) && rid.equals(orderTmpId)) {
            request.getSession().removeAttribute(GlobalContants.ORDER_TEMP_ID);
        } else {
            attr.addAttribute("error", errorCode);
            attr.addAttribute("message", "訂單提交數(shù)據(jù)有誤,請(qǐng)不要重復(fù)提交");
            return "redirect:/order/orderSubmitRe
        }
        // ...balabalabala 這里有很多代碼..
        return "redirect:/order/orderSubmitResult";
    } catch (Exception e) {
        logger.error("提交訂單異常", e);
        attr.addAttribute("error", GlobalContants.CREATE_ORDER_ERROR_NEW_ORDER_FAIL);
    } finally {
        // 釋放鎖
        redisLockUtil.unlock(memberVo.getMember().getId());
    }
    model.put("error", GlobalContants.CREATE_ORDER_ERROR_NEW_ORDER_FAIL);
    return "redirect:/order/orderSubmitResult";
}

簡(jiǎn)而言之,就是這么一個(gè)流程:

獲取鎖 -> 獲取session的rid -> 校驗(yàn)rid是否重復(fù)提交 -> 刪除session的rid -> 業(yè)務(wù)邏輯 -> 釋放鎖

看似很?chē)?yán)謹(jǐn)啊,那問(wèn)題出在哪里呢?回憶一下上文提到的,spring-session在默認(rèn)情況下,是在response被commit后,將數(shù)據(jù)寫(xiě)入Redis。相信到此大家都明白了吧,釋放鎖的操作在respone被commit之前!當(dāng)在較短的間隔內(nèi)有A、B兩個(gè)請(qǐng)求進(jìn)入這個(gè)Action,A獲得鎖進(jìn)行處理,而B(niǎo)在等待A釋放鎖,此時(shí)A處理完了業(yè)務(wù)邏輯但還沒(méi)有提交response鎖就被釋放了!B獲得了鎖并且讀取了A還沒(méi)提交的Session!就好比小明上廁所,屁股還沒(méi)擦水還沒(méi)沖就把門(mén)打開(kāi)了,后面進(jìn)來(lái)的人就當(dāng)然能看到馬桶里aslfkjsdalvijasdvjlsaslvjasdiovjvjsdalvjasdlvjsdvjasdklv哎!我寫(xiě)文章呢lkjaslfjladsjfldfjafl你干嘛!aslfjasldkvjlasdnvlsavjnsljuiewosvnvowijjvsovn

咳咳,大家不要誤會(huì),我的臉絕對(duì)沒(méi)有被摁在鍵盤(pán)上摩擦,OK,這篇分享就先到這,我們有緣再會(huì)!

keywords: spring-session removeAttribute 無(wú)效

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/69472.html

相關(guān)文章

  • 布式冪等問(wèn)題解決方案三部曲

    摘要:解決冪等問(wèn)題的三部曲,也是作者的思考框架。這是解決冪等問(wèn)題的第二部曲列出并減少副作用的分析維度。所以在并發(fā)執(zhí)行的維度,將并發(fā)重復(fù)執(zhí)行變成串行重復(fù)執(zhí)行是最好的冪等解決方案。 綱要 文章目的:本文旨在提煉一套分布式冪等問(wèn)題的思考框架,而非解決某個(gè)具體的分布式冪等問(wèn)題。在這個(gè)框架體系內(nèi),會(huì)有一些方案舉例說(shuō)明。文章目標(biāo):希望讀者能通過(guò)這套思考框架設(shè)計(jì)出符合自己業(yè)務(wù)的完備的冪等解決方案。文章內(nèi)容...

    mumumu 評(píng)論0 收藏0
  • 后端好書(shū)閱讀與推薦(續(xù)三)

    摘要:后端好書(shū)閱讀與推薦系列文章后端好書(shū)閱讀與推薦后端好書(shū)閱讀與推薦續(xù)后端好書(shū)閱讀與推薦續(xù)二后端好書(shū)閱讀與推薦續(xù)三這里依然記錄一下每本書(shū)的亮點(diǎn)與自己讀書(shū)心得和體會(huì),分享并求拍磚。然后又請(qǐng)求封鎖,當(dāng)釋放了上的封鎖之后,系統(tǒng)又批準(zhǔn)了的請(qǐng)求一直等待。 后端好書(shū)閱讀與推薦系列文章:后端好書(shū)閱讀與推薦后端好書(shū)閱讀與推薦(續(xù))后端好書(shū)閱讀與推薦(續(xù)二)后端好書(shū)閱讀與推薦(續(xù)三) 這里依然記錄一下每本書(shū)的...

    ckllj 評(píng)論0 收藏0
  • 后端好書(shū)閱讀與推薦(續(xù)三)

    摘要:后端好書(shū)閱讀與推薦系列文章后端好書(shū)閱讀與推薦后端好書(shū)閱讀與推薦續(xù)后端好書(shū)閱讀與推薦續(xù)二后端好書(shū)閱讀與推薦續(xù)三這里依然記錄一下每本書(shū)的亮點(diǎn)與自己讀書(shū)心得和體會(huì),分享并求拍磚。然后又請(qǐng)求封鎖,當(dāng)釋放了上的封鎖之后,系統(tǒng)又批準(zhǔn)了的請(qǐng)求一直等待。 后端好書(shū)閱讀與推薦系列文章:后端好書(shū)閱讀與推薦后端好書(shū)閱讀與推薦(續(xù))后端好書(shū)閱讀與推薦(續(xù)二)后端好書(shū)閱讀與推薦(續(xù)三) 這里依然記錄一下每本書(shū)的...

    jcc 評(píng)論0 收藏0
  • 后端好書(shū)閱讀與推薦(續(xù)三)

    摘要:后端好書(shū)閱讀與推薦系列文章后端好書(shū)閱讀與推薦后端好書(shū)閱讀與推薦續(xù)后端好書(shū)閱讀與推薦續(xù)二后端好書(shū)閱讀與推薦續(xù)三這里依然記錄一下每本書(shū)的亮點(diǎn)與自己讀書(shū)心得和體會(huì),分享并求拍磚。然后又請(qǐng)求封鎖,當(dāng)釋放了上的封鎖之后,系統(tǒng)又批準(zhǔn)了的請(qǐng)求一直等待。 后端好書(shū)閱讀與推薦系列文章:后端好書(shū)閱讀與推薦后端好書(shū)閱讀與推薦(續(xù))后端好書(shū)閱讀與推薦(續(xù)二)后端好書(shū)閱讀與推薦(續(xù)三) 這里依然記錄一下每本書(shū)的...

    lauren_liuling 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

econi

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<