This page describes how to use the FOR UPDATE clause in serializable
isolation.
The locking mechanism of the FOR UPDATE clause is different for
repeatable read and
serializable isolation. Using
serializable isolation, when you use the SELECT query to scan a table, adding
a FOR UPDATE clause enables exclusive locks at the intersection of the
row-and-column granularity level, otherwise known as cell-level. The lock
remains in place for the lifetime of the read-write transaction. During this
time, the FOR UPDATE clause prevents other transactions from modifying the
locked cells until the current transaction completes.
To learn how to use the FOR UPDATE clause, see the
GoogleSQL
and PostgreSQL
FOR UPDATE reference guides.
Why use the FOR UPDATE clause
In databases with less strict isolation levels, the FOR UPDATE clause might be
necessary to ensure that a concurrent transaction doesn't update data between
reading the data and committing the transaction. Since Spanner
enforces serializability by default, it's guaranteed that the transaction only
commits successfully if the data accessed within the transaction isn't stale at
commit time. Therefore, the FOR UPDATE clause isn't necessary to ensure
transaction correctness in Spanner.
However, in use cases with high write contention, such as when multiple transactions are concurrently reading and writing to the same data, the simultaneous transactions might cause an increase in aborts. This is because when multiple, simultaneous transactions acquire shared locks, and then try to upgrade to exclusive locks, the transactions cause a deadlock. The deadlock permanently blocks the transactions because each is waiting for the other to release the resource that it needs. In order to make progress, Spanner aborts all but one of the transactions to resolve the deadlock. For more information, see Locking.
A transaction that uses the FOR UPDATE clause acquires the exclusive lock
proactively and proceeds to execute, while other transactions wait their turn
for the lock. Although Spanner might still limit throughput
because the conflicting transactions can only be performed one at a time, but
because Spanner is only making progress on one transaction, it
saves time that would otherwise be spent aborting and retrying transactions.
Therefore, if reducing the number of aborted transactions in a simultaneous
write request scenario is important, then you can use the FOR UPDATE clause to
reduce the overall number of aborts and increase workload execution efficiency.
Comparison to the LOCK_SCANNED_RANGES hint
The FOR UPDATE clause serves a similar function as the
LOCK_SCANNED_RANGES=exclusive
hint.
There are two key differences:
If you use the
LOCK_SCANNED_RANGEShint, the transaction acquires exclusive locks on the scanned ranges for the entire statement. You can't acquire exclusive locks on a subquery. Using the lock hint might result in acquiring more locks than necessary and contributing to lock contention in the workload. The following example shows how to use a lock hint:@{lock_scanned_ranges=exclusive} SELECT s.SingerId, s.FullName FROM Singers AS s JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000) AS a ON a.SingerId = s.SingerId;On the other hand, you can use the
FOR UPDATEclause in a subquery as shown in the following example:SELECT s.SingerId, s.FullName FROM Singers AS s JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000) FOR UPDATE AS a ON a.SingerId = s.SingerId;You can use the
LOCK_SCANNED_RANGEShint in DML statements whereas you can only use theFOR UPDATEclause inSELECTstatements.
Lock semantics
To reduce simultaneous write requests and the cost of transactions being aborted
as a result of deadlock, Spanner locks data at the cell-level if
possible. The cell-level is the most granular level of data within a table - a
data point at the intersection of a row and a column. When using the
FOR UPDATE clause, Spanner locks specific cells that are
scanned by the SELECT query.
In the following example, the MarketingBudget cell in the SingerId = 1 and
AlbumId = 1 row is exclusively locked in the Albums table, preventing
concurrent transactions from modifying that cell until this transaction is
committed or rolled back. However, concurrent transactions can still update the
AlbumTitle cell in that row.
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1
FOR UPDATE;
Concurrent transactions might block on reading locked data
When one transaction has acquired exclusive locks on a scanned range, concurrent transactions might block reading that data. Spanner enforces serializability so data can only be read if it is guaranteed to be unchanged by another transaction within the lifetime of the transaction. Concurrent transactions that attempt to read already locked data might have to wait until the transaction holding the locks is committed, rolled back, or times out.
In the following example, Transaction 1 locks the MarketingBudget cells for
1 <= AlbumId < 5.
-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;
Transaction 2, which is attempting to read the MarketingBudget for
AlbumId = 1, is blocked until Transaction 1 either commits or is rolled
back.
-- Transaction 2
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1;
-- Blocked by Transaction 1
Similarly, a transaction attempting to lock a scanned range with FOR UPDATE is
blocked by a concurrent transaction that locks an overlapping scanned range.
Transaction 3 in the following example is also blocked since Transaction 1
has locked the MarketingBudget cells for 3 <= AlbumId < 5, which is the
overlapping scanned range with Transaction 3.
-- Transaction 3
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 3 and AlbumId < 10
FOR UPDATE;
-- Blocked by Transaction 1
Read an index
A concurrent read might not be blocked if the query that locked the scanned range locks the rows in the base table, but the concurrent transaction reads from an index.
The following Transaction 1 locks the SingerId and SingerInfo cells for
SingerId = 1.
-- Transaction 1
SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = 1
FOR