先來說說事務與回滾
在資料庫中,事務(Transaction)是指對資料庫的一系列操作,要不就全部成功執行,要不就全部失敗的一種機制。其中若是在事務操作中,遇到錯誤或是條件不滿足時,我們會撤銷已經執行的所有更改,並將資料恢復到事務開始之前的狀態,這樣的行為稱做回滾(rollback)。
以實際的應用來舉例:
- 銀行轉賬:從一個帳戶轉賬到另一個帳戶,需要確保:① 從轉出帳戶扣除金額 ② 往轉入增加金額。這兩件事情,要不就兩個操作都成功,要不就兩者都不發生任何更改,方能保證存款人的資金安全。
- 訂單系統:在電商平台或售票系統下單時,涉及以下操作:① 庫存扣減 ② 生成訂單 ③ 更新客戶帳戶。這一系列的操作完成,才能確保庫存與訂單資料正確,不會兜不上。
在 Django 中,可能會這樣去寫:
from django.db import transaction
from myapp.models import Account
def transfer_funds(from_account_id, to_account_id, amount):
try:
with transaction.atomic():
from_account = Account.objects.select_for_update().get(id=from_account_id)
to_account = Account.objects.select_for_update().get(id=to_account_id)
# 檢查餘額是否足夠
if from_account.balance < amount:
raise ValueError("Insufficient funds")
# 扣除轉出帳戶金額
from_account.balance -= amount
from_account.save()
# 增加轉入帳戶金額
to_account.balance += amount
to_account.save()
except Exception as e:
print("Transaction failed:", e)
再來聊聊資料庫的 ACID 特性
資料庫管理系統中,在寫入或更新資料的過程中,為了保證事務(transaction)是正確可靠的,必須具備下述的四種特性,我們一般會取這四個特性的首字母,縮寫為 ACID 特性:
- 原子性(Atomicity)
事務中的所有操作要不就全部執行,要不就全部不執行。如果事務中途出現錯誤,所有已完成的操作將被回滾(rollback)到事務開始前的狀態。 - 一致性(Consistency)
事務執行完成後,資料必須保持符合規則應使資料庫從一種狀態轉換到另一種狀態。換句話說,事務在完成前後的資料庫狀態必須符合所有的業務規則和約束條件。 - 隔離性(Isolation)
在多個事務並行(concurrency)執行時,不同事務之間的操作不應互相干擾。在資料庫層面會使用以下方式來實現:- 透過設定不同的隔離層級(isolation level)來控制事務之間資料的可見性與訪問性。
- 資料庫引擎透過鎖(lock)機制來阻止其他事務讀取或修改未提交的資料。
- 持久性(Durability)
一旦事務提交(commit)出去,其對資料庫的修改將永久保存,即使發生系統崩潰或斷電等情況。資料庫透過預寫日誌記錄(WAL)和事務備份來實現持久性。
那些並行事務引發的那些困擾
讓我們先來看看在現實生活中,可能會遇到什麼狀況?
- 銀行系統:多個使用者可能會同時訪問同一個帳戶記錄,如一個使用者在提款時,另一個使用者查詢帳戶餘額。
- 訂單系統:在搶購活動中,多個使用者同時下單購買同一件商品,需要確保庫存不會被超賣。
不同的使用者在上述狀況中,可能同時會觸發不同的事務(transaction)。而所謂的並行事務,便是指多個事務在同一資料庫中同時被執行,由於多個事務之間可能需要訪問或修改相同的一筆資料,因此會使得彼此之間相互影響,從而引發下述常見的並行問題。
事務並行問題:髒讀(Dirty Read)
髒讀(Dirty Read)是指一個事務讀取了另一個事務尚未提交(un-commit)的資料,如果該筆資料被修改或回滾,前一個事務就會得到不一致或錯誤的資料。
舉例:兩位銀行職員,在同一時間處理同一個帳戶的相關交易:事務 A 是將帳戶餘額從 1000 元修改為 800 元,這個修改尚未提交。同時,事務 B 查詢該帳戶餘額並顯示為 800 元。此時如果事務 A 回滾,則餘額應該回到 1000 元,但事務 B 顯示的餘額卻是不正確的,因為它讀取了未提交的髒資料。
事務並行問題:不可重複讀(Non-repeatable Read)
不可重複讀(Non-repeatable Read)是指在同一事務中多次讀取相同數據時,由於其他事務的修改導致讀到的數據值不一致。例如,事務 A 第一次讀取某數據後,事務 B 修改了該數據並提交,事務 A 再次讀取該數據時會發現值已變化。
舉例:一個訂單管理系統中,客戶正在查看自己訂單的狀態(事務 A),並在該事務中多次查詢訂單的狀態;此時,另一個管理員修改了該訂單的狀態(事務 B)。當客戶再次查詢時,發現訂單狀態已經改變,前後不一致,這就是不可重複讀的現象。
事務並行問題:幻讀(Phantom Read)
幻讀(Phantom Read)是指一個事務在某些條件下會讀取多筆資料,當再次讀取時,由於其他事務在過程中插入或刪除了符合條件的數據,導致讀取結果的資料條目數量不一致。
舉例:公司正在生成一份銷售報告,事務 A 查詢所有銷售資料的記錄以計算總銷售額。同時,事務 B 新增了一筆銷售訂單並提交。當事務 A 再次查詢時,結果中多了一筆訂單數據,導致總銷售額發生變化。這就是幻讀的情況。
事務並行問題:更新丟失(Lost Update)
更新丟失(Lost Update)是指當兩個事務同時更新相同的數據時,後提交的事務會覆蓋先提交事務的更新,導致數據變化被覆蓋。
舉例:兩個用戶同時編輯公司網站上的某一篇文章。事務 A 先修改了文章內容並保存,但尚未提交。此時事務 B 也修改了相同的文章內容並提交。最後,事務 A 提交其修改後,覆蓋了事務 B 的更改,使得 B 的編輯被丟失。
處理競爭與衝突的手段:隔離與上鎖
我們不難看出,引致這些事務並行的問題原因,主要是多個事務對共享數據的同時訪問。在資料庫中,會使用以下手段或機制,處理事務並行時的競爭與衝突問題。
隔離層級(Isolation Levels)
資料庫可以通過設置事務的隔離層級(Isolation Levels),來控制不同事務之間的可見性和訪問性。根據不同需求,隔離級別從低到高依次為:
- 未提交讀取(Read Uncommitted)
允許事務讀取其他事務尚未提交的資料,可能導致「髒讀」。亦即基本上不做任何隔離,通常只用於不嚴格要求一致性的情況,適用於不重要或臨時的資料讀取操作,例如:日誌分析。 - 已提交讀取(Read Committed)
僅允許事務讀取已提交的資料,可以避免髒讀,但可能出現「不可重複讀」問題。適用於需要避免讀到過時資料的系統,比如:銀行的餘額查詢,確保讀到的資料是其他事務提交後的結果。 - 可重複讀取(Repeatable Read):保證事務中多次讀取的結果一致,避免髒讀和不可重複讀,但可能出現「幻讀」。適合電商系統,確保使用者讀取的商品價格和庫存量一致,避免多次讀取時資料發生變化。
- 可被順序化(Serializable):最高的隔離級別,將並行事務予以順序化,可以防止所有的並行問題,但會因為鎖定大量資料而降低性能。適用於銀行的轉賬操作等高一致性要求的場景。
from django.http import JsonResponse
from myapp.models import Product
def update_product_stock(request, product_id, quantity):
# 設置事務隔離級別為 REPEATABLE READ
with connection.cursor() as cursor:
cursor.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;")
try:
with transaction.atomic():
# 鎖定行以防止競爭條件
product = Product.objects.select_for_update().get(id=product_id)
# 檢查庫存
if product.stock >= quantity:
product.stock -= quantity
product.save()
return JsonResponse({'status': 'success', 'message': 'Stock updated successfully'})
else:
return JsonResponse({'status': 'failed', 'message': 'Insufficient stock'})
except Product.DoesNotExist:
return JsonResponse({'status': 'failed', 'message': 'Product not found'})
鎖機制(Locks)
資料庫中的鎖機制(Locks),可以用來確保多個事務同時訪問相同資料時的資料一致性和完整性。能夠有效地在事務並行時,防止讀寫衝突和競爭條件問題。
鎖的分類會根據視角的不同而有所不同,以 MySQL 關聯式資料庫為例:
- 依使用方式,可以分成共享鎖(Share Lock)、排他鎖(Exclusive Lock)
- 依行為模式,可以分為悲觀鎖(Pessimistic Lock)、樂觀鎖(Optimistic Lock)
共享鎖與排他鎖
共享鎖(Share lock)又稱為讀取鎖(Read Lock),是指當資源被鎖定時,允許其它事務讀取該資源,也允許其它事務從該資源上再次獲取共享鎖,但不能對該資源進行寫入。
START TRANSACTION;
SELECT * FROM employees
WHERE department = 'IT'
LOCK IN SHARE MODE;
-- Perform operations like data analysis or reporting on the selected rows
COMMIT;
LOCK TABLES employees READ;
-- Perform read-only operations on the table
SELECT * FROM employees;
UNLOCK TABLES;
排他鎖(Exclusive Lock)又稱為寫入鎖(Write Lock),主要是防止其它事務和當前加鎖事務鎖定同一資源。亦即當排他鎖加在指定資源上時,其它事務無法對該資源進行 INSERT
、UPDATE
、DELETE
、ALTER
和 DROP
等操作。
START TRANSACTION;
SELECT * FROM employees
WHERE department = 'HR'
FOR UPDATE;
-- Perform update operations on the selected rows
UPDATE employees SET salary = salary + 500 WHERE department = 'HR';
COMMIT;
LOCK TABLES employees WRITE;
-- Perform operations such as inserts, updates, or deletes
UPDATE employees SET salary = salary + 500 WHERE department = 'HR';
UNLOCK TABLES;
悲觀鎖與樂觀鎖
悲觀鎖(Pessimistic Lock)假設資料會經常發生衝突,因此在訪問資源時,會先進行加鎖以確保資料不會被別的事務修改。
- 通常會使用資料庫的鎖機制來實現。
- 適用於高併發且數據經常被修改的場景,例如:銀行轉賬、訂單管理等。
樂觀鎖(Optimistic Lock)假設資料很少會發生衝突,因此在訪問資源時不進行加鎖,只有在更新時才會資料檢查是否被其他事務修改過,如果發現衝突則回滾當前操作並重新嘗試。
- 通常會使用版本編號或時間戳記來實現,例如添加
version
或timestamp
欄位作為比對用途。 - 適用於讀多寫少、數據衝突較少的情況,例如:社交媒體中的瀏覽計數、產品庫存查看等。