Để cung cấp một ví dụ cụ thể (vì có một thiếu đáng ngạc nhiên của ví dụ đúng tuyến): đây là làm thế nào để thực hiện một "atomic bank balance transfer" trong CouchDB (chủ yếu là sao chép từ bài viết trên blog của tôi trên cùng một subject: http://blog.codekills.net/2014/03/13/atomic-bank-balance-transfer-with-couchdb/)
Đầu tiên, tóm tắt lại vấn đề: làm thế nào một hệ thống ngân hàng cho phép tiền được chuyển giữa các tài khoản được thiết kế sao cho không có điều kiện nào trong cuộc đua có thể khiến số dư không hợp lệ hoặc vô lý?
Có một vài phần trong vấn đề này:
Đầu tiên: nhật ký giao dịch. Thay vì lưu trữ số dư của tài khoản trong một bản ghi hoặc tài liệu duy nhất - {"account": "Dave", "balance": 100}
- số dư của tài khoản là được tính bằng cách cộng tổng tất cả tín dụng và ghi nợ vào tài khoản đó. Các khoản tín dụng và ghi nợ được lưu trữ trong một nhật ký giao dịch, trong đó có thể trông một cái gì đó như thế này:
{"from": "Dave", "to": "Alex", "amount": 50}
{"from": "Alex", "to": "Jane", "amount": 25}
Và CouchDB đồ-giảm chức năng để tính toán sự cân bằng có thể nhìn một cái gì đó như thế này:
POST /transactions/balances
{
"map": function(txn) {
emit(txn.from, txn.amount * -1);
emit(txn.to, txn.amount);
},
"reduce": function(keys, values) {
return sum(values);
}
}
Để hoàn chỉnh, đây là danh sách các số dư:
GET /transactions/balances
{
"rows": [
{
"key" : "Alex",
"value" : 25
},
{
"key" : "Dave",
"value" : -50
},
{
"key" : "Jane",
"value" : 25
}
],
...
}
Nhưng nghỉ này s câu hỏi rõ ràng: làm thế nào là lỗi xử lý? Điều gì sẽ xảy ra nếu ai đó cố gắng thực hiện chuyển khoản lớn hơn số dư của họ?
Với CouchDB (và các cơ sở dữ liệu tương tự) loại logic nghiệp vụ này và lỗi xử lý phải được triển khai ở cấp ứng dụng. Ngây thơ, một chức năng như vậy có thể trông như thế này:
def transfer(from_acct, to_acct, amount):
txn_id = db.post("transactions", {"from": from_acct, "to": to_acct, "amount": amount})
if db.get("transactions/balances") < 0:
db.delete("transactions/" + txn_id)
raise InsufficientFunds()
Nhưng chú ý rằng nếu ứng dụng bị treo giữa chèn giao dịch và kiểm tra số dư được cập nhật cơ sở dữ liệu sẽ bị bỏ lại trong một tình trạng mâu thuẫn: người gửi có thể bên trái có số dư âm và người nhận có số tiền là trước đây không tồn tại:
// Initial balances: Alex: 25, Jane: 25
db.post("transactions", {"from": "Alex", "To": "Jane", "amount": 50}
// Current balances: Alex: -25, Jane: 75
Làm cách nào để khắc phục?
Để đảm bảo hệ thống là không bao giờ trong tình trạng mâu thuẫn, hai mảnh thông tin cần phải được thêm vào mỗi giao dịch:
Thời gian giao dịch đã được tạo ra (để đảm bảo rằng có một strict total ordering giao dịch) và
Trạng thái - cho dù giao dịch có thành công hay không.
Cũng sẽ cần phải được hai quan điểm - một trong đó trả về có sẵn cân bằng của một tài khoản (ví dụ, tổng của tất cả các giao dịch "thành công"), và một người khác mà trả về "chờ xử lý" giao dịch lâu đời nhất:
POST /transactions/balance-available
{
"map": function(txn) {
if (txn.status == "successful") {
emit(txn.from, txn.amount * -1);
emit(txn.to, txn.amount);
}
},
"reduce": function(keys, values) {
return sum(values);
}
}
POST /transactions/oldest-pending
{
"map": function(txn) {
if (txn.status == "pending") {
emit(txn._id, txn);
}
},
"reduce": function(keys, values) {
var oldest = values[0];
values.forEach(function(txn) {
if (txn.timestamp < oldest) {
oldest = txn;
}
});
return oldest;
}
}
Danh sách chuyển nhượng có thể bây giờ nhìn một cái gì đó như thế này:
{"from": "Alex", "to": "Dave", "amount": 100, "timestamp": 50, "status": "successful"}
{"from": "Dave", "to": "Jane", "amount": 200, "timestamp": 60, "status": "pending"}
Tiếp theo, ứng dụng sẽ cần phải có một chức năng mà có thể giải quyết giao dịch bằng cách kiểm tra mỗi giao dịch đang chờ xử lý để xác minh rằng nó là hợp lệ, sau đó cập nhật tình trạng của nó từ "chờ" cho một trong hai "thành công" hoặc "từ chối":
def resolve_transactions(target_timestamp):
""" Resolves all transactions up to and including the transaction
with timestamp `target_timestamp`. """
while True:
# Get the oldest transaction which is still pending
txn = db.get("transactions/oldest-pending")
if txn.timestamp > target_timestamp:
# Stop once all of the transactions up until the one we're
# interested in have been resolved.
break
# Then check to see if that transaction is valid
if db.get("transactions/available-balance", id=txn.from) >= txn.amount:
status = "successful"
else:
status = "rejected"
# Then update the status of that transaction. Note that CouchDB
# will check the "_rev" field, only performing the update if the
# transaction hasn't already been updated.
txn.status = status
couch.put(txn)
Cuối cùng, mã ứng dụng cho đúng thực hiện chuyển khoản:
def transfer(from_acct, to_acct, amount):
timestamp = time.time()
txn = db.post("transactions", {
"from": from_acct,
"to": to_acct,
"amount": amount,
"status": "pending",
"timestamp": timestamp,
})
resolve_transactions(timestamp)
txn = couch.get("transactions/" + txn._id)
if txn_status == "rejected":
raise InsufficientFunds()
một vài lưu ý:
Vì lợi ích của ngắn gọn, việc triển khai cụ thể này giả định một số lượng nguyên tử trong bản đồ-giảm của CouchDB. Cập nhật mã để nó không dựa trên giả định đó được để lại như một bài tập cho người đọc.
Sao chép chính/bản gốc hoặc đồng bộ hóa tài liệu của CouchDB chưa được xem xét . Bản sao và đồng bộ hóa chính/chủ làm cho vấn đề này khó khăn hơn đáng kể.
Trong hệ thống thực, sử dụng time()
có thể dẫn đến va chạm, do đó, sử dụng thứ gì đó có entropy hơn một chút có thể là một ý tưởng hay; có thể "%s-%s" %(time(), uuid())
hoặc sử dụng tài liệu _id
trong đơn đặt hàng. Bao gồm thời gian là không cần thiết, nhưng nó giúp duy trì hợp lý nếu có nhiều yêu cầu đến cùng một lúc.
CouchDB không phải là khóa-giá trị, đó là cửa hàng tài liệu. – OrangeDog