在事件溯源的敘事里,系統鼓吹的“不可變性”總是聽起來很完美:你不更新記錄、不刪除記錄,只管向后面追加新事件,于是歷史日志就成了永久性的鐵證。然而只要往底層看一眼,就會發現這多半只是一種約定,而不是物理約束。事件流里的每條記錄之所以不可變,本質上是因為所有訪問這段數據的代碼都同意不去碰它,并非有什么機制真的能阻止修改。在存儲層,這些事件不過是 PostgreSQL 里的一行行普通記錄,而任何擁有寫入權限的數據庫管理員都可以直接碰那些行。一次“清理舊數據”的遷移腳本、一條凌晨兩點誤連到主庫的手動查詢、一份還原時出現了極輕微字節差異的備份,都能在不產生任何告警的情況下改掉事件內容。
更麻煩的是,改動其中一行之后,整個系統的重放過程仍然會走得四平八穩。聚合根照常重建,投影也照常重建,一切看起來都好好的。通常第一個察覺到異樣的人是某個發現賬戶余額不對勁的顧客,可到了那個時候,追查線索早就涼透了。光靠追加操作本身,并不足以阻止篡改被發現前已悄悄滲入所有后續計算。要讓事件流擁有真正的完整性,需要在每個事件之間建立起一種鏈式依賴。
方法并不復雜:給每一行增加兩個額外的列——一個存放當前行內容的哈希值,另一個存放前一行的哈希值。這樣一來,事件流就變成了一個前后咬合的鏈條。比如第一條“賬戶已開立”記錄,其前一哈希指針指向一串零值,內容哈希則由負載數據計算得出;第二條“存款事件”的前一哈希指向前一條的內容哈希,自身內容哈希又基于該指針和本次負載共同算出;第三條“取款事件”同樣依此鏈接下去。這里用到的哈希算法不過是 SHA-256,把“上一行的哈希”與“當前負載的 JSON 序列化字節”拼接在一起后做一次散列,毫無花哨。真正厲害的地方在于,每個哈希都依賴于前一個哈希,所以你一旦改動了某條記錄的負載,它的內容哈希就不再匹配;如果你想把這個內容哈希也改掉以掩蓋篡改,那么下一條記錄的前一哈希指針就會立刻斷裂。修好一處,必然弄壞下一處,無從掩飾。
追加新事件時,代碼也只是把新舊數據串起來再求一次哈希,大概四十行就能實現。在 C# 風格的片段里,首次追加時把前一哈希設為約定的“創世哈希”(全零),然后調用 ComputeHash 方法,將前一哈希的字節數組與當前負載的 JSON 序列化字節拼接,交給 SHA256 計算,最后把序號、負載、前一指針和內容哈希封裝起來存入列表。驗證過程則正好是倒著走一遍:遍歷所有記錄,對每條記錄檢查它的前一哈希是否確實等于上一條記錄計算出的內容哈希,再重新對當前負載執行一次哈希計算,看是否與存儲的內容哈希一致。任何一步失敗,都立即拋出損壞異常,并指出具體序列號。
設想有人直接跑到數據庫里把 Alice 第 2 號存款事件中的 50 美元改成 5000 美元,那么在驗證走到第 2 條時,重新計算的哈希必然與存儲值不同,程序會在這一行精準停住,并拋出一句明確的提示:“事件流篡改在序列 2 處被檢測到:存儲的哈希與負載重新散列的結果不匹配(負載在提交后被修改)”。這個機制不依賴任何外部審計系統,也不要求額外的保護區,只靠事件流自身的內在結構就能判定歷史是否曾被污染。
整個方案的價值還不只是防住主動篡改。由于每一行的哈希都融入了上一行的指紋,哪怕是無意間的靜默損壞——比如存儲層出現位翻轉,或者備份恢復時導致的細微偏差——同樣會觸發驗證失敗。它對所有寫入者,包括擁有最高權限的數據庫管理員,都施加了同一種不可繞過的約束。在事件溯源中,“只追加”本來只是一個約定,而加上這條哈希鏈之后,它才真正變成一道物理性質的護城河。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.