2009-07-07 34 views
17

Vui lòng giải thích bất kỳ sai sót nào trong thuật ngữ. Đặc biệt, tôi đang sử dụng các thuật ngữ cơ sở dữ liệu quan hệ.Giao dịch nguyên tử trong các cửa hàng có giá trị quan trọng

Có một số cửa hàng khóa-giá trị liên tục, bao gồm CouchDBCassandra, cùng với nhiều dự án khác.

Một đối số tiêu biểu chống lại họ là họ thường không cho phép giao dịch nguyên tử trên nhiều hàng hoặc bảng. Tôi tự hỏi nếu có một cách tiếp cận chung sẽ giải quyết vấn đề này.

Lấy ví dụ về tình huống của một tập hợp tài khoản ngân hàng. Làm cách nào để chúng tôi chuyển tiền từ tài khoản ngân hàng này sang tài khoản ngân hàng khác? Nếu mỗi tài khoản ngân hàng là một hàng, chúng tôi muốn cập nhật hai hàng là một phần của cùng một giao dịch, giảm giá trị trong một và tăng giá trị trong một hàng khác.

Một cách tiếp cận rõ ràng là có bảng riêng mô tả giao dịch. Sau đó, chuyển tiền từ tài khoản ngân hàng này sang tài khoản khác bao gồm việc chỉ cần chèn một hàng mới vào bảng này. Chúng tôi không lưu trữ số dư hiện tại của một trong hai tài khoản ngân hàng và thay vào đó dựa vào tổng hợp tất cả các hàng thích hợp trong bảng giao dịch. Nó rất dễ dàng để tưởng tượng rằng điều này sẽ được quá nhiều công việc, tuy nhiên; một ngân hàng có thể có hàng triệu giao dịch mỗi ngày và một tài khoản ngân hàng cá nhân có thể nhanh chóng có vài nghìn 'giao dịch' liên kết với nó.

Một số (tất cả?) Của các cửa hàng giá trị khóa sẽ 'quay lại' một hành động nếu dữ liệu cơ bản đã thay đổi kể từ lần cuối bạn nắm lấy nó. Có thể điều này có thể được sử dụng để mô phỏng các giao dịch nguyên tử, sau đó, như bạn có thể chỉ ra rằng một trường cụ thể bị khóa. Có một số vấn đề rõ ràng với cách tiếp cận này.

Bất kỳ ý tưởng nào khác? Hoàn toàn có thể là cách tiếp cận của tôi đơn giản là không chính xác và tôi chưa bao bọc bộ não của mình xung quanh cách suy nghĩ mới.

+0

CouchDB không phải là khóa-giá trị, đó là cửa hàng tài liệu. – OrangeDog

Trả lời

10

Nếu, lấy ví dụ của bạn, bạn muốn cập nhật nguyên tử giá trị trong một tài liệu đơn (hàng trong thuật ngữ quan hệ), bạn có thể làm như vậy trong CouchDB. Bạn sẽ nhận được một lỗi xung đột khi bạn cố gắng cam kết thay đổi nếu một ứng dụng khách khác cạnh tranh đã cập nhật cùng một tài liệu kể từ khi bạn đọc nó. Sau đó, bạn sẽ phải đọc giá trị mới, cập nhật và thử lại cam kết. Có một số không xác định (có thể là vô hạn nếu có số lượng của ganh đua), nhưng bạn được đảm bảo có tài liệu trong cơ sở dữ liệu với số dư cập nhật nguyên tử nếu cam kết của bạn thành công.

Nếu bạn cần cập nhật hai số dư (nghĩa là chuyển khoản từ tài khoản này sang tài khoản khác), bạn cần sử dụng tài liệu giao dịch riêng biệt (bảng khác có hiệu quả giao dịch hàng) lưu trữ số tiền và hai tài khoản (trong và ngoài). Đây là một thực hành kế toán phổ biến, bằng cách này. Vì CouchDB chỉ tính toán các lượt xem khi cần thiết, thực sự vẫn rất hiệu quả để tính toán số tiền hiện tại trong một tài khoản từ các giao dịch liệt kê tài khoản đó. Trong CouchDB, bạn sẽ sử dụng một hàm bản đồ phát ra số tài khoản là khóa và số tiền của giao dịch (tích cực cho đến, âm cho đi). Hàm reduce của bạn sẽ chỉ tổng hợp các giá trị cho mỗi khóa, phát ra cùng một khóa và tổng số tiền. Sau đó, bạn có thể sử dụng chế độ xem với nhóm = True để nhận số dư tài khoản, được khóa bằng số tài khoản.

+0

Cảm ơn bạn đã giải thích điều này. Bạn nói rằng đó là "vẫn còn rất hiệu quả" để làm một nhóm. Bạn có thể giải thích về điều này một chút không? Đối với cơ sở dữ liệu quan hệ lưu lượng truy cập cao, thực tế phổ biến là làm biến đổi một cột. Tôi có thể tưởng tượng rằng CouchDB và những người khác lưu trữ dữ liệu một cách đáng kể khác nhau và điều này có nghĩa là việc nhóm các giao dịch có thể hiệu quả hơn. Nhưng bạn sẽ làm điều này với 10 giao dịch để nhóm? 100? 100.000? – ChrisInEdmonton

+0

CouchDB sử dụng một mô hình Map/Reduce để xem các tài liệu trong cơ sở dữ liệu. Vì bản đồ chỉ áp dụng cho các tài liệu đã thay đổi, hiệu quả (thời gian) của nó về cơ bản là O (1) trong tổng số tài liệu, nhưng O (n) trong số tài liệu đã thay đổi. Giá trị giảm được tính toán và lưu trữ trong một cây b. Rõ ràng tất cả các nút có tài liệu con được thay đổi sẽ cần phải được tính toán lại. Do đó có thể mất nhiều thời gian hơn để chạy giảm. CouchDB đã được chứng minh trong sản xuất với hàng triệu tài liệu vì vậy tôi không nghĩ rằng đó sẽ là một vấn đề trong trường hợp này. –

+0

Cảm ơn bạn. Nhân tiện, tôi làm việc cho một trang mạng xã hội. Chúng tôi không có kế hoạch chuyển sang kho khóa-giá trị liên tục trong tương lai trung bình. Chúng tôi sử dụng máy chủ cơ sở dữ liệu MySQL bị phân mảnh và memcache, tất nhiên. Có vẻ như các bàn bận rộn của chúng tôi đã thấy hàng trăm triệu hàng nhưng không có gì ngoài đó. Từ câu trả lời của bạn, có vẻ như CouchDB, ít nhất, đã được thiết kế đặc biệt để xử lý các loại vấn đề mà tôi mong đợi sẽ xuất hiện cho một trang web giống như chúng ta. Không quá ngạc nhiên nhưng vẫn tốt để nghe. Tôi chắc chắn CouchDB và những người khác sẽ làm một số điều tốt hơn và điều thường xuyên tồi tệ hơn. – ChrisInEdmonton

5

CouchDB không phù hợp với các hệ thống giao dịch vì nó không hỗ trợ các hoạt động khóa và nguyên tử.

Để hoàn thành một chuyển khoản ngân hàng, bạn phải làm một vài điều:

  1. Validate giao dịch, đảm bảo có đủ tiền trong tài khoản nguồn, mà cả hai tài khoản được mở, không bị khóa, và tốt đứng, và vân vân
  2. Giảm số dư của tài khoản nguồn
  3. Tăng số dư tài khoản đích

Nếu thay đổi được thực hiện ở giữa bất kỳ các bước này là số dư hoặc trạng thái của các tài khoản, giao dịch có thể trở thành không hợp lệ sau khi được gửi, đây là một vấn đề lớn trong hệ thống loại này.

Ngay cả khi bạn sử dụng cách tiếp cận được đề xuất ở trên, nơi bạn chèn bản ghi "chuyển" và sử dụng chế độ xem bản đồ/giảm để tính số dư tài khoản cuối cùng, bạn không có cách nào đảm bảo rằng bạn không rút quá nhiều tài khoản vẫn còn một điều kiện chủng tộc giữa việc kiểm tra số dư tài khoản nguồn và chèn giao dịch mà hai giao dịch có thể đồng thời được thêm sau khi kiểm tra số dư.

Vì vậy, ... đó là công cụ sai cho công việc. CouchDB có lẽ là tốt ở rất nhiều thứ, nhưng đây là một cái gì đó mà nó thực sự không thể làm được.

EDIT: Có thể đáng lưu ý rằng các ngân hàng thực tế trong thế giới thực sử dụng tính nhất quán cuối cùng. Nếu bạn rút tiền quá nhiều vào tài khoản ngân hàng của mình, bạn sẽ bị tính phí thấu chi. Nếu bạn rất giỏi, bạn thậm chí có thể rút tiền từ hai máy ATM khác nhau tại cùng một thời điểm và rút tiền từ tài khoản của bạn vì có điều kiện chủng tộc để kiểm tra số dư, phát hành tiền và ghi lại giao dịch. Khi bạn gửi séc vào tài khoản của mình, họ sẽ thực hiện số dư nhưng thực sự giữ các khoản tiền đó trong một khoảng thời gian "chỉ trong trường hợp" tài khoản nguồn thực sự không có đủ tiền.

+1

Đây là giả mạo sai: https://gist.github.com/wolever/1940301d4f7f530c0791 - nó chỉ đơn giản sử dụng mô hình giao dịch * khác nhau * (mặc dù phức tạp hơn đáng kể). Mặc dù ứng dụng loại "chuyển khoản ngân hàng" không yêu cầu các hoạt động nguyên tử (trong đó ghế dài có: cập nhật tài liệu bằng kiểm tra phiên bản), không cần khóa. –

2

Để 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:

  1. 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à

  2. 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.

1

BerkeleyDB và LMDB là cả hai cửa hàng giá trị khóa có hỗ trợ cho giao dịch ACID. Trong BDB txns là tùy chọn trong khi LMDB chỉ hoạt động giao dịch.

1

Đối số điển hình chống lại chúng là chúng thường không cho phép giao dịch nguyên tử trên nhiều hàng hoặc bảng. Tôi tự hỏi nếu có một cách tiếp cận chung sẽ giải quyết vấn đề này.

Rất nhiều kho lưu trữ dữ liệu hiện đại không hỗ trợ cập nhật multi-key nguyên tử (giao dịch), nhưng hầu hết chúng cung cấp nguyên thủy cho phép bạn tạo các giao dịch ACID phía máy khách.

Nếu một kho lưu trữ dữ liệu hỗ trợ cho mỗi tuyến tính chính và hoạt động so sánh và hoán đổi hoặc kiểm tra và đặt thì nó đủ để thực hiện các giao dịch có thể tuần tự. Ví dụ, cách tiếp cận này được sử dụng trong Google's Percolator và trong cơ sở dữ liệu CockroachDB.

Trong blog của mình, tôi đã tạo step-by-step visualization of serializable cross shard client-side transactions, mô tả các trường hợp sử dụng chính và cung cấp liên kết đến các biến thể của thuật toán. Tôi hy vọng nó sẽ giúp bạn hiểu làm thế nào để thực hiện chúng cho bạn lưu trữ dữ liệu.

Trong số các lưu trữ dữ liệu có hỗ trợ mỗi linearizability chủ chốt và CAS là:

  • Cassandra với các giao dịch nhẹ
  • Riak với xô phù
  • RethinkDB
  • Zookeeper
  • Etdc
  • HBase
  • DynamoDB
  • MongoDB

Bằng cách này, nếu bạn đang sử dụng tốt với Read Cam mức cô lập sau đó nó làm cho tinh thần để có một cái nhìn vào RAMP transactions Peter Bailis. Chúng cũng có thể được triển khai cho cùng một tập hợp các kho dữ liệu.

Các vấn đề liên quan