2009-06-23 28 views
29

Đây là một ví dụ đơn giản của một cái nhìn django với một điều kiện chủng tộc tiềm năng:điều kiện Race trong django

# myapp/views.py 
from django.contrib.auth.models import User 
from my_libs import calculate_points 

def add_points(request): 
    user = request.user 
    user.points += calculate_points(user) 
    user.save() 

Các điều kiện chủng tộc nên khá rõ ràng: Người dùng có thể thực hiện yêu cầu này hai lần, và các ứng dụng có thể có khả năng thực hiện đồng thời user = request.user, khiến một trong các yêu cầu ghi đè lên đối tượng kia.

Giả sử hàm calculate_points tương đối phức tạp và tính toán dựa trên tất cả các loại công cụ lạ không thể đặt trong một đơn update và sẽ khó đặt trong quy trình được lưu trữ.

Vì vậy, đây là câu hỏi của tôi: Những loại cơ chế khóa có sẵn để django, để đối phó với các tình huống tương tự như thế này?

+0

Mở đầu tiên vượt qua, có vẻ như bạn cần cơ sở dữ liệu cấp khóa trên hàng trong câu hỏi vào thời điểm đó. Tôi sẽ tham khảo tài liệu SQL cho cơ sở dữ liệu của bạn và gửi một truy vấn tùy chỉnh để thực hiện nó. –

+1

Tôi thích giải pháp "cơ sở dữ liệu bất khả tri" nếu nó có thể thực hiện được. – Fragsworth

+1

'@ transaction.commit_on_success' +' QuerySet.select_for_update() ' – orokusaki

Trả lời

38

Django 1.4+ hỗ trợ select_for_update, trong các phiên bản trước đó bạn có thể thực hiện các truy vấn SQL liệu ví dụ select ... for update tùy thuộc vào DB cơ bản sẽ khóa hàng từ bất kỳ bản cập nhật nào, bạn có thể làm bất cứ điều gì bạn muốn với hàng đó cho đến khi kết thúc giao dịch. ví dụ.

from django.db import transaction 

@transaction.commit_manually() 
def add_points(request): 
    user = User.objects.select_for_update().get(id=request.user.id) 
    # you can go back at this point if something is not right 
    if user.points > 1000: 
     # too many points 
     return 
    user.points += calculate_points(user) 
    user.save() 
    transaction.commit() 
+1

Đây phải là câu trả lời được chấp nhận. –

+0

Có vẻ như đã có bản vá trong một thời gian dài cho tính năng này https://code.djangoproject.com/ticket/2705 - Gần đây tôi đã áp dụng nó cho Django 1.3.5 (đối với một dự án lớn, khó di chuyển đến 1.4) – HighCat

+0

Tôi tự hỏi làm thế nào điều này được thực hiện tốt nhất như là một phương pháp của lớp người dùng (để có thể tái sử dụng ở những nơi khác, không chỉ trong chế độ xem đó).Vấn đề đối với tôi là mã gọi điện thoại vẫn phải thực hiện cuộc gọi select_for_update(), nhưng tôi muốn nó được incapsulated trong phương pháp của người dùng. –

6

Bạn có nhiều cách để đơn luồng loại điều này.

Một cách tiếp cận tiêu chuẩn là Cập nhật Đầu tiên. Bạn làm một bản cập nhật sẽ nắm bắt một khóa độc quyền trên hàng; sau đó làm công việc của bạn; và cuối cùng cam kết thay đổi. Để làm việc này, bạn cần phải bỏ qua bộ nhớ đệm của ORM.

Một cách tiếp cận tiêu chuẩn khác là có một máy chủ ứng dụng đơn luồng riêng biệt tách riêng các giao dịch Web khỏi tính toán phức tạp.

  • ứng dụng web của bạn có thể tạo ra một danh sách các yêu cầu chấm điểm, đẻ trứng một quá trình riêng biệt, và sau đó viết các yêu cầu điểm đến hàng đợi này. Các đẻ trứng có thể được đặt trong Django của urls.py do đó, nó sẽ xảy ra trên khởi động ứng dụng web. Hoặc nó có thể được đưa vào riêng biệt manage.py kịch bản quản trị. Hoặc nó có thể được thực hiện "khi cần" khi yêu cầu chấm điểm đầu tiên được thực hiện.

  • Bạn cũng có thể tạo một máy chủ web riêng có hương vị WSGI sử dụng Werkzeug chấp nhận yêu cầu WS qua urllib2. Nếu bạn có một số cổng duy nhất cho máy chủ này, các yêu cầu được xếp hàng đợi bởi TCP/IP. Nếu trình xử lý WSGI của bạn có một luồng, thì bạn đã đạt được luồng đơn được tuần tự hóa. Điều này có thể mở rộng hơn một chút, vì công cụ tính điểm là một yêu cầu WS và có thể chạy ở bất cứ đâu.

Tuy nhiên, một cách tiếp cận khác là phải có một số tài nguyên khác phải được mua và giữ để tính toán.

  • Đối tượng Singleton trong cơ sở dữ liệu. Một hàng duy nhất trong một bảng duy nhất có thể được cập nhật với ID phiên để nắm quyền kiểm soát; cập nhật với ID phiên của None để giải phóng kiểm soát. Bản cập nhật cần thiết phải bao gồm bộ lọc WHERE SESSION_ID IS NONE để đảm bảo rằng bản cập nhật không thành công khi khóa được giữ bởi người khác. Điều này thật thú vị bởi vì nó vốn không có chủng tộc - đó là một bản cập nhật duy nhất - không phải là một chuỗi SELECT-UPDATE.

  • Một semaphore vườn có thể được sử dụng bên ngoài cơ sở dữ liệu. Hàng đợi (nói chung) là dễ dàng hơn để làm việc với hơn một semaphore cấp thấp.

+1

+1 Tôi thực sự thích ý tưởng xếp hàng yêu cầu chấm điểm. –

+0

Câu trả lời hay. Bằng cách nào đó truy cập vào hàng cơ sở dữ liệu đã được serialized và tôi nghĩ rằng hàng đợi có khả năng mở rộng hơn so với ổ khóa. @ Fragsworth: xem dự án này cho một đơn giản để sử dụng thực hiện hàng đợi ở Django sử dụng RabbitMQ: http://ask.github.com/celery/introduction.html –

8

Khóa cơ sở dữ liệu là cách để đến đây. Có kế hoạch để thêm "chọn để cập nhật" hỗ trợ cho Django (here), nhưng bây giờ đơn giản nhất là sử dụng SQL thô để cập nhật đối tượng người dùng trước khi bạn bắt đầu tính toán điểm số.


Khóa bi quan hiện được hỗ trợ bởi ORM của Django 1.4 khi cơ sở DB (như Postgres) hỗ trợ nó. Xem Django 1.4a1 release notes.

1

Điều này có thể đơn giản hóa tình huống của bạn, nhưng thay thế chỉ là một liên kết JavaScript? Nói cách khác khi người dùng nhấp vào liên kết hoặc nút bọc yêu cầu trong một hàm JavaScript sẽ vô hiệu hóa ngay lập tức/"chuyển màu xám" liên kết và thay thế văn bản bằng thông tin "Đang tải ..." hoặc "Đang gửi yêu cầu ..." hoặc gì đó giống. bạn có muốn công việc kia?

+2

-1 nó vẫn không bảo vệ trang web. đôi khi người dùng đang sử dụng các ứng dụng khách http khác so với các trình duyệt. tức là người dùng có thể sử dụng wget để tìm nạp URL đã cho, sau đó vô hiệu hóa URL bằng jscript sẽ không giúp bạn tiết kiệm. Jscript nên được sử dụng chỉ để làm cho người dùng trang rùng rợn nếu bạn muốn, nhưng bạn không nên sử dụng nó để sửa các vấn đề trong ứng dụng phía máy chủ. – SashaN

+0

@SashaN: Người đăng không nói rằng điều này sẽ không chỉ được truy cập thông qua trình duyệt web. Chúng tôi không thể ngay lập tức giả định tất cả các trường hợp ngoại lệ khác như wget. Tôi cũng bắt đầu câu trả lời với "Điều này có thể là quá đơn giản tình hình của bạn ..." để bao gồm các trường hợp ngoại lệ, vì đề xuất này cũng có thể là một giải pháp phù hợp cho nhiều người. Hãy suy nghĩ về những người xem trong tương lai của câu hỏi này, những người có thể có một kịch bản hơi khác trong đó câu trả lời này có thể chỉ là vé. Tôi chắc chắn không chấp nhận rằng nó xứng đáng được bỏ phiếu "không hữu ích", nhưng tôi đánh giá cao bạn ít nhất là cung cấp một lý do. –

+1

"Thou Shall Not Trust Side Client" – Ekevoo

14

Kể từ Django 1.1, bạn có thể sử dụng biểu thức F() của ORM để giải quyết vấn đề cụ thể này.

from django.db.models import F 

user = request.user 
user.points = F('points') + calculate_points(user) 
user.save() 

Để biết thêm chi tiết hãy xem tài liệu:

https://docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

https://docs.djangoproject.com/en/1.8/ref/models/expressions/#django.db.models.F

+4

Các biểu thức 'F()' vẫn không cho phép bạn thêm điều kiện vào bản cập nhật. Vì vậy, bạn có thể nói tăng điểm người dùng nếu họ vẫn đang hoạt động. –

+0

nope ... điều này sẽ thất bại nếu bạn có cập nhật bên trong vòng lặp for! – NoobEditor

0

Bây giờ, bạn phải sử dụng:

Model.objects.select_for_update().get(foo=bar) 
+1

Giải thích ý định của bạn sẽ cải thiện câu trả lời của bạn . – reporter

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