Django: Làm cách nào để bảo vệ chống lại việc sửa đổi đồng thời các mục nhập cơ sở dữ liệu


81

Nếu có cách nào để bảo vệ khỏi các sửa đổi đồng thời của cùng một mục nhập cơ sở dữ liệu bởi hai hoặc nhiều người dùng?

Có thể chấp nhận hiển thị thông báo lỗi cho người dùng thực hiện thao tác cam kết / lưu thứ hai, nhưng dữ liệu không được ghi đè một cách âm thầm.

Tôi nghĩ rằng khóa mục nhập không phải là một tùy chọn, vì người dùng có thể sử dụng nút "Quay lại" hoặc chỉ cần đóng trình duyệt của mình, để lại khóa mãi mãi.


4
Nếu một đối tượng có thể được cập nhật bởi nhiều người dùng đồng thời, bạn có thể gặp vấn đề lớn hơn về thiết kế. Có thể đáng suy nghĩ về các tài nguyên dành riêng cho người dùng hoặc tách các bước xử lý thành các bảng riêng biệt để ngăn điều này thành vấn đề.
S.Lott

Câu trả lời:


48

Đây là cách tôi thực hiện khóa lạc quan trong Django:

updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
          .update(updated_field=new_value, version=e.version+1)
if not updated:
    raise ConcurrentModificationException()

Mã được liệt kê ở trên có thể được triển khai như một phương pháp trong Trình quản lý tùy chỉnh .

Tôi đang đưa ra các giả định sau:

  • filter (). update () sẽ dẫn đến một truy vấn cơ sở dữ liệu duy nhất vì bộ lọc lười
  • một truy vấn cơ sở dữ liệu là nguyên tử

Những giả định này đủ để đảm bảo rằng không có ai khác đã cập nhật mục nhập trước đó. Nếu nhiều hàng được cập nhật theo cách này, bạn nên sử dụng các giao dịch.

CẢNH BÁO Tài liệu Django :

Lưu ý rằng phương thức update () được chuyển đổi trực tiếp thành câu lệnh SQL. Đây là một hoạt động hàng loạt để cập nhật trực tiếp. Nó không chạy bất kỳ phương thức save () nào trên mô hình của bạn hoặc phát ra các tín hiệu pre_save hoặc post_save


12
Đẹp! Tuy nhiên, đó không nên là '&' thay vì '&&'?
Giles Thomas,

1
Bạn có thể tránh được vấn đề 'cập nhật' không chạy phương thức save () bằng cách đặt lệnh gọi 'update' bên trong phương thức save () bị ghi đè của riêng bạn không?
Jonathan Hartley

1
Điều gì sẽ xảy ra khi hai luồng gọi đồng thời filter, cả hai đều nhận được một danh sách giống hệt nhau với chưa sửa đổi e, và sau đó cả hai cùng gọi update? Tôi thấy không có semaphore nào chặn bộ lọc và cập nhật đồng thời. CHỈNH SỬA: ồ, tôi đã hiểu bộ lọc lười biếng. Nhưng giá trị của việc giả sử update () là nguyên tử là gì? chắc chắn DB xử lý truy cập đồng thời
totowtwo

1
@totowtwo Tôi trong ACID đảm bảo việc đặt hàng ( en.wikipedia.org/wiki/ACID ). Nếu một CẬP NHẬT đang thực thi trên dữ liệu liên quan đến đồng thời (nhưng bắt đầu sau đó) CHỌN, nó sẽ chặn cho đến khi CẬP NHẬT hoàn tất. Tuy nhiên, nhiều SELECT có thể được thực hiện cùng một lúc.
Kit Sunde

1
Có vẻ như điều này sẽ chỉ hoạt động bình thường với chế độ tự động gửi (là mặc định). Nếu không, COMMIT cuối cùng sẽ tách biệt khỏi câu lệnh SQL cập nhật này, vì vậy mã đồng thời có thể chạy giữa chúng. Và chúng tôi có mức cô lập ReadCommited trong Django, vì vậy nó sẽ đọc phiên bản cũ. (Tại sao tôi muốn giao dịch thủ công ở đây - vì tôi muốn tạo một hàng trong một bảng khác cùng với bản cập nhật này.) Tuy nhiên, ý tưởng tuyệt vời.
Alex Lokk,

39

Câu hỏi này hơi cũ và câu trả lời của tôi hơi muộn, nhưng sau những gì tôi hiểu, điều này đã được sửa trong Django 1.4 bằng cách sử dụng:

select_for_update(nowait=True)

xem tài liệu

Trả về một bộ truy vấn sẽ khóa các hàng cho đến khi kết thúc giao dịch, tạo câu lệnh SQL SELECT ... FOR UPDATE trên cơ sở dữ liệu được hỗ trợ.

Thông thường, nếu một giao dịch khác đã có khóa trên một trong các hàng đã chọn, thì truy vấn sẽ chặn cho đến khi khóa được giải phóng. Nếu đây không phải là hành vi bạn muốn, hãy gọi select_for_update (nowait = True). Điều này sẽ làm cho cuộc gọi không bị chặn. Nếu một khóa xung đột đã được thực hiện bởi một giao dịch khác, DatabaseError sẽ xuất hiện khi bộ truy vấn được đánh giá.

Tất nhiên điều này sẽ chỉ hoạt động nếu back-end hỗ trợ tính năng "chọn để cập nhật", ví dụ như sqlite thì không. Thật không may: nowait=Truekhông được MySql hỗ trợ, ở đó bạn phải sử dụng : nowait=False, sẽ chỉ chặn cho đến khi khóa được phát hành.


2
Đây không phải là một câu trả lời tuyệt vời - câu hỏi rõ ràng không muốn khóa (bi quan) và hai câu trả lời được bình chọn cao hơn hiện tập trung vào kiểm soát đồng thời lạc quan ("khóa lạc quan") vì lý do đó. Mặc dù vậy, lựa chọn để cập nhật vẫn tốt trong các tình huống khác.
RichVel

@ giZm0 Điều đó vẫn khiến nó bị khóa bi quan. Sợi đầu tiên có được khóa có thể giữ nó vô thời hạn.
knaperek

6
Tôi thích câu trả lời này vì là tài liệu của Django chứ không phải là một phát minh đẹp đẽ của bất kỳ bên thứ ba nào.
anizzomc

29

Trên thực tế, các giao dịch không giúp bạn nhiều ở đây ... trừ khi bạn muốn có các giao dịch chạy trên nhiều yêu cầu HTTP (điều mà bạn có thể không muốn nhất).

Những gì chúng tôi thường sử dụng trong những trường hợp đó là "Khóa lạc quan". Django ORM không hỗ trợ điều đó theo như tôi biết. Nhưng đã có một số cuộc thảo luận về việc thêm tính năng này.

Vì vậy, bạn đang ở trên của riêng bạn. Về cơ bản, những gì bạn nên làm là thêm trường "phiên bản" vào mô hình của bạn và chuyển nó cho người dùng dưới dạng trường ẩn. Chu kỳ thông thường cho một bản cập nhật là:

  1. đọc dữ liệu và hiển thị nó cho người dùng
  2. người dùng sửa đổi dữ liệu
  3. người dùng đăng dữ liệu
  4. ứng dụng lưu lại trong cơ sở dữ liệu.

Để triển khai khóa lạc quan, khi bạn lưu dữ liệu, bạn kiểm tra xem phiên bản mà bạn nhận lại từ người dùng có giống với phiên bản trong cơ sở dữ liệu hay không, sau đó cập nhật cơ sở dữ liệu và tăng phiên bản. Nếu không, điều đó có nghĩa là đã có sự thay đổi kể từ khi dữ liệu được tải.

Bạn có thể làm điều đó với một lệnh gọi SQL duy nhất với một cái gì đó như:

UPDATE ... WHERE version = 'version_from_user';

Lệnh gọi này sẽ chỉ cập nhật cơ sở dữ liệu nếu phiên bản vẫn như cũ.


1
Câu hỏi tương tự này cũng xuất hiện trên Slashdot. Các Khóa lạc bạn đề nghị cũng đã được đề xuất ở đó, nhưng giải thích một chút tốt hơn IMHO: hardware.slashdot.org/comments.pl?sid=1381511&cid=29536367
hopla

5
Cũng lưu ý rằng bạn muốn sử dụng các giao dịch ở trên cùng, để tránh trường hợp này: hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Django cung cấp phần mềm trung gian để tự động gói mọi hành động trên cơ sở dữ liệu trong một giao dịch, bắt đầu từ yêu cầu ban đầu và chỉ cam kết sau khi phản hồi thành công: docs.djangoproject.com/en/dev/topics/db/transactions (lưu ý bạn: phần mềm trung gian giao dịch chỉ giúp tránh vấn đề trên với khóa lạc quan, nó không cung cấp khóa bởi chính nó)
hopla

Tôi cũng đang tìm kiếm chi tiết về cách thực hiện việc này. Không có may mắn cho đến nay.
seanyboy 9/12/09

1
bạn có thể làm điều này bằng cách sử dụng cập nhật hàng loạt django. kiểm tra câu trả lời của tôi.
Andrei Savu

14

Django 1.11 có ba tùy chọn thuận tiện để xử lý tình huống này tùy thuộc vào yêu cầu logic kinh doanh của bạn:

  • Something.objects.select_for_update() sẽ chặn cho đến khi mô hình trở nên miễn phí
  • Something.objects.select_for_update(nowait=True)và nắm bắt DatabaseErrornếu mô hình hiện bị khóa để cập nhật
  • Something.objects.select_for_update(skip_locked=True) sẽ không trả lại các đối tượng hiện đang bị khóa

Trong ứng dụng của tôi, có cả luồng công việc tương tác và hàng loạt trên các mô hình khác nhau, tôi tìm thấy ba tùy chọn này để giải quyết hầu hết các tình huống xử lý đồng thời của mình.

Việc "chờ đợi" select_for_updaterất thuận tiện trong các quy trình hàng loạt tuần tự - tôi muốn tất cả chúng thực thi, nhưng hãy để chúng mất thời gian. Các nowaitđược sử dụng khi người dùng muốn sửa đổi một đối tượng mà hiện tại đang bị khóa để cập nhật - Tôi sẽ chỉ nói với họ nó đang được sửa đổi tại thời điểm này.

Điều skip_lockednày hữu ích cho một loại cập nhật khác, khi người dùng có thể kích hoạt quét lại một đối tượng - và tôi không quan tâm ai kích hoạt nó, miễn là nó được kích hoạt, vì vậy skip_lockedcho phép tôi âm thầm bỏ qua các trình kích hoạt trùng lặp.


1
Tôi có cần bao bọc lựa chọn để cập nhật với transaction.atomic () không? Nếu tôi thực sự sử dụng kết quả để cập nhật? Nó sẽ không khóa toàn bộ bảng khiến select_for_update trở thành noop?
Paul Kenjora

3

Để tham khảo trong tương lai, hãy xem https://github.com/RobCombs/django-locking . Nó khóa theo cách không để lại ổ khóa vĩnh viễn, bằng cách kết hợp mở khóa javascript khi người dùng rời khỏi trang và khóa thời gian chờ (ví dụ: trong trường hợp trình duyệt của người dùng bị treo). Tài liệu khá đầy đủ.


3
Tôi của tôi, đây là một ý tưởng thực sự kỳ lạ.
julx

1

Bạn có lẽ nên sử dụng phần mềm trung gian giao dịch django ít nhất, ngay cả khi vấn đề này xảy ra.

Đối với vấn đề thực tế của bạn khi có nhiều người dùng chỉnh sửa cùng một dữ liệu ... vâng, hãy sử dụng khóa. HOẶC LÀ:

Kiểm tra phiên bản mà người dùng đang cập nhật (thực hiện điều này một cách an toàn, vì vậy người dùng không thể chỉ cần hack hệ thống để nói rằng họ đang cập nhật bản sao mới nhất!) Và chỉ cập nhật nếu phiên bản đó là mới nhất. Nếu không, hãy gửi lại cho người dùng một trang mới với phiên bản gốc mà họ đang chỉnh sửa, phiên bản đã gửi của họ và (các) phiên bản mới do người khác viết. Yêu cầu họ hợp nhất các thay đổi thành một phiên bản cập nhật hoàn toàn. Bạn có thể cố gắng tự động hợp nhất những thứ này bằng cách sử dụng một bộ công cụ như diff + patch, nhưng dù sao thì bạn cũng cần có phương pháp hợp nhất thủ công hoạt động cho các trường hợp lỗi, vì vậy hãy bắt đầu với điều đó. Ngoài ra, bạn sẽ cần lưu giữ lịch sử phiên bản và cho phép quản trị viên hoàn nguyên các thay đổi, trong trường hợp ai đó vô tình hoặc cố ý làm xáo trộn quá trình hợp nhất. Nhưng dù sao thì bạn cũng nên có cái đó.

Rất có thể có một ứng dụng / thư viện django thực hiện hầu hết việc này cho bạn.


Đây cũng là Khóa lạc quan, như Guillaume đã đề xuất. Nhưng anh ấy dường như đạt được tất cả các điểm :)
hopla 29/09/09

0

Một thứ khác cần tìm là từ "nguyên tử". Một hoạt động nguyên tử có nghĩa là thay đổi cơ sở dữ liệu của bạn sẽ diễn ra thành công hoặc thất bại rõ ràng. Tìm kiếm nhanh cho thấy câu hỏi này hỏi về các hoạt động nguyên tử trong Django.


Tôi không muốn thực hiện một giao dịch hoặc khóa nhiều yêu cầu, vì điều này có thể mất bất kỳ thời gian nào (và có thể không bao giờ kết thúc)
Ber

Nếu một giao dịch bắt đầu, nó phải kết thúc. Bạn chỉ nên khóa hồ sơ (hoặc bắt đầu giao dịch, hoặc bất cứ điều gì bạn quyết định làm) sau khi người dùng nhấp vào "gửi", chứ không phải khi họ mở hồ sơ để xem.
Harley Holcombe

Có, nhưng vấn đề của tôi khác, ở chỗ hai người dùng mở cùng một biểu mẫu và sau đó cả hai đều cam kết các thay đổi của họ. Tôi không nghĩ rằng khóa là giải pháp cho điều này.
Ber

Bạn nói đúng, nhưng vấn đề không có giải pháp cho việc này. Một người dùng giành chiến thắng, người kia nhận được thông báo thất bại. Bạn khóa bản ghi càng muộn thì bạn càng gặp ít vấn đề hơn.
Harley Holcombe

Tôi đồng ý. Tôi hoàn toàn chấp nhận thông báo thất bại cho người dùng khác. Tôi đang tìm cách tốt để phát hiện trường hợp này (mà tôi mong đợi là rất hiếm).
Ber

0

Ý tưởng trên

updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
      .update(updated_field=new_value, version=e.version+1)
if not updated:
      raise ConcurrentModificationException()

trông tuyệt vời và sẽ hoạt động tốt ngay cả khi không có các giao dịch có thể tuần tự hóa.

Vấn đề là làm thế nào để tăng cường hành vi deafult .save () để không phải làm hệ thống ống nước thủ công để gọi phương thức .update ().

Tôi đã xem xét ý tưởng Trình quản lý tùy chỉnh.

Kế hoạch của tôi là ghi đè phương thức Manager _update được Model.save_base () gọi để thực hiện cập nhật.

Đây là mã hiện tại trong Django 1.3

def _update(self, values, **kwargs):
   return self.get_query_set()._update(values, **kwargs)

Những gì cần làm IMHO là những việc như:

def _update(self, values, **kwargs):
   #TODO Get version field value
   v = self.get_version_field_value(values[0])
   return self.get_query_set().filter(Q(version=v))._update(values, **kwargs)

Điều tương tự cần xảy ra khi xóa. Tuy nhiên, việc xóa khó hơn một chút vì Django đang triển khai khá nhiều voodoo trong lĩnh vực này thông qua django.db.models.deletion.Collector.

Thật kỳ lạ khi công cụ modren như Django thiếu hướng dẫn cho Kiểm soát sự đồng bộ tối ưu.

Tôi sẽ cập nhật bài đăng này khi tôi giải được câu đố. Hy vọng rằng giải pháp sẽ theo một cách tốt đẹp mà không liên quan đến hàng tấn mã hóa, chế độ xem kỳ lạ, bỏ qua các phần thiết yếu của Django, v.v.


-2

Để được an toàn, cơ sở dữ liệu cần hỗ trợ các giao dịch .

Nếu các trường là "dạng tự do", ví dụ: văn bản, v.v. và bạn cần cho phép nhiều người dùng có thể chỉnh sửa các trường giống nhau (bạn không thể có quyền sở hữu người dùng duy nhất đối với dữ liệu), bạn có thể lưu trữ dữ liệu gốc trong một Biến đổi. Khi người dùng cam kết, hãy kiểm tra xem dữ liệu đầu vào có thay đổi so với dữ liệu gốc hay không (nếu không, bạn không cần bận tâm đến DB bằng cách viết lại dữ liệu cũ), nếu dữ liệu gốc so với dữ liệu hiện tại trong db là giống nhau. bạn có thể lưu, nếu nó đã thay đổi, bạn có thể cho người dùng thấy sự khác biệt và yêu cầu người dùng phải làm gì.

Nếu các trường là số, ví dụ: số dư tài khoản, số lượng mặt hàng trong cửa hàng, v.v., bạn có thể xử lý nó tự động hơn nếu bạn tính toán sự khác biệt giữa giá trị ban đầu (được lưu trữ khi người dùng bắt đầu điền vào biểu mẫu) và giá trị mới mà bạn có thể bắt đầu giao dịch, đọc giá trị hiện tại và thêm chênh lệch, sau đó kết thúc giao dịch. Nếu bạn không thể có giá trị âm, bạn nên hủy giao dịch nếu kết quả là âm và thông báo cho người dùng.

Tôi không biết django, vì vậy tôi không thể cho bạn teh cod3s ..;)


-6

Từ đây:
Cách ngăn ghi đè đối tượng mà người khác đã sửa đổi

Tôi giả định rằng dấu thời gian sẽ được giữ dưới dạng trường ẩn trong biểu mẫu mà bạn đang cố gắng lưu chi tiết.

def save(self):
    if(self.id):
        foo = Foo.objects.get(pk=self.id)
        if(foo.timestamp > self.timestamp):
            raise Exception, "trying to save outdated Foo" 
    super(Foo, self).save()

1
mã bị hỏng. điều kiện chạy đua vẫn có thể xảy ra giữa truy vấn if check và save. bạn cần sử dụng objects.filter (kiểm tra id = .. & timestamp) .update (...) và đưa ra một ngoại lệ nếu không có hàng nào được cập nhật.
Andrei Savu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.