2012-04-15 31 views
6

Hãy giúp tôi tìm hiểu sự hiểu lầm của mình.App Engine, giao dịch và idempotency

Tôi đang viết một RPG trên App Engine. Một số hành động mà người chơi sẽ sử dụng một số liệu thống kê nhất định. Nếu chỉ số đạt đến 0 thì người chơi không thể thực hiện thêm hành động nào. Tôi bắt đầu lo lắng về người chơi gian lận, mặc dù - nếu một người chơi gửi hai hành động rất nhanh, ngay bên cạnh nhau? Nếu mã giảm số liệu thống kê không có trong giao dịch, thì người chơi có cơ hội thực hiện hành động hai lần. Vì vậy, tôi nên quấn mã mà giảm số liệu trong một giao dịch, phải không? Càng xa càng tốt.

Trong GAE Python, tuy nhiên, chúng tôi có điều này trong documentation:

Note: Nếu ứng dụng của bạn nhận được một ngoại lệ khi nộp một giao dịch, nó không luôn có nghĩa là giao dịch thất bại. Bạn có thể nhận được Timeout, TransactionFailedError hoặc ngoại lệ InternalError trong trường hợp giao dịch đã được cam kết và cuối cùng sẽ áp dụng thành công . Bất cứ khi nào có thể, hãy thực hiện các giao dịch Datastore của bạn một cách không cần thiết để rằng nếu bạn lặp lại một giao dịch, kết quả cuối cùng sẽ giống nhau.

Rất tiếc. Điều đó có nghĩa là chức năng tôi đang chạy có dạng như sau:


def decrement(player_key, value=5): 
    player = Player.get(player_key) 
    player.stat -= value 
    player.put() 

Vâng, điều đó không hiệu quả vì điều này không phải là không đáng tin cậy, phải không? Nếu tôi đặt một vòng lặp thử lại xung quanh nó (tôi cần phải trong Python? Tôi đã đọc mà tôi không cần phải trên SO ... nhưng tôi không thể tìm thấy nó trong tài liệu) nó có thể tăng giá trị hai lần, đúng? Kể từ khi mã của tôi có thể bắt một ngoại lệ nhưng kho dữ liệu vẫn cam kết dữ liệu ... huh? Làm thế nào để sửa lỗi này? Đây có phải là trường hợp tôi cần distributed transactions không? Tôi có thực sự?

+1

Vâng, vâng, và đó là một điểm tốt ... nhưng trước khi tôi vứt bỏ mã của mình với một loạt khó chẩn đoán, khó khăn sinh sản lỗi Tôi muốn tìm hiểu những gì tôi nên đi mô hình ở đây. –

+0

Mẫu của bạn đang đi đúng hướng, nhưng GAE có một số sắc thái khó chịu khiến cho việc triển khai chính xác khó khăn như thế này. Theo kinh nghiệm của tôi với GAE, đôi khi nó có giá trị nỗ lực, và đôi khi nó không. –

+1

@TravisWebb Không đồng ý. An toàn giao dịch không phải là 'tối ưu hóa sớm', cũng không phải là xung đột giao dịch đặc biệt là không thể xảy ra. –

Trả lời

13

Trước tiên, câu trả lời của Nick không chính xác. Giao dịch của DHayes không phải là idempotent, vì vậy nếu nó chạy nhiều lần (ví dụ: thử lại khi cố gắng đầu tiên được cho là đã thất bại, khi nó không), thì giá trị sẽ bị giảm đi nhiều lần. Nick nói rằng "kho dữ liệu sẽ kiểm tra nếu các thực thể đã được sửa đổi vì chúng đã được tìm nạp", nhưng điều đó không ngăn được sự cố vì hai giao dịch có các lần tìm nạp riêng biệt và lần tìm nạp thứ hai là SAU giao dịch đầu tiên hoàn thành.

Để giải quyết vấn đề, bạn có thể thực hiện idempotent giao dịch bằng cách tạo "khóa giao dịch" và ghi lại khóa đó trong một thực thể mới như là một phần của giao dịch. Giao dịch thứ hai có thể kiểm tra khóa giao dịch đó, và nếu được tìm thấy, sẽ không làm gì cả. Khóa giao dịch có thể bị xóa khi bạn đã hài lòng rằng giao dịch đã hoàn tất hoặc bạn từ bỏ việc thử lại.

Tôi muốn biết những gì "cực kỳ hiếm" có nghĩa là cho AppEngine (1-trong-một triệu, hoặc 1-trong-một tỷ?), Nhưng lời khuyên của tôi là giao dịch không cần thiết là cần thiết cho các vấn đề tài chính , nhưng không phải cho điểm số trò chơi hoặc thậm chí là "sống" ;-)

1

Bạn không nên lưu trữ loại thông tin này trong Memcache, nhanh hơn nhiều so với Datastore (thứ bạn cần nếu stat này được sử dụng trong ứng dụng của bạn). Memcache cung cấp cho bạn một chức năng tốt đẹp: decr trong đó:

Giảm một cách nguyên tắc giá trị của khóa. Bên trong, giá trị là số nguyên 64 bit không dấu. Memcache không kiểm tra luồng tràn 64 bit. Giá trị, nếu quá lớn, sẽ quấn quanh.

Tìm kiếm decrhere. Sau đó, bạn nên sử dụng một nhiệm vụ để lưu giá trị trong khóa này vào kho dữ liệu hoặc mỗi x giây hoặc khi một điều kiện nhất định được đáp ứng.

+0

Cảm ơn câu trả lời, nhưng tôi không nghĩ rằng nó sẽ hoạt động. Nếu đây là một loại truy cập toàn cầu mà tôi có thể đủ khả năng cho các giá trị mờ cho điều đó sẽ là hoàn hảo, nhưng tôi tưởng tượng người chơi sẽ khá điên nếu giá trị bị sai lầm vì bị đuổi khỏi memcache. –

+0

Lưu ý rằng chức năng decc() của Memcache cũng có "mũ giảm dần từ 0 đến 0" [\ [1 \]] (https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api. memcache # google.appengine.api.memcache.Client.decr). – Lee

1

Nếu bạn suy nghĩ kỹ về những gì bạn mô tả, nó có thể không thực sự là một vấn đề. Hãy suy nghĩ về điều này theo cách này:

Bạn có một điểm stat còn lại. Sau đó, anh ta gửi hai hành động (A1 và A2) ngay lập tức mà mỗi cái cần phải tiêu thụ điểm đó. Cả A1 và A2 đều giao dịch.

Đây là những gì có thể xảy ra:

A1 thành công. A2 sau đó sẽ hủy bỏ. Tất cả đều tốt.

A1 không hợp pháp (không thay đổi dữ liệu). Thử lại theo lịch. A2 sau đó cố gắng, thành công. Khi A1 cố gắng một lần nữa, nó sẽ hủy bỏ.

A1 thành công nhưng báo cáo lỗi. Thử lại theo lịch. Lần sau khi A1 hoặc A2 cố gắng, họ sẽ hủy bỏ.

Để làm việc này, bạn cần phải theo dõi xem liệu A1 và A2 đã hoàn thành - có thể cung cấp cho họ một nhiệm vụ UUID và lưu trữ danh sách các tác vụ đã hoàn thành không? Hoặc thậm chí chỉ cần sử dụng hàng đợi nhiệm vụ.

+0

Cảm ơn, nhưng tôi không chắc chắn rằng các công trình, kể từ khi lưu trữ ID chính nó có thể chạy vào vấn đề này ... nhưng tôi nghĩ rằng câu trả lời của Nick ở trên bao gồm trường hợp của tôi. –

4

Chỉnh sửa: Điều này không chính xác - vui lòng xem các nhận xét.

Mã của bạn vẫn ổn. Các idempotence các tài liệu tham khảo là liên quan đến tác dụng phụ. Theo các tài liệu giải thích, chức năng giao dịch của bạn có thể được chạy nhiều lần; trong những tình huống như vậy nếu hàm có bất kỳ tác dụng phụ nào, chúng sẽ được áp dụng nhiều lần. Vì chức năng giao dịch của bạn không làm điều đó, nó sẽ ổn thôi.

Một ví dụ về một hàm có vấn đề liên quan đến idempotence sẽ là một cái gì đó như thế này:

def do_something(self): 
    def _tx(): 
    # Do something transactional 
    self.counter += 1 
    db.run_in_transaction(_tx) 

Trong trường hợp này, self.counter có thể được tăng thêm 1, hoặc có khả năng hơn 1. Điều này có thể tránh được bằng cách thực hiện các tác dụng phụ bên ngoài giao dịch:

def do_something(self): 
    def _tx(): 
    # Do something transactional 
    return 1 
    self.counter += db.run_in_transaction(_tx) 
+0

Cảm ơn, Nick.Bạn đang nói rằng bất kỳ hoạt động kho dữ liệu nào tôi thực hiện trong một giao dịch sẽ chỉ xảy ra một lần, ngay cả khi mã của tôi có ngoại lệ và thử lại nó? Nếu giao dịch 'decrement' của tôi bị gọi hai lần do thử lại, nó sẽ chỉ giảm một lần? Trong ndb-đất, là bởi vì giao dịch của tôi được gán một số ID mà kho dữ liệu biết nó đã cam kết chưa? –

+0

(và bởi "mã của tôi ... retries" Tôi có nghĩa là "ndb retries cho tôi") –

+1

@ D.Hayes Họ sẽ xảy ra nhiều lần, nhưng chỉ một trong số họ sẽ nhận được cam kết trở lại kho dữ liệu. Kho dữ liệu sử dụng đồng thời lạc quan, vì vậy khi nó cố gắng thực hiện một giao dịch, kho dữ liệu sẽ kiểm tra xem các thực thể đã được sửa đổi hay chưa và chúng chỉ chấp nhận giao dịch nếu chúng chưa được thực hiện. –