{"slug": "row-lock-for-update", "title": "Row Lock — FOR UPDATE", "summary": "PostgreSQL's SELECT ... FOR UPDATE provides a row-level lock that prevents lost updates by blocking concurrent transactions from modifying the same row until the lock holder commits. The lock is implemented by storing the transaction ID in the tuple's xmax field, avoiding shared memory overhead. Variants include FOR KEY SHARE, FOR SHARE, FOR NO KEY UPDATE, and FOR UPDATE, with modifiers NOWAIT and SKIP LOCKED for different concurrency needs.", "body_md": "`SELECT ... FOR UPDATE`\n\nlà cách rõ ràng nhất để nói với Postgres \"tao sẽ sửa row này, đừng cho ai khác đụng vào cho tới khi tao commit\". Nó là một row-level lock thật sự — khác `SELECT`\n\nthường (chỉ chụp snapshot MVCC, không ngăn ai update song song). Lý do dev gặp nó trong việc thật là class bug **lost update**: hai transaction cùng đọc một row, cùng tính giá trị mới dựa trên giá trị đọc được, rồi cùng `UPDATE`\n\n— bản ghi cuối đè bản trước, một nửa thay đổi biến mất không log lỗi gì. `FOR UPDATE`\n\nép hai bên xếp hàng tại bước đọc, một bên đợi bên kia commit rồi tự đọc lại bản mới. Đổi lại, nếu nhiều code path khoá nhiều row theo thứ tự khác nhau, Postgres sẽ bắn `ERROR: deadlock detected`\n\nvà một bên transaction bay theo.\n\nKhi một transaction chạy `SELECT ... FOR UPDATE`\n\n, Postgres không ghi row lock vào lock table chính (như cách nó làm với relation-level lock). Thay vào đó, nó ghi xid của transaction hiện tại vào ** xmax** của chính tuple đó trên heap, kèm cờ infomask đánh dấu \"đây là lock chứ chưa phải delete\". Hệ quả: ôm row lock cho hàng triệu row gần như không tốn shared memory. Khi một transaction khác chạm cùng row (qua\n\n`UPDATE`\n\n, `DELETE`\n\n, hay một `SELECT ... FOR UPDATE`\n\nnữa), nó đọc `xmax`\n\n, thấy transaction kia còn sống, và `transactionid`\n\n`pg_locks`\n\nđể đợi xid đó kết thúc. Đó là cơ chế \"đợi xid\" lộ ra qua `wait_event = 'transactionid'`\n\nở `pg_stat_activity`\n\n. Khi bên giữ `COMMIT`\n\nhoặc `ROLLBACK`\n\n, bên đợi được đánh thức, đọc lại tuple (visibility check theo isolation level), rồi mới chạy tiếp.\n\n```\n-- Session A\nBEGIN;\nSELECT id, balance FROM accounts WHERE id = 42 FOR UPDATE;\n-- giữ row lock trên id=42, chưa commit\n\n-- Session B (terminal khác)\nBEGIN;\nUPDATE accounts SET balance = balance - 100 WHERE id = 42;\n-- treo, đợi xid của Session A\n```\n\n`FOR UPDATE`\n\ncó bốn biến thể, mạnh dần ngược lại: `FOR KEY SHARE`\n\n(yếu nhất, chỉ chặn thay đổi key — đây là lock mà foreign key check tự áp khi `INSERT`\n\nrow con), `FOR SHARE`\n\n, `FOR NO KEY UPDATE`\n\n(như `FOR UPDATE`\n\nnhưng không chặn các `FOR KEY SHARE`\n\nsong song — `UPDATE`\n\nkhông đụng key tự dùng mode này), và `FOR UPDATE`\n\n(mạnh nhất, mọi mode khác chặn lẫn nhau). Khi nhiều transaction cùng ôm shared mode trên một row (`FOR SHARE`\n\n/`FOR KEY SHARE`\n\n), `xmax`\n\nkhông đủ chỗ — Postgres tạo một **multixact id** đại diện cho tập transaction đang ôm row đó, ghi multixact id vào `xmax`\n\n. Multixact có table riêng (`pg_multixact`\n\n), được vacuum riêng, và là nguồn gốc của class incident \"multixact wraparound\" khi workload có nhiều shared row lock kéo dài.\n\nHai modifier quan trọng: `NOWAIT`\n\nbáo lỗi ngay nếu không lấy được lock (`ERROR: could not obtain lock on row in relation`\n\n), `SKIP LOCKED`\n\nbỏ qua row đang bị lock và đọc row tiếp theo. `SKIP LOCKED`\n\nlà cơ chế gốc cho job queue pattern.\n\n**Failure mode 1: lost update vì đọc-tính-ghi không khoá.** Đây là incident kinh điển trên mọi domain có counter, balance, inventory, vote count. Read Committed (mặc định Postgres) **không** chặn được nó — mỗi statement chụp snapshot mới, hai `UPDATE`\n\ncuối cùng đều thấy row tồn tại và đều chạy:\n\n```\n-- Sai: read-modify-write trên application code, không lock\nBEGIN;\nSELECT balance INTO v_balance FROM accounts WHERE id = 42;\n-- application tính: v_new = v_balance - 100\nUPDATE accounts SET balance = v_new WHERE id = 42;\nCOMMIT;\n-- Hai transaction song song cùng đọc balance = 1000, cùng ghi 900.\n-- Kết quả đáng lẽ phải là 800, nhưng còn 900. 100 đồng biến mất.\n```\n\nCó hai cách sửa. Cách thẳng: gộp tính toán vào một statement nguyên tử, không cần lock vì `UPDATE`\n\ntự đọc lại row theo Read Committed re-check:\n\n```\nUPDATE accounts SET balance = balance - 100 WHERE id = 42 AND balance >= 100\nRETURNING balance;\n```\n\nCách thứ hai khi application bắt buộc phải đọc rồi tính giữa chừng (ví dụ gọi service ngoài, format message): khoá hẳn bằng `FOR UPDATE`\n\n:\n\n```\nBEGIN;\nSELECT balance FROM accounts WHERE id = 42 FOR UPDATE;\n-- transaction B chạm cùng row sẽ đợi tới khi mình COMMIT\n-- application làm việc gì đó với balance...\nUPDATE accounts SET balance = balance - 100 WHERE id = 42;\nCOMMIT;\n```\n\nLưu ý: dùng `Repeatable Read`\n\n/`Serializable`\n\nkhông thay được `FOR UPDATE`\n\nở pattern này — chúng sẽ phát hiện conflict và ném `ERROR: could not serialize access`\n\n(SQLSTATE `40001`\n\n), bên gọi phải retry. `FOR UPDATE`\n\nở Read Committed thì transaction đợi rồi đọc lại, không cần retry layer.\n\n**Failure mode 2: deadlock vì lock row không cùng thứ tự.** Hai code path khoá hai row theo thứ tự ngược nhau là công thức kinh điển:\n\n``` php\n-- Transaction A: chuyển tiền 42 -> 99\nBEGIN;\nUPDATE accounts SET balance = balance - 100 WHERE id = 42;  -- lock 42\nUPDATE accounts SET balance = balance + 100 WHERE id = 99;  -- lock 99\n\n-- Transaction B: chuyển tiền 99 -> 42 (cùng lúc)\nBEGIN;\nUPDATE accounts SET balance = balance - 100 WHERE id = 99;  -- lock 99\nUPDATE accounts SET balance = balance + 100 WHERE id = 42;  -- đợi A\n-- A giờ đợi B ở row 99 → cycle → sau deadlock_timeout (mặc định 1s)\n-- Postgres ném ERROR: deadlock detected, một bên rollback\n```\n\nSửa ở tầng application: **luôn lock row theo thứ tự id tăng dần**, bất kể nghiệp vụ muốn debit hay credit trước. Cụ thể:\n\n```\nBEGIN;\n-- sort id trước khi lock\nSELECT id FROM accounts WHERE id IN (42, 99) ORDER BY id FOR UPDATE;\n-- sau đó UPDATE theo thứ tự nghiệp vụ\nUPDATE accounts SET balance = balance - 100 WHERE id = 42;\nUPDATE accounts SET balance = balance + 100 WHERE id = 99;\nCOMMIT;\n```\n\nMột cách khác: dồn cả nghiệp vụ vào một CTE/function để Postgres tự quyết thứ tự lock theo plan, không bị application chen vào.\n\n**Failure mode 3: ôm FOR UPDATE rồi rớt qua boundary application (idle in transaction).** Code lấy row, lock, gọi service ngoài, service treo, transaction vẫn open, row vẫn bị lock — mọi worker khác đụng row đó queue lại. Triệu chứng: latency tăng vọt trên một subset row \"nóng\",\n\n`pg_stat_activity`\n\nđầy `wait_event = 'transactionid'`\n\n. Xử: `SET idle_in_transaction_session_timeout`\n\ncấp cluster để Postgres tự cắt; mọi external call phải có timeout cứng; pattern \"lock → gọi service ngoài → update\" cần đổi thành \"gọi service ngoài → lock → quick update\".**Failure mode 4: queue table dùng FOR UPDATE thiếu SKIP LOCKED.** N worker cùng poll, mỗi worker chạy\n\n`SELECT ... FOR UPDATE LIMIT 1`\n\n— worker 2 đợi worker 1 nhả, parallel hoá serial. Sửa bằng `SKIP LOCKED`\n\n:\n\n```\n-- Đúng: nhiều worker pick job song song, mỗi worker một batch riêng\nWITH next_jobs AS (\n  SELECT id FROM jobs\n  WHERE status = 'pending'\n  ORDER BY created_at\n  FOR UPDATE SKIP LOCKED\n  LIMIT 50\n)\nUPDATE jobs SET status = 'processing', picked_at = now()\nWHERE id IN (SELECT id FROM next_jobs)\nRETURNING *;\n```\n\n`SKIP LOCKED`\n\nthay đổi semantic: hai worker không bao giờ pick cùng một row, nhưng nếu mọi row top đều đang lock, worker có thể nhận về 0 row dù bảng còn job — đó là cái giá để parallel.\n\nTriệu chứng row lock contention: latency tăng tập trung vào một subset row \"nóng\" (account chính, inventory bestseller), `wait_event_type = 'Lock'`\n\nvới `wait_event = 'transactionid'`\n\nhoặc `'tuple'`\n\n, CPU server không cao. Khác với relation lock (FIFO trên cả bảng) — row lock chỉ ảnh hưởng transaction đụng đúng row đó.\n\nQuery \"ai đang đợi row của ai\":\n\n```\nSELECT a.pid,\n       a.application_name,\n       a.state,\n       a.wait_event_type,\n       a.wait_event,\n       now() - a.xact_start AS xact_age,\n       pg_blocking_pids(a.pid) AS blocked_by,\n       left(a.query, 200) AS query\nFROM pg_stat_activity a\nWHERE a.backend_type = 'client backend'\n  AND (a.wait_event = 'transactionid'\n       OR a.wait_event = 'tuple'\n       OR pg_blocking_pids(a.pid) <> '{}')\nORDER BY xact_age DESC NULLS LAST;\n```\n\nĐể thấy row lock cụ thể (lock trên xid của transaction đang ôm row):\n\n```\nSELECT l.pid,\n       l.locktype,\n       l.transactionid,\n       l.mode,\n       l.granted,\n       a.application_name,\n       left(a.query, 120) AS query\nFROM pg_locks l\nLEFT JOIN pg_stat_activity a ON a.pid = l.pid\nWHERE l.locktype IN ('transactionid', 'tuple')\nORDER BY l.granted, l.pid;\n```\n\n`locktype = 'transactionid'`\n\nxuất hiện ở **bên đợi** (granted = false, đợi xid bên giữ row). `locktype = 'tuple'`\n\nít gặp hơn — là lock ngắn trên tuple header khi nhiều backend cùng đua update một row (Postgres dùng nó để serialize giai đoạn cập nhật `xmax`\n\n).\n\nGUC cần bật:\n\n`log_lock_waits = on`\n\n— Postgres log mọi lock chờ vượt `deadlock_timeout`\n\n(mặc định 1s). Cùng với `log_line_prefix`\n\nchứa `%a`\n\n, đủ để truy ngược application gây block.`deadlock_timeout`\n\n— giữ mặc định 1s; giảm thấp hơn làm Postgres tốn CPU dò cycle, cao hơn làm deadlock kéo dài.`idle_in_transaction_session_timeout`\n\n— chống transaction ôm row lock vì code rớt qua boundary.`lock_timeout`\n\n— set ở session-level cho job nhạy cảm, tránh transaction đợi vô hạn.Metric tối thiểu phải có:\n\n`deadlocks`\n\ntừ `pg_stat_database`\n\n— tăng đều = code đang lock không cùng thứ tự, phải sửa.`wait_event = 'transactionid'`\n\n— vượt baseline = hot row.`max(now() - xact_start)`\n\ncho session ở `state = 'idle in transaction'`\n\n) — nguồn gốc hầu hết row lock incident.`pg_locks`\n\n`locktype = 'transactionid'`\n\njoin với `pg_stat_activity.query`\n\nđể gắn vào endpoint/application_name) — `postgres_exporter`\n\n(Prometheus) phơi sẵn `pg_locks`\n\nseries.Rule phòng ngừa: mọi code path lock nhiều row phải sort id trước; mọi `FOR UPDATE`\n\nđi kèm `lock_timeout`\n\nhoặc `statement_timeout`\n\n; alert đồng thời trên deadlock counter + transactionid wait count + xact_age.\n\n`FOR UPDATE`\n\nđổi **throughput** lấy **consistency**: tuần tự hoá truy cập tới cùng row, transaction đụng row nóng xếp hàng thay vì chạy song song. Cái giá thấy rõ nhất trên hot row (account VIP, inventory bestseller, settings global) — throughput cap bằng `1 / (thời gian giữ lock trung bình)`\n\n. Cái lợi là semantic rõ: không lost update, không cần retry layer như optimistic locking. So với optimistic (`UPDATE ... WHERE version = $1`\n\nrồi check rows-affected), `FOR UPDATE`\n\nthắng khi contention thấp tới trung bình và transaction ngắn (đỡ retry, đỡ kéo workflow nghiệp vụ); optimistic thắng khi contention cao nhưng phần lớn lần đọc không dẫn tới ghi (đa số transaction không cần đợi). So với raw atomic `UPDATE`\n\n(gộp đọc-tính-ghi thành một statement): atomic luôn rẻ hơn nếu logic biểu diễn được trong SQL — `FOR UPDATE`\n\nchỉ dùng khi phải có một quãng application logic giữa đọc và ghi. Quy tắc thực tế: **ưu tiên một UPDATE nguyên tử (SET col = col + delta) nếu được; cần đọc-tính-ghi với logic application thì dùng FOR UPDATE (lock càng hẹp càng tốt, FOR NO KEY UPDATE thay FOR UPDATE khi không sửa key, SKIP LOCKED cho queue, NOWAIT cho real-time path); và luôn sort id trước khi lock nhiều row để tránh deadlock**.\n\n`SELECT ... FOR UPDATE`\n\ndùng khi nào? Nó khác`UPDATE`\n\nthường ở chỗ nào, và vì sao chỉ tăng isolation lên Repeatable Read không thay thế được nó?\n\n`FOR UPDATE`\n\nlà pessimistic row-level lock: nó ghi xid hiện tại vào `xmax`\n\ncủa tuple và ép mọi transaction khác chạm cùng row (`UPDATE`\n\n, `DELETE`\n\n, hay `SELECT ... FOR UPDATE/SHARE`\n\nkhác) đăng ký một heavyweight `transactionid`\n\nlock để đợi tới khi mình `COMMIT`\n\n/`ROLLBACK`\n\n. Dùng nó khi có pattern **read-modify-write** mà phần \"modify\" phải nằm trên application code — đọc balance, gọi service ngoài hay format message, rồi mới ghi — và không gộp được vào một `UPDATE`\n\nnguyên tử kiểu `SET balance = balance - 100`\n\n. Class bug nó chặn là **lost update**: hai transaction Read Committed cùng đọc balance = 1000, cùng tính 900, cùng `UPDATE`\n\n, kết quả còn 900 trong khi đáng lẽ phải là 800; cả hai đều \"thành công\" và không có lỗi. Khác `UPDATE`\n\nthường: `UPDATE`\n\ncũng lock row trước khi ghi (cùng cơ chế `xmax`\n\n), nhưng nó chỉ lock tại thời điểm ghi và Postgres tự đọc lại row (re-check visibility) — nếu logic tính giá trị mới nằm ngoài SQL, `UPDATE`\n\nthường không bảo vệ được. Vì sao Repeatable Read không thay thế: ở Repeatable Read, transaction giữ snapshot từ statement đầu tiên, khi `UPDATE`\n\nchạm row đã bị transaction khác commit, Postgres không \"đọc bản mới\" mà ném `ERROR: could not serialize access`\n\n(SQLSTATE `40001`\n\n) — application phải có retry layer; `FOR UPDATE`\n\nở Read Committed thay vào đó **đợi** rồi tự đọc lại bản mới, transaction tiếp tục chạy bình thường, đơn giản hơn ở tầng code. Hậu quả production đi kèm phải nêu hai cái: (1) hot row contention — throughput cap bằng `1 / thời gian giữ lock`\n\n, hot account/inventory thành bottleneck; (2) deadlock khi nhiều code path lock nhiều row theo thứ tự khác nhau, xử bằng quy tắc \"luôn sort id tăng dần trước khi lock\" (`SELECT ... WHERE id IN (...) ORDER BY id FOR UPDATE`\n\n). Điểm ăn điểm là gọi tên `xmax`\n\n+ `transactionid`\n\nwait, phân biệt `FOR UPDATE`\n\nvs Repeatable Read về cơ chế xử lý conflict (đợi vs lỗi serialize), và gắn được `FOR NO KEY UPDATE`\n\n/ `SKIP LOCKED`\n\n/ `NOWAIT`\n\nvào đúng use case.\n\nMục tiêu: dựng Postgres, tái hiện lost update khi không khoá, sửa bằng `FOR UPDATE`\n\n, rồi tái hiện deadlock vì lock row sai thứ tự và sửa bằng quy tắc sort id.\n\nDựng container và seed dữ liệu:\n\n```\ndocker run -d --name pg-forupd \\\n  -e POSTGRES_PASSWORD=pw -p 5438:5432 \\\n  postgres:16 \\\n  -c log_lock_waits=on -c deadlock_timeout=1s \\\n  -c log_line_prefix='%m [%p] %a '\nsleep 5\ndocker exec -i pg-forupd psql -U postgres <<'SQL'\nCREATE TABLE accounts (\n  id bigint PRIMARY KEY, owner text NOT NULL,\n  balance numeric(12,2) NOT NULL CHECK (balance >= 0)\n);\nINSERT INTO accounts VALUES (42,'alice',1000.00),(99,'bob',1000.00);\nSQL\n```\n\nMở hai session psql (A và B), đặt `application_name`\n\nđể query `pg_stat_activity`\n\nrõ ràng:\n\n```\ndocker exec -it pg-forupd psql \"postgresql://postgres:pw@localhost/postgres?application_name=sessA\"\ndocker exec -it pg-forupd psql \"postgresql://postgres:pw@localhost/postgres?application_name=sessB\"\n```\n\n**Bước 1 — tái hiện lost update (không FOR UPDATE).** Chạy xen kẽ:\n\n```\n-- A:  BEGIN; SELECT balance FROM accounts WHERE id=42;       -- 1000\n-- B:  BEGIN; SELECT balance FROM accounts WHERE id=42;       -- 1000\n-- B:  UPDATE accounts SET balance=900 WHERE id=42; COMMIT;\n-- A:  UPDATE accounts SET balance=900 WHERE id=42; COMMIT;\n-- check: SELECT balance FROM accounts WHERE id=42;            -- 900, đáng lẽ 800\n```\n\n100 đồng biến mất, không lỗi gì — đó là lost update.\n\n**Bước 2 — sửa bằng FOR UPDATE.** Reset rồi lặp lại:\n\n```\nUPDATE accounts SET balance=1000 WHERE id=42;\n\n-- A:  BEGIN; SELECT balance FROM accounts WHERE id=42 FOR UPDATE;   -- ôm row lock\n-- B:  BEGIN; SELECT balance FROM accounts WHERE id=42 FOR UPDATE;   -- treo\n```\n\nTrong khi B đợi, mở session watcher quan sát:\n\n```\nSELECT a.pid, a.application_name, a.wait_event_type, a.wait_event,\n       pg_blocking_pids(a.pid) AS blocked_by\nFROM pg_stat_activity a\nWHERE a.application_name IN ('sessA','sessB');\n\nSELECT pid, locktype, transactionid, mode, granted\nFROM pg_locks WHERE locktype IN ('transactionid','tuple');\n```\n\n`sessB`\n\nở `wait_event = 'transactionid'`\n\n, `blocked_by`\n\nchứa pid của `sessA`\n\n; `pg_locks`\n\ncó một entry `locktype='transactionid'`\n\n, `granted=false`\n\n. Tiếp:\n\n```\n-- A:  UPDATE accounts SET balance=balance-100 WHERE id=42; COMMIT;\n-- B (tự chạy tiếp): UPDATE accounts SET balance=balance-100 WHERE id=42; COMMIT;\n-- check: SELECT balance FROM accounts WHERE id=42;            -- 800, đúng\n```\n\n**Bước 3 — deadlock vì lock sai thứ tự.** Reset balance về 1000 cho cả hai row, rồi:\n\n```\n-- A: BEGIN; UPDATE accounts SET balance=balance-50 WHERE id=42;   -- lock 42\n-- B: BEGIN; UPDATE accounts SET balance=balance-50 WHERE id=99;   -- lock 99\n-- B: UPDATE accounts SET balance=balance+50 WHERE id=42;          -- đợi A\n-- A: UPDATE accounts SET balance=balance+50 WHERE id=99;          -- đợi B → cycle\n-- Sau ~1s: một bên nhận ERROR: deadlock detected, rollback.\n```\n\nXem log để thấy format chuẩn cho alert (process X waits for ... blocked by process Y):\n\n```\ndocker logs pg-forupd 2>&1 | grep -i 'deadlock\\|still waiting' | tail\n```\n\n**Bước 4 — sửa bằng quy tắc sort id tăng dần.** Cả hai code path cùng lock theo `ORDER BY id`\n\n:\n\n```\n-- A (chuyển 50 từ 42 → 99):\nBEGIN;\nSELECT id FROM accounts WHERE id IN (42,99) ORDER BY id FOR UPDATE;\nUPDATE accounts SET balance=balance-50 WHERE id=42;\nUPDATE accounts SET balance=balance+50 WHERE id=99;\nCOMMIT;\n\n-- B (chuyển 50 từ 99 → 42): cùng pattern lock id tăng dần\nBEGIN;\nSELECT id FROM accounts WHERE id IN (42,99) ORDER BY id FOR UPDATE;\nUPDATE accounts SET balance=balance+50 WHERE id=42;\nUPDATE accounts SET balance=balance-50 WHERE id=99;\nCOMMIT;\n```\n\nCả hai đua lock theo cùng thứ tự (42 trước, 99 sau): bên nào lấy được 42 trước thì bên kia đợi tại 42, không có cycle.\n\n**Bước 5 — SKIP LOCKED cho job queue.** Tạo bảng\n\n`jobs`\n\nvà bắn hai worker:\n\n```\nCREATE TABLE jobs (\n  id bigserial PRIMARY KEY,\n  status text NOT NULL DEFAULT 'pending',\n  payload jsonb NOT NULL,\n  created_at timestamptz NOT NULL DEFAULT now()\n);\nINSERT INTO jobs(payload) SELECT jsonb_build_object('n',g) FROM generate_series(1,100) g;\n\n-- Chạy ở cả A và B, không COMMIT:\nBEGIN;\nWITH next_jobs AS (\n  SELECT id FROM jobs WHERE status='pending'\n  ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 10\n)\nUPDATE jobs SET status='processing'\nWHERE id IN (SELECT id FROM next_jobs) RETURNING id;\n```\n\nHai worker pick id khác nhau. Bỏ `SKIP LOCKED`\n\n, worker thứ hai treo đợi worker thứ nhất — đó là khác biệt giữa queue parallel và queue serial.\n\nDọn dẹp: `docker rm -f pg-forupd`\n\n.", "url": "https://wpnews.pro/news/row-lock-for-update", "canonical_source": "https://dev.to/nguoidungai/row-lock-for-update-2128", "published_at": "2026-07-07 15:49:33+00:00", "updated_at": "2026-07-07 15:58:48.531856+00:00", "lang": "en", "topics": ["large-language-models", "developer-tools"], "entities": ["PostgreSQL", "Postgres"], "alternates": {"html": "https://wpnews.pro/news/row-lock-for-update", "markdown": "https://wpnews.pro/news/row-lock-for-update.md", "text": "https://wpnews.pro/news/row-lock-for-update.txt", "jsonld": "https://wpnews.pro/news/row-lock-for-update.jsonld"}}