最終一致性落地實踐

壹頁書發表於2016-06-04
http://blog.itpub.net/29254281/viewspace-1819422/

單位最近趕工,要做一款直播APP
本來關於後端,我建議了很多設計
包括
1.應用Redis鍵空間/事件通知功能,做兩級快取(Redis,JVM)
2.Redis讀寫分離.(這個很好做,但是單位大部分專案,還是沒有這麼用.主要是訪問量還是低)
3.單執行緒處理併發控制
4.最終一致性分庫設計.
5.系統基於ES彈性搜尋構建,而不是基於資料庫構建.(不涉及事務的查詢,一律走搜尋進行查詢)
6.ELK監控事務補償

最終除了資料庫最終一致性的設計,其餘都被拍了回來...
用之則行,舍之則藏
人不能逆勢而行。。。

最終一致性,無外乎用Translog和Msglog保障使用者的帳號餘額最終一致.
如果不一致則事務補償.
  1. create table account_diamond(  
  2.     UserID bigint primary key comment '使用者ID',  
  3.     DiamondCount int not null default 0 comment '剩餘鑽石數量',  
  4.     State smallint not null default 1 comment '狀態.0表示帳號鎖定.1表示正常',  
  5.     CheckSum varchar(32) not null comment '使用者ID和剩餘數量的校驗',  
  6.     CreateTime timestamp not null default current_timestamp comment '建立時間',  
  7.     UpdateTime timestamp not null default current_timestamp on update current_timestamp comment '更新時間'    
  8. ) comment '帳號鑽石表';  
  9.   
  10.   
  11. create table translog(  
  12.     TransLogID bigint not null,  
  13.     Params Text not null comment '引數資訊',  
  14.     SenderUserID bigint not null comment '傳送方使用者ID',  
  15.     CreateTime timestamp not null default current_timestamp comment '建立時間',  
  16.     CreateYear year not null,  
  17.     key(CreateTime),  
  18.     primary key(TransLogID,CreateYear)  
  19. ) comment '事務表'  
  20. PARTITION BY RANGE(CreateYear)  
  21. (  
  22. PARTITION p2016 VALUES LESS THAN (year('2016-01-01')),  
  23. PARTITION p2017 VALUES LESS THAN (year('2017-01-01')),  
  24. PARTITION p2018 VALUES LESS THAN (year('2018-01-01')),  
  25. PARTITION p2019 VALUES LESS THAN (year('2019-01-01')),  
  26. PARTITION p2020 VALUES LESS THAN (year('2020-01-01'))  
  27. );  
  28.   
  29.   
  30. create table msglog(  
  31.     TransLogID bigint not null,  
  32.     Params Text not null comment '引數資訊',  
  33.     ReceiverUserID bigint not null,  
  34.     CreateTime timestamp not null default current_timestamp comment '建立時間',  
  35.     CreateYear year not null,  
  36.     primary key(TransLogID,ReceiverUserID,CreateYear)  
  37. ) comment '訊息表'  
  38. PARTITION BY RANGE(CreateYear)  
  39. (  
  40. PARTITION p2016 VALUES LESS THAN (year('2016-01-01')),  
  41. PARTITION p2017 VALUES LESS THAN (year('2017-01-01')),  
  42. PARTITION p2018 VALUES LESS THAN (year('2018-01-01')),  
  43. PARTITION p2019 VALUES LESS THAN (year('2019-01-01')),  
  44. PARTITION p2020 VALUES LESS THAN (year('2020-01-01'))  
  45. );  
  46.    

因為時間短,我在原來分庫的架構上,做了一些簡化.
原來的設計,是有佇列的.
我開始用全域性的TransLogID mod 3 寫日誌.
如果成功,則分為兩個Callable(傳送方增加餘額的任務和接收方扣減金額的任務)掛到執行緒池佇列中處理.
最後,更新Redis傳送方的快取.

後來發現,這個結構無法獲取傳送方真實的帳號餘額.因為有一部分應該扣減的金額,還線上程池佇列中等待處理.
當時開會討論的時候,領導們居然沒人注意到這個問題,透過了這個方案.
這個問題如果不能處理,就有點丟人現眼了.

第一版補救..原來按照TransLogID mod 3 找到資料庫,寫Translog表
那麼現在改為 傳送方ID mod 3 找到資料庫,寫Translog表.
這樣,最終處理的時候,傳送方的帳號表,Msglog表和Translog表都會落在同一個資料庫上.
如果給Translog表,增加一個狀態欄位,表明這個事務正在處理 還是 已經完成.
傳送方的扣減過程,以更新translog的狀態,作為事務完成的標誌.
這樣傳送方的事務完成標誌由translog記錄,就和msglog沒有關係了.
msglog僅僅記錄接收方的情況.最終用於比對
這樣,統計傳送方餘額的時候,我可以鎖定傳送方的帳號,和正在處理的Translog記錄,就可以得到使用者的真實餘額。
translog中未處理的記錄,就是線上程池佇列中已經扣減的金額

第二版補救
後來我分析了一下場景
檢查帳號餘額需要一個當前讀的select,鎖定使用者的帳號資訊和未處理的translog資訊.
寫translog 需要一個Insert

傳送方任務處理的時候,
一個update更新帳號餘額,記錄兩個業務日誌
update translog狀態.

這個過程,幾個行鎖,三個insert,兩個update,一個select一共6個SQL

與其這樣,不如改為同步扣減傳送方帳號,非同步處理接收方資訊.
傳送方的扣減過程如下
1.呼叫check_diamondaccount_exist
檢查鑽石帳號是否存在
是否有足夠鑽石餘額
帳號是否被鎖定
資料是否被篡改
2.如果透過第一步的檢查,扣減傳送方的鑽石帳號
3.寫入事務日誌表,呼叫insert_translog.並且提交資料庫事務
4.JAVA組成一個非同步任務,用於增加接收方的票帳號
5.更新Redis 傳送方的鑽石帳號餘額

一次當前讀,鎖定一行使用者鑽石帳號記錄
兩次Insert業務日誌
一次update,修改帳號餘額
一次Insert translog 記錄

相對於之前的設計,少了一個SQL,並且還少了記錄的鎖定,translog不用鎖定讀了.

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29254281/viewspace-2113781/,如需轉載,請註明出處,否則將追究法律責任。

相關文章