Postgres CẬP NHẬT GIỚI HẠN 1


77

Tôi có cơ sở dữ liệu Postgres chứa chi tiết về các cụm máy chủ, chẳng hạn như trạng thái máy chủ ('hoạt động', 'chờ', v.v.). Các máy chủ hoạt động bất cứ lúc nào cũng có thể cần chuyển sang chế độ chờ và tôi không quan tâm cụ thể chế độ chờ nào được sử dụng.

Tôi muốn một truy vấn cơ sở dữ liệu để thay đổi trạng thái của chế độ chờ - CHỈ MỘT - và trả lại IP máy chủ sẽ được sử dụng. Lựa chọn có thể tùy ý: vì trạng thái của máy chủ thay đổi theo truy vấn, nên việc chọn chế độ chờ nào không quan trọng.

Có thể giới hạn truy vấn của tôi chỉ một bản cập nhật?

Đây là những gì tôi có cho đến nay:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

Postgres không thích điều này. Tôi có thể làm gì khác?


Chỉ cần chọn máy chủ trong mã và thêm nó vào nơi bị ràng buộc. Điều này cũng cho phép bạn có các điều kiện bổ sung (cũ nhất, mới nhất, mới nhất còn tồn tại, ít tải nhất, cùng dc, giá khác nhau, ít lỗi nhất) được kiểm tra trước tiên. Hầu hết các giao thức failover yêu cầu một số hình thức xác định nào.
eckes

@eckes Đó là một ý tưởng thú vị. Trong trường hợp của tôi, "chọn máy chủ trong mã" có nghĩa là trước tiên hãy đọc danh sách các máy chủ có sẵn từ db và sau đó cập nhật một bản ghi. Bởi vì nhiều phiên bản của ứng dụng có thể thực hiện hành động này, nên có một điều kiện chủng tộc và hoạt động nguyên tử là cần thiết (hoặc là 5 năm trước). Sự lựa chọn không cần phải mang tính quyết định.
differlysuperiorman

Câu trả lời:


125

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 FROMmệ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 LIMITkế 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 LIMITingtruy vấn con, gây ra nhiều UPDATEshơ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 LIMITtruy 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 READSERIALIZABLE) 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 ( ROLLBACKhoặ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'
   );

SELECTcũ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: ( UPDATEcho đế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 LOCKEDtrong 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 TRUEcò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.

idlà bất kỳ bigintcột duy nhất (hoặc bất kỳ loại nào có một biểu mẫu ẩn như int4hoặ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)- idlà duy nhất integerở đây.
tableoidlà một đại bigintlượng, về mặt lý thuyết nó có thể tràn integer. Nếu bạn đủ hoang tưởng, (tableoid::bigint % 2147483648)::intthay 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 0hack (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 CASEtuyên bố như:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Tuy nhiên , CASEmá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 CASElà 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()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:

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.