Không có quyền truy cập ghi đồng thời
Cụ thể hóa một lựa chọn trong CTE và tham gia vào nó trong FROM
mệnh đề của UPDATE
.
WITH cte AS (
SELECT server_ip -- pk column or any (set of) unique column(s)
FROM server_info
WHERE status = 'standby'
LIMIT 1 -- arbitrary pick (cheapest)
)
UPDATE server_info s
SET status = 'active'
FROM cte
WHERE s.server_ip = cte.server_ip
RETURNING server_ip;
Ban đầu tôi có một truy vấn con đơn giản ở đây, nhưng điều đó có thể vượt qua các LIMIT
kế hoạch truy vấn nhất định như Feike đã chỉ ra:
Công cụ lập kế hoạch có thể chọn tạo một kế hoạch thực hiện một vòng lặp lồng nhau trên LIMITing
truy vấn con, gây ra nhiều UPDATEs
hơn LIMIT
, ví dụ:
Update on buganalysis [...] rows=5
-> Nested Loop
-> Seq Scan on buganalysis
-> Subquery Scan on sub [...] loops=11
-> Limit [...] rows=2
-> LockRows
-> Sort
-> Seq Scan on buganalysis
Sinh sản thử nghiệm
Cách khắc phục ở trên là bọc các LIMIT
truy vấn con trong CTE của chính nó, vì CTE được vật chất hóa, nó sẽ không trả về các kết quả khác nhau trên các lần lặp khác nhau của vòng lặp lồng nhau.
Hoặc sử dụng một truy vấn con tương quan thấpcho trường hợp đơn giản vớiLIMIT
1
. Đơn giản hơn, nhanh hơn:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
Với quyền truy cập ghi đồng thời
Giả sử mức cô lập mặc địnhREAD COMMITTED
cho tất cả điều này. Mức cô lập nghiêm ngặt hơn ( REPEATABLE READ
và SERIALIZABLE
) vẫn có thể dẫn đến lỗi nối tiếp. Xem:
Trong tải ghi đồng thời, thêm FOR UPDATE SKIP LOCKED
để khóa hàng để tránh điều kiện cuộc đua. SKIP LOCKED
đã được thêm vào trong Postgres 9.5 , cho các phiên bản cũ hơn xem bên dưới. Hướng dẫn sử dụng:
Với SKIP LOCKED
, bất kỳ hàng nào được chọn không thể bị khóa ngay lập tức đều bị bỏ qua. Bỏ qua các hàng bị khóa cung cấp một cái nhìn không nhất quán về dữ liệu, do đó, điều này không phù hợp với công việc cho mục đích chung, nhưng có thể được sử dụng để tránh tranh chấp khóa với nhiều người tiêu dùng truy cập vào một bảng giống như hàng đợi.
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING server_ip;
Nếu không còn hàng đủ điều kiện, đã mở khóa, không có gì xảy ra trong truy vấn này (không có hàng nào được cập nhật) và bạn nhận được kết quả trống. Đối với các hoạt động không chính xác có nghĩa là bạn đã hoàn thành.
Tuy nhiên, các giao dịch đồng thời có thể có hàng bị khóa, nhưng sau đó không hoàn thành cập nhật ( ROLLBACK
hoặc lý do khác). Để chắc chắn chạy kiểm tra cuối cùng:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
cũng thấy hàng bị khóa. Nếu không trả lại true
, một hoặc nhiều hàng vẫn đang được xử lý và giao dịch vẫn có thể được khôi phục. (Hoặc các hàng mới đã được thêm vào trong khi đó.) Đợi một chút, sau đó lặp hai bước: ( UPDATE
cho đến khi bạn không nhận được hàng nào; SELECT
...) cho đến khi bạn nhận được true
.
Liên quan:
Không có SKIP LOCKED
trong PostgreSQL 9.4 trở lên
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
Các giao dịch đồng thời cố gắng khóa cùng một hàng bị chặn cho đến khi giao dịch đầu tiên giải phóng khóa của nó.
Nếu lần đầu tiên được khôi phục, giao dịch tiếp theo sẽ khóa và tiến hành bình thường; Những người khác trong hàng đợi tiếp tục chờ đợi.
Nếu cam kết đầu tiên, WHERE
điều kiện được đánh giá lại và nếu không TRUE
còn nữa ( status
đã thay đổi) thì CTE (hơi ngạc nhiên) không trả về hàng. Chẳng có gì xảy ra. Đó là hành vi mong muốn khi tất cả các giao dịch muốn cập nhật các giống hàng .
Nhưng không phải khi mỗi giao dịch muốn cập nhật các cạnh hàng . Và vì chúng tôi chỉ muốn cập nhật một hàng tùy ý (hoặc ngẫu nhiên ) , không có gì phải chờ đợi cả.
Chúng tôi có thể bỏ chặn tình huống với sự trợ giúp của các khóa tư vấn :
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
AND pg_try_advisory_xact_lock(id)
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
Bằng cách này, hàng tiếp theo chưa bị khóa sẽ được cập nhật. Mỗi giao dịch được một hàng mới để làm việc với. Tôi đã có sự giúp đỡ từ Séc Postgres Wiki cho thủ thuật này.
id
là bất kỳ bigint
cột duy nhất (hoặc bất kỳ loại nào có một biểu mẫu ẩn như int4
hoặc int2
).
Nếu các khóa tư vấn được sử dụng cho nhiều bảng trong cơ sở dữ liệu của bạn đồng thời, hãy phân biệt với pg_try_advisory_xact_lock(tableoid::int, id)
- id
là duy nhất integer
ở đây.
Vì tableoid
là một đại bigint
lượng, về mặt lý thuyết nó có thể tràn integer
. Nếu bạn đủ hoang tưởng, (tableoid::bigint % 2147483648)::int
thay vào đó hãy sử dụng - để lại một "va chạm băm" trên lý thuyết cho ...
Ngoài ra, Postgres có thể tự do kiểm tra các WHERE
điều kiện theo bất kỳ thứ tự nào. Nó có thể kiểm tra pg_try_advisory_xact_lock()
và có được một khóa trước status = 'standby'
đó, điều này có thể dẫn đến các khóa tư vấn bổ sung trên các hàng không liên quan, điều status = 'standby'
này không đúng. Câu hỏi liên quan về SO:
Thông thường, bạn chỉ có thể bỏ qua điều này. Để đảm bảo rằng chỉ các hàng đủ điều kiện bị khóa, bạn có thể lồng (các) vị từ trong CTE như trên hoặc truy vấn con với OFFSET 0
hack (ngăn chặn nội tuyến) . Thí dụ:
Hoặc (rẻ hơn cho các lần quét liên tiếp) lồng các điều kiện trong một CASE
tuyên bố như:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Tuy nhiên , CASE
mánh khóe cũng sẽ khiến Postgres không sử dụng chỉ mục status
. Nếu có sẵn một chỉ mục như vậy, bạn không cần thêm lồng nhau để bắt đầu: chỉ các hàng đủ điều kiện sẽ bị khóa trong quá trình quét chỉ mục.
Vì bạn không thể chắc chắn rằng một chỉ mục được sử dụng trong mọi cuộc gọi, bạn chỉ có thể:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Đây CASE
là dự phòng logic, nhưng nó phục vụ mục đích thảo luận.
Nếu lệnh là một phần của giao dịch dài, hãy xem xét các khóa cấp phiên có thể (và phải) được phát hành thủ công. Vì vậy, bạn có thể mở khóa ngay khi bạn hoàn thành với hàng bị khóa: pg_try_advisory_lock()
vàpg_advisory_unlock()
. Hướng dẫn sử dụng:
Sau khi có được ở cấp phiên, khóa tư vấn được giữ cho đến khi được phát hành rõ ràng hoặc phiên kết thúc.
Liên quan: