最近的一個(gè)業(yè)務(wù)系統(tǒng),需要實(shí)現(xiàn)一套操作日志的記錄方式。這篇日志算是對(duì)各種實(shí)現(xiàn)的一個(gè)思考翩剪。
一 每個(gè)操作記錄流水
這種方式非常適合每一步操作都非常重要的系統(tǒng)。比如常見的金融操作系統(tǒng)饶氏。在一般的場(chǎng)景下,我們只會(huì)對(duì)涉及金錢的操作記錄這種流水日志有勾,比如轉(zhuǎn)賬疹启,扣款等。
在設(shè)計(jì)賬戶流水表的時(shí)候蔼卡,需要有以下幾個(gè)必須的字段:
(1) instruction_id 流水號(hào)
(2) amount 金額
(3) operation_type 操作類型(轉(zhuǎn)賬or扣款)
(4) before_balance 操作前賬戶余額
(5) after_balance 操作后賬戶余額
(6) time 操作時(shí)間
第一個(gè)字段喊崖,流水號(hào),是主鍵雇逞。這個(gè)字段的必要性在于荤懂,它能維護(hù)該記錄修改的冪等性。簡(jiǎn)單的實(shí)現(xiàn)塘砸,流水號(hào)可以使用uuid节仿。
當(dāng)前端發(fā)起一個(gè)充值的操作時(shí),首先在前端產(chǎn)生一個(gè)流水號(hào)掉蔬,然后將其他的用戶填寫的參數(shù)通過(guò)rpc請(qǐng)求發(fā)送到后端廊宪。后端的處理流程如下:
(1) 開啟事務(wù)
(2) 從db中查詢賬戶余額
(3) 修改賬戶余額
(4) 記錄流水
(5) 提交事務(wù)
假設(shè)在執(zhí)行過(guò)程中,出現(xiàn)了網(wǎng)絡(luò)問題女轿,該執(zhí)行結(jié)果處于未知狀態(tài)箭启,那么前端可以通過(guò)剛才的uuid反復(fù)重試,直到得到一個(gè)確定的結(jié)果為止蛉迹。
我們可以對(duì)流水表稍做改進(jìn)傅寡,以適應(yīng)普遍的情況。字段如下:
(1) instruction_id 流水號(hào)(在對(duì)可靠性要求不太高的場(chǎng)景下婿禽,可使用數(shù)據(jù)庫(kù)自增id)
(2) operation_type 操作類型
(3) before (一個(gè)json赏僧,描述修改前的該行記錄)
(4) after (json, 描述修改后的該行記錄)
(5) time
還有一些可選的字段大猛,比如 input_param(輸入?yún)?shù))扭倾,操作人等等。
這種基于流水的方式優(yōu)點(diǎn)有:
1 可靠性很高
2 實(shí)現(xiàn)方式確定挽绩,可以基于接口做注解實(shí)現(xiàn)
3 可以基于該流水膛壹,實(shí)現(xiàn)業(yè)務(wù)層的操作回滾
但是它的缺點(diǎn)也很明顯:
1 增加開發(fā)量,需要額外增加記錄寫入
2 降低運(yùn)行效率,需要多一到兩次查詢(before, after)
3 和業(yè)務(wù)綁定的比較緊
二 基于數(shù)據(jù)庫(kù)的binlog實(shí)現(xiàn)
第二種方式是基于mysql的binlog來(lái)實(shí)現(xiàn)模聋。這種方式最大的好處是可以和業(yè)務(wù)完全的解耦肩民,而且統(tǒng)計(jì)的結(jié)果是完全準(zhǔn)確的(相比于有些完全基于業(yè)務(wù)代碼的實(shí)現(xiàn)來(lái)說(shuō),這類實(shí)現(xiàn)一般是對(duì)執(zhí)行前后查詢兩次)链方。
關(guān)于binlog的定義及獲取持痰,可以參考
binlog可以描述為下面這種model:
@Data
public class RowDiffModel {
long timestamp;
String tableName;
List<String> pkColumnName = new ArrayList<>(); //主鍵列
List<Object> pk = new ArrayList<>();
int type; //1 新建 //2 更新 //3 刪除
List<String> diffColumns = new ArrayList<>();
Map<String, Object> preValue = new HashMap<>();
Map<String, Object> newValue = new HashMap<>();
}
基于binlog的實(shí)現(xiàn)還有一個(gè)小問題。如何獲得用戶的操作id祟蚀?在一些涉及權(quán)限授權(quán)及權(quán)限分離的系統(tǒng)中工窍,操作id非常重要。但由于這個(gè)字段完全隸屬于業(yè)務(wù)層前酿,和數(shù)據(jù)庫(kù)的設(shè)計(jì)關(guān)聯(lián)度并不大患雏。
為了解決這個(gè)問題,我們需要在每個(gè)涉及用戶操作的表中增加一個(gè)新的字段operator_id罢维。
還有一種情況是淹仑,用戶的某一步操作涉及多個(gè)表的修改,這個(gè)時(shí)候可以按事務(wù)id將binlog聚集起來(lái)肺孵。
之后匀借,我們需要將binlog轉(zhuǎn)換為一條操作記錄,記錄到庫(kù)里平窘,操作記錄的字段如下:
(1) operator_id (操作人id)
(2) operation_type (操作類型怀吻,可根據(jù)表及binlog類型(insert,update初婆,delete)確定)
(3) before (一個(gè)json蓬坡,描述本行記錄修改前的值)
(4) after (描述本行記錄修改后的值)
(5) timestamp (時(shí)間戳)
現(xiàn)在,用戶操作日志的生成及查詢磅叛,整體流程如下:
1 用戶操作修改表
2 開啟異步服務(wù)獲取binlog
3 根據(jù)事務(wù)id將binlog做一定的聚合處理
4 將binlog轉(zhuǎn)換為一條原始的操作日志屑咳,并記錄到庫(kù)里
5 當(dāng)用戶查詢操作日志時(shí),根據(jù)條件檢索操作日志
6 將操作日志轉(zhuǎn)換為一個(gè)用戶可讀的view返回