填坑指南:一次通過Oracle序列自增解決業務編號唯一的併發問題

  • 背景描述

某日上午生產上突然出現應用無法連接數據庫,c3p0錯誤connect time out,重啓應用後依然不見好轉,經DBA檢查發現存在對某張表的for update,以及其他業務操作對該表的update操作,且這些會話均長時間未釋放,查看日誌也發現這些sql語句執行時間有些甚至長達100多秒,後通過DBA 手工刪除會話,釋放連接後系統才恢復正常,導致此事故的是系統的某一個業務功能,但卻因爲這個不當操作導致系統全線業務癱瘓,由於此前該功能已經運行多日,卻未發現異常。

  • 代碼檢查

這裏先貼一段代碼,由於代碼是在前任挖坑離職後,我後面接過來的,大家自行體會。

這段代碼目的是先鎖住整表,然後查出主鍵的maxvalue,然後根據規則對maxvalue進行+1 ,然後進行insert操作,在沒請求量,不對錶做update操作的情況下,單次執行確實是沒什麼問題,但是問題就在這張表是業務表,是會對數據進行操作的,一旦加上存在併發,哪怕這個併發都不是秒級的,這種操作都是撐不住的,關鍵!做這個操作的表還不止一張!其中有幾張還是比較核心的業務表,每個表的主鍵都是通過這種方式來搞,當時的感覺就彷彿吃了一口老八祕製小漢堡。

DBA殺掉會話臨時解決後,下午又出現了同樣的情況,沒辦法,完全修復需要時間,只能先緊急對sql進行修改 select * from table for update wait 3 ,也就是本次for update 若3s未獲得鎖,則直接返回資源忙碌,雖然會造成前端的部分請求失敗,但至少不會導致系統全線崩潰,然後緊急上線,後未發生同樣的情況。

  • 代碼修改

知道了問題,那就定方案吧,第一時間想到的是將序號增加操作表和業務表分開,單獨建立一張序號表,由於業務需求是對序號每日從1開始遞增+1,可以以日期做主鍵,和不同的表類型做主鍵,來進行行級鎖,但是無論是從代碼改動量還是性能上來說,都不是很合適,於是採用了oracle sequence 自增的方案,由於本業務的併發並不高,所以性能上應該是夠了,下面是修改過後的代碼(非正式投產版)。

通過創建序列,每次只需要獲得序列的下一個值,保證了主鍵唯一性,然後再進行業務規則拼接,再對業務表進行插入,代碼寫好了,接下來進行併發性能測試。

  • 測試代碼

由於代碼是有公司框架封裝的,根據思路理解就好

      1.首先創建線程類,執行方法單元測試。

      2.創建線程池,模擬500併發情況下,是否會執行失敗或者序號衝突,接下來開始執行。

      3.控制檯打印

可以看到線程無序執行

500線程併發執行時間不超過3s,由於多個請求在執行序列,出現部分線程執行序列時間在1-2s之間,但考慮到是本地運行測試,所以在可接受範圍,此時查看業務表的數據庫業務編號

可以看到序號從1開始到500結束,count查看條數,500條數據無誤,執行過程中未發生錯誤,基本上可以認爲通過這種方式解決了本次出現的問題。

  • 剩下的問題

代碼層面的問題通過上面我們可以認爲是解決了,但是依然有個問題,業務規則要求業務序號每日從1開始遞增,我們創建的序列卻是一直遞增的,這樣明顯不行;

於是決定通過每日凌晨重置序列(業務在凌晨時不會發生新增)的方式來達到業務需求,具體實現如下:

     一、創建oracle存儲過程

這個存儲過程是指在執行時

1.首先拿到當前的下一個序列號,然後賦值給變量n

2.修改sequence增長數值爲-n

3.再次執行sequence.nextval,此時sequence序列號變爲0;

4.再次修改sequence增長數爲1;

經過這個過程後當執行select sequence.nextval from dual 時,就會從1開始了。

     二、創建oracle自動任務job

存儲過程創建完成後,我們需要讓它能夠自動執行,不可能每天晚上人工執行,於是通過下面的方式讓oralce每晚0點來替我們自動執行存儲過程,在測試驗證時可以先將執行間隔調整爲30s或者更短。

經過驗證發現Oracle自動任務幫我們自動完成了序列每日重置爲1 操作,這樣一來,我們的問題就得到了一個相對完整的解決方案了。

但是轉念一想還是有風險,若今天爲29號當前序號已經到了20,29號 23:59:59:999接受了一筆業務取到當前序號爲21,存儲過程即便自動執行了,但這筆業務在30號00:00:00:001的時候執行了業務表插入操作,序號爲202005300001;這sequence的currval已經是0,當下一筆業務到來時,取到nextval就成了1,那這時就出現了兩筆202005300001,第二筆進行insert肯定會導致主鍵衝突,或者業務錯誤。

這種情況其實是很極端了,第一當前業務產經凌晨基本上不會有數據請求,但是爲了避免風險,還是和業務部門商量在前端頁面增加23:59:30-00:00:30不接受提交的控制,若業務場景是必須保持24小時不間斷服務,而且請求量很大的話,可能本方案就要重新進行設計了

  • 總結

引發本次生產事故的直接原因是對整表,而且是會頻繁update的業務表進行了全表for update,開發時的思考不夠縝密,這是其一;其二是進行代碼審覈時不夠仔細沒有及時發現並提出問題;其三是未對新增業務進行必要的壓力測試;針對for update語句,以後需要明令禁止投入生產,即便是逼不得已的情況下必須使用for update,也要儘可能的指定表中的某一條或少量的數據,並且增加wait條件及時釋放。

填坑不容易,且填且珍惜!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章