SQL 設(shè)計模式 | 關(guān)系型數(shù)據(jù)庫的冪等性處理
在 IT 的很多術(shù)語中,正向解釋非常難,反向描述反而更容易懂。冪等性處理就是這類。
舉兩個數(shù)據(jù)處理時,非冪等性常見的場景:
1.在創(chuàng)建訂單時,偶有因網(wǎng)絡(luò)抖動,癡呆,掉線等因素,造成客戶端與服務(wù)器之間通訊不暢。比如,客戶端發(fā)起請求后,在約定時間內(nèi)(通常 30秒),沒有得到服務(wù)器的反饋,導(dǎo)致重復(fù)發(fā)起創(chuàng)建訂單的請求,實(shí)際上前面看似失敗的訂單已創(chuàng)建成功,最終造成創(chuàng)建兩個甚至多個同樣的訂單
2.重復(fù)扣款,扣庫存。這個是最不能容忍的。如前所述,客戶端重新不斷發(fā)起扣款、扣庫存的請求,會導(dǎo)致賬目混亂。
由此可見,做好程序的冪等性處理,非常重要!
很多教科書,會籠統(tǒng)的說,冪等性處理是一種最終返回結(jié)果一致的程序處理。這么講,不完美。冪等性處理,不僅對結(jié)果有約束,對處理造成的負(fù)面影響也有約束。
來看關(guān)系型數(shù)據(jù)庫的 DML 的冪等性處理。在庫存管理軟件中,對同一批貨物操作增刪改,就可能帶來負(fù)面影響。
比如在蘋果門店的倉庫管理軟件中,某天門店客流量非常大,操作庫存也比平時頻繁了很多。這樣一來,給庫存管理就帶來了風(fēng)險。
比如某臺結(jié)算終端,就因?yàn)樵L問人數(shù)過多,經(jīng)常掉線,超時。小王好不容易賣出去兩臺,結(jié)果死活就是結(jié)賬不成功,連續(xù)操作4,5次后無果后,小王叫店長來重啟了電腦。
等重啟后,結(jié)算是成功了,但庫存為 0 了。店長跑去倉庫一看,10 臺 iPhone 13 都好好躺在那里,為什么庫存為 0 了呢?
這就是非冪等性處理造成的??蛻舳税l(fā)起交易后,網(wǎng)絡(luò)堵塞,結(jié)賬請求一直沒發(fā)成功。等計算機(jī)重啟后,連續(xù)將之前的訂單,重復(fù)發(fā)送了 10次,結(jié)果庫存全扣沒了。
看下庫存表的設(shè)計:
create table ProductInventory(
ProductLotId INT,
ProductName VARCHAR(200),
ProductInventoryVolume INT )
iPhone 13 庫存是這樣的:
ProductLotId ProductName ProductInventoryVolume
A0001 iPhone13 10
更新程序也挺簡單:
UPDATE ProductInventory
SET ProductInventoryVolume = ProductInventoryVolume - 1
WHERE ProductLotId = 'A0001'
由此可見,是連續(xù)的交易請求,讓庫存清 0 了。
于是,第一種冪等性處理方法就來了 - UUID 通用唯一標(biāo)識符:
CREATE TABLE ProductSalesTransactionAudit(
AuditId BIGINT,
RequestUUID UniqueIdentifier,
RequestCompleted BIT )
在每次請求中,加入一個 RequestUUID(Universally Unique Identifier,通用唯一標(biāo)識符, Java/C#/Python 等編程語言均有實(shí)現(xiàn) UUID 的庫)
在數(shù)據(jù)庫端維護(hù)一張表 ProductSalesTransactionAudit,若有請求被數(shù)據(jù)庫接收到,先去該表查詢是否存在.
若存在且 RequestCompleted 為1,就表示該請求被數(shù)據(jù)庫正確處理過,可以跳過這次處理,并將 RequestCompleted 返回給客戶端;沒有,則在這表里插入一行,且把數(shù)據(jù)庫的處理結(jié)果,更新到 RequestCompleted.
這樣,一個可行的冪等性處理,就完成了。但不是十分完美,因?yàn)樵摫頂?shù)據(jù)量,會顯著性增長,造成性能緩慢。
于是,要尋找下一種冪等性處理方案。
接下來再看這個例子,依舊是以蘋果這家門店為例。
某天倉庫中剩余 10只 iPhone 13. 小王和小黃同時銷售出去 2只,理論上剩下 6只。按照正常操作,小王和小黃在操作庫存時,同時看到有 10只,每人減去 2只,剩余 8只,由于看不到對方的操作,因此顯示 8只剩余時,兩個人都沒覺得庫存錯了。
create table ProductInventory(
ProductLotId INT,
ProductName VARCHAR(200),
ProductInventoryVolume INT )
小王和小黃,同時查詢 iPhone 的庫存時,是這樣:
ProductLotId ProductName ProductInventoryVolume
A0001 iPhone 13 10
他倆抓取后,經(jīng)過他倆各自的本地計算(網(wǎng)頁端或手持設(shè)備),變成了這樣:
ProductLotId ProductName ProductInventoryVolume
A0001 iPhone 13 8
當(dāng)他們把本地數(shù)據(jù)上傳時,無論誰先,數(shù)據(jù)庫最終的 iPhone 13 的存量,都成了 8. 但事實(shí)上,錯的離譜,店長要罵娘!
那么平時我們設(shè)計系統(tǒng)時,該怎么處理這種意料中的錯誤呢,這里涉及到事務(wù)管理的技巧。
有一種樂觀派做法是,在庫存表上,加一列,標(biāo)識行的版本。當(dāng)本行數(shù)據(jù)更新時,首先對比這個版本列,若相同,則更新,若不同,則報 ”您修改的數(shù)據(jù),已被其他人搶先更新,請確定后再次保存“ 的提示,最后標(biāo)識列會被自動更新。
接下來,實(shí)現(xiàn)上面這種版本控制的做法:
create table ProductInventory(
ProductLotId INT,
ProductName VARCHAR(200),
ProductInventoryVolume INT,
ProductLotTS timestamp)
原庫存是這樣:
ProductLotId ProductName ProductInventoryVolume ProductLotTS
A0001 iPhone 13 10 2022050114364700001
他倆抓取后,經(jīng)過各自的本地計算,變成了這樣:
ProductLotId ProductName ProductInventoryVolume ProductLotTS
A0001 iPhone 13 8 2022050114364700001
當(dāng)小王上傳數(shù)據(jù)時,程序會同時以 A0001 + 2022050114364700001 作為更新條件,先將 ProductInventoryVolume 更新成8,同時因 timestamp 是系統(tǒng)自動更新的對象,已經(jīng)變成了 2022050114364700002 .
等到小黃再更新,程序也同樣同時以 A0001 + 2022050114364700001 作為更新條件,發(fā)現(xiàn) ProductLotTS 已經(jīng)改變了,意味著在讀取數(shù)據(jù)后,有別人先一步做了更新,此時小黃更新庫存就會失敗。他必須重新讀取數(shù)據(jù)后,再操作。
只要一次更新成功,ProductLotTS 就會改變,即使相同的請求再發(fā)送一遍,也會因?yàn)?ProductLotTS 不匹配,導(dǎo)致失敗!
這就是第二種冪等性處理程序,不僅僅做了防重復(fù)處理,還能省去一張表的維護(hù)代價。