摘要:盡可能地將數據寫入,例如創建設置的都會將數據立即的寫入再來看看文檔怎么描述的看看這可愛的默認值我們終于知道了當我們不做任何設置時,默認采用的是方式顯而易見,使用方式能最大限度的減少與的交互,而在大多數場景下都是沒有問題的。
0.問題背景
此次問題源于一次挺嚴重的生產事故:客戶的訂單被重復生成了,而出問題的代碼其實很簡單:
// .... redisLockUtil.lock(memberVo.getMember().getId()); String orderTmpId = orderSubmitVo.getRid(); /** 防止表單重復提交,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", "訂單提交數據有誤,請不要重復提交"); return "redirect:/order/orderSubmitResult"; } //...
代碼的邏輯很簡單,首先,通過redisLockUtil.lock實現了一個輪候鎖,每個用戶的多次請求是以輪候排隊形式進行處理;其次,通過預分配并存入Session的RID,臨時訂單號防止重復提交,一切看上去是多么的健壯啊,怎么會出問題呢!
項目使用了spring-session框架的RedisSession實現基于Redis的跨應用的Session共享1.初步分析
一開始,我們并不能穩定的重現問題,總是在正常訂單中偶爾的出現一些重復單,在通過不斷的嘗試后,終于讓我們發現了一些規律:
使用QQ瀏覽器會極大的提高重現成功率(不要問我為什么QQ瀏覽器總會發送兩個時間間隔極短的請求!ε=( o`ω′)ノ)
當程序處理較慢時容易重現
接下來我們模擬了連續發送重復請求的場景進行了測試,結果發現了一個有趣的情況,提交兩個連續的請求,會生成兩個一樣的訂單,而提交三個連續請求時也只會生成兩個一樣的訂單,提交4個請求呢,生成了3個訂單!而訂單的生成時間間隔通常都在2s到3s之間,這基本就可以排除輪候鎖的問題了,那,難道是rid的判重出問題了?
接下來的測試我們將主要關注rid的變化,以下是其中一組數據示意:
req1: {SESSION[TEMP_ORDER_ID]: 2018052204911}
req2: {SESSION[TEMP_ORDER_ID]: 2018052204911}
req3: {SESSION[TEMP_ORDER_ID]: null}
等等!session_rid重復了2次,怎么可能!根據代碼,在req1處理之后,session中的TEMP_ORDER_ID應該立即被remove掉才對!
于是,我們繼續關注這個rid,發現存在這樣的詭異情況:
req1、req2在調用request.getSession().getAttribute(GlobalContants.ORDER_TEMP_ID)后,都可獲取到同一個rid,而req3為空
req1、req2在調用完 request.getSession().removeAttribute(GlobalContants.ORDER_TEMP_ID) 后打印Session中的ORDER_TEMP_ID,值為空
req2中可以獲取到req1中本應被刪除的rid,而直到處理req3時,SESSION中的TEMO_ORDER_ID才被正確移除!但是,每次removeAttribute后,request.getSession().getAttribute(GlobalContants.ORDER_TEMP_ID)的取值又的確為空!這怎么可能?!
因為項目使用了RedisSession實現Session共享,冷靜下來的我又去看了看Redis中的數據,結果發現,當req1調用完removeAttribute后,Redis上Value里的ORDER_TEMP_ID屬性根本沒置空,同樣的,也是直到req2處理完畢req3開始處理時才變為空!現在基本可以確定就是removeAttribute沒有如我們所想的那樣去正確刪除Redis里的值導致了下一請求處理時仍然能獲取到本應被刪除的屬性。
難道是spring-session搞的鬼?跟進源碼看看吧...
2.抽絲剝繭先看看RedisSession里是怎么實現removeAttribute的:
先在cached中移除待刪除的屬性,然后將detla中的對應屬性至空
嗯....好像也沒什么問題...再看看flushImmediateIfNecessary方法,這個方法應該就是吧detla中保存的屬性寫入Redis了吧,至少也是前置的某些步驟吧:
嗯,果然調用了saveDelta,看名字相當直白,就是保存detla,看看具體實現吧
可見,delta就是Session里的內容,通過BoundHashOperations寫入Redis,嗯,很Spring,很正路,應該也沒有太多問題...
等等,好像哪里不對
flushImmediateIfNecessary? IfNecessary?!
回顧一下之前看到的代碼,調用saveDelta前可是有個判斷的,只有配置了redisFlushMode為RedisFlushMode.IMMEDIATE時才會立即將session寫入Redis!
那么,問題來了,如果不設置這個配置呢?
來看看RedisSession提供了什么FlushMode:
可以看到,RedisFlushMode提供了ON_SAVE跟IMMEDIATE兩種方式,根據這里的注釋,這兩個配置的作用分別是這樣的:
ON_SAVE: 只有當SessionRepository.save方法被調用的時候才將緩存的Session屬性寫入Redis,而在一般的Web項目中,上述方法會在Http Response被提交的時候才會被調用。
IMMEDIATE: 盡可能地將數據寫入Redis,例如創建Session、設置Session的Attribute都會將數據立即的寫入Redis
再來看看API文檔怎么描述的
看看這可愛的默認值!我們終于知道了當我們不做任何設置時,spring-session默認采用的是ON_SAVE方式!顯而易見,使用ON_SAVE方式能最大限度的減少與Redis的IO交互,而在大多數場景下都是沒有問題的。然而我們的代碼就恰恰是在第一個請求還沒提交,第二個請求已經進入到Action方法并獲取Session,此時緩存中的TEMP_ORDER_ID并沒有在Redis中被設置成空,因此導致了這個幾乎不可能發生的“Session臟讀”事件!
目前我們采取將RedisFlushMode改為IMMEDIATE,修改方法為在@EnableRedisHttpSession注解中指定flushMode:
Configuration @EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE) public class WebSessionConfig { //... }
如此修改后,在每次調用removeAttribure后,都能正確的觀察到Redis中相應的屬性被置為空,問題也就基本得到了解決。
更多的思考到此,其實問題已經解決了,但是還有一個疑問:我的輪候鎖是假的么?說好的鎖中貴族鐵將軍呢?!怎么還能有重復的請求進來呢?!
讓我們再次的回顧一下整體的代碼,將業務代碼去掉,我們的代碼是這樣的:
@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 { //同一用戶排隊下單 redisLockUtil.lock(memberVo.getMember().getId()); String orderTmpId = orderSubmitVo.getRid(); /** 防止表單重復提交,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", "訂單提交數據有誤,請不要重復提交"); 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"; }
簡而言之,就是這么一個流程:
獲取鎖 -> 獲取session的rid -> 校驗rid是否重復提交 -> 刪除session的rid -> 業務邏輯 -> 釋放鎖
看似很嚴謹啊,那問題出在哪里呢?回憶一下上文提到的,spring-session在默認情況下,是在response被commit后,將數據寫入Redis。相信到此大家都明白了吧,釋放鎖的操作在respone被commit之前!當在較短的間隔內有A、B兩個請求進入這個Action,A獲得鎖進行處理,而B在等待A釋放鎖,此時A處理完了業務邏輯但還沒有提交response鎖就被釋放了!B獲得了鎖并且讀取了A還沒提交的Session!就好比小明上廁所,屁股還沒擦水還沒沖就把門打開了,后面進來的人就當然能看到馬桶里aslfkjsdalvijasdvjlsaslvjasdiovjvjsdalvjasdlvjsdvjasdklv哎!我寫文章呢lkjaslfjladsjfldfjafl你干嘛!aslfjasldkvjlasdnvlsavjnsljuiewosvnvowijjvsovn
咳咳,大家不要誤會,我的臉絕對沒有被摁在鍵盤上摩擦,OK,這篇分享就先到這,我們有緣再會!
keywords: spring-session removeAttribute 無效
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/69472.html
摘要:解決冪等問題的三部曲,也是作者的思考框架。這是解決冪等問題的第二部曲列出并減少副作用的分析維度。所以在并發執行的維度,將并發重復執行變成串行重復執行是最好的冪等解決方案。 綱要 文章目的:本文旨在提煉一套分布式冪等問題的思考框架,而非解決某個具體的分布式冪等問題。在這個框架體系內,會有一些方案舉例說明。文章目標:希望讀者能通過這套思考框架設計出符合自己業務的完備的冪等解決方案。文章內容...
摘要:后端好書閱讀與推薦系列文章后端好書閱讀與推薦后端好書閱讀與推薦續后端好書閱讀與推薦續二后端好書閱讀與推薦續三這里依然記錄一下每本書的亮點與自己讀書心得和體會,分享并求拍磚。然后又請求封鎖,當釋放了上的封鎖之后,系統又批準了的請求一直等待。 后端好書閱讀與推薦系列文章:后端好書閱讀與推薦后端好書閱讀與推薦(續)后端好書閱讀與推薦(續二)后端好書閱讀與推薦(續三) 這里依然記錄一下每本書的...
摘要:后端好書閱讀與推薦系列文章后端好書閱讀與推薦后端好書閱讀與推薦續后端好書閱讀與推薦續二后端好書閱讀與推薦續三這里依然記錄一下每本書的亮點與自己讀書心得和體會,分享并求拍磚。然后又請求封鎖,當釋放了上的封鎖之后,系統又批準了的請求一直等待。 后端好書閱讀與推薦系列文章:后端好書閱讀與推薦后端好書閱讀與推薦(續)后端好書閱讀與推薦(續二)后端好書閱讀與推薦(續三) 這里依然記錄一下每本書的...
摘要:后端好書閱讀與推薦系列文章后端好書閱讀與推薦后端好書閱讀與推薦續后端好書閱讀與推薦續二后端好書閱讀與推薦續三這里依然記錄一下每本書的亮點與自己讀書心得和體會,分享并求拍磚。然后又請求封鎖,當釋放了上的封鎖之后,系統又批準了的請求一直等待。 后端好書閱讀與推薦系列文章:后端好書閱讀與推薦后端好書閱讀與推薦(續)后端好書閱讀與推薦(續二)后端好書閱讀與推薦(續三) 這里依然記錄一下每本書的...
閱讀 1689·2023-04-25 20:16
閱讀 3849·2021-10-09 09:54
閱讀 2702·2021-09-04 16:40
閱讀 2521·2019-08-30 15:55
閱讀 837·2019-08-29 12:37
閱讀 2741·2019-08-26 13:55
閱讀 2911·2019-08-26 11:42
閱讀 3151·2019-08-23 18:26