READ COMMITTED isolation level
Introduction
READ COMMITTED is one of the three isolation levels in PostgreSQL and also its default. A unique property of this isolation level is: clients don’t need retry logic for serialization errors (40001) in applications when using this isolation level.
The other two isolation levels (SERIALIZABLE and REPEATABLE READ) require apps to have retry logic for serialization errors. READ COMMITTED in PostgreSQL works around conflicts by allowing single statements to work on an “inconsistent snapshot” (i.e., non-conflicting rows are read as of the statement’s snapshot but conflict resolution is done by reading and attempting re-execution/ locking on the latest version of the row).
YSQL now supports READ COMMITTED isolation level as well. The behaviour will be the same as that of PostgreSQL’s READ COMMITTED level as mentioned in section 13.2.1 here. Notable points from the section are listed again in the Semantics section below.
Semantics
To support READ COMMITTED isolation level in YSQL with the same semantics as PostgreSQL, the following requirements follow -
- SELECTs (without explicit row locking)
- New read point is chosen at statement start that includes anything that committed before the query began.
- Data from updates by previous statements in same transaction is visible.
- UPDATE, DELETE, SELECT FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE, FOR KEY SHARE (the last two are not mentioned in PostgreSQL documentation but the same behavior is seen for these as below.)
-
New read point is chosen at statement start that includes anything that committed before the query began.
-
If the row of interest -
- is being updated (or deleted) by other transactions in a conflicting way (i.e., read time of statement falls within read time of other transactions to current time), wait for them to commit/rollback and then perform recheck steps (see below).
- has been updated (or deleted) by other transactions in a conflicting way (i.e., read time of statement falls within read time to commit time of other transactions), perform recheck steps.
- has been locked by other transactions in a conflicting way, wait for them to commit/ rollback and then perform recheck steps.
Recheck steps:
- If a row is deleted, ignore it
- Apply update/ acquire lock on updated version of row if where clause evaluates to true on the updated version of row. (Note that the updated version of a row could have a different pk as well - this implies PostgreSQL follows the chain of updates for a row even across pk changes).
-
- INSERT
- ON CONFLICT DO UPDATE: if a conflict occurs, wait for the conflicting transaction to commit/ rollback.
- On rollback, proceed as usual
- On commit, modify the new version of row
- ON CONFLICT DO NOTHING: do nothing if a conflict occurs
- ON CONFLICT DO UPDATE: if a conflict occurs, wait for the conflicting transaction to commit/ rollback.
Apart from the above requirements, there is a YSQL specific requirement: ensure that external clients don’t face kReadRestart errors.
Usage
By setting the gflag yb_enable_read_committed_isolation=true
, the READ COMMITTED isolation in YSQL will actually map to the READ COMMITTED implementation in docdb. If set to false, it will have the earlier behaviour of mapping READ COMMITTED to REPEATABLE READ.
The following ways can be used to start a READ COMMITTED transaction after setting the gflag:
START TRANSACTION isolation level read committed [read write | read only];
BEGIN [TRANSACTION] isolation level read committed [read write | read only];
BEGIN [TRANSACTION]; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN [TRANSACTION]; SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;
READ COMMITTED on YSQL will have pessimistic locking behaviour i.e., a READ COMMITTED transaction
will wait for other READ COMMITTED transactions to commit/ rollback in case of a conflict. Two or
more transactions could be waiting on each other in a cycle. Hence, to avoid a deadlock, make sure
to configure a statement timeout (by setting the statement_timeout
parameter in ysql_pg_conf_csv
tserver gflag on cluster startup). Statement timeouts will help avoid deadlocks (see example 1).
Examples
create table test (k int primary key, v int);
1) Deadlocks are avoided in READ COMMITTED by relying on statement timeout.
truncate table test;
insert into test values (1, 5);
insert into test values (2, 5);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2) Behavior of SELECT (without explicit locking)
truncate table test;
insert into test values (1, 5);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3) Behavior of UPDATE
truncate table test;
insert into test values (0, 5), (1, 5), (2, 5), (3, 5), (4, 1);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4) Behavior of SELECT FOR UPDATE
truncate table test;
insert into test values (0, 5), (1, 5), (2, 5), (3, 5), (4, 1);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5) Behavior of INSERTs
i) insert new key that is also just changed by another transaction
truncate table test;
insert into test values (1, 1);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
ii) Same as i) but with ON CONFLICT
truncate table test;
insert into test values (1, 1);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
iii) INSERT old key that is removed by other transaction
truncate table test;
insert into test values (1, 1);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
iv) same with iii) but with ON CONFLICT
truncate table test;
insert into test values (1, 1);
Client 1 | Client 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Cross feature interaction
This feature interacts with the following features:
- Follower reads (integration in progress): When follower reads is turned on, the read point for each statement in a READ COMMITTED transaction will be picked as Now() - yb_follower_read_staleness_ms (if the transaction/statement is known to be explicitly/ implicitly read only).
- Pessimistic locking: READ COMMITTED has a dependency on pessimistic locking to fully work. To be precise, on facing a conflict, a transaction has to wait for the conflicting transaction to rollback/commit. Pessimistic locking behaviour can be seen for READ COMMITTED. An optimized version of pessimistic locking will come in near future, which will give better performance and will also work for REPEATABLE READ and SERIALIZABLE isolation levels. The optimized version will also help detect deadlocks proactively instead of relying on statement timeouts for deadlock avoidance (see example 1).
Limitations
-
READ COMMITTED semantics ensure that the client doesn't face conflict and read restart errors. YSQL maintains these semantics as long as a statement's output doesn't exceed
ysql_output_buffer_size
(a gflag with a default of 256KB). If this condition is not met, YSQL will resort to optimistic locking for that statement. -
PostgreSQL requires the following as mentioned in its docs here - "STABLE and IMMUTABLE functions use a snapshot established as of the start of the calling query, whereas VOLATILE functions obtain a fresh snapshot at the start of each query they execute." YSQL uses a single snapshot for the whole procedure instead of one for each statement in the procedure.
Work is in progress to get rid of both limitations.
Noteworthy Considerations
- This isolation level allows both phantom and non-repeatable reads (example 2).
- Adding this new isolation level won’t affect the performance of existing isolation levels.
- Tuning for performance:
If a statement in the READ COMMITTED isolation level faces a conflict, it will be retried with
exponential backoff till the statement times out. There are three parameters that control the
backoff:
- retry_max_backoff: the maximum backoff in milliseconds between retries.
- retry_min_backoff: the minimum backoff in milliseconds between retries.
- retry_backoff_multiplier: the multiplier used to calculate the next retry backoff.
These parameters can be set on a per-session basis or in the ysql_pg_conf_csv
tserver gflag on cluster startup.
Once the optimized version of pessimistic locking (as described in section 5) is completed, there won't be a need to hand tune these parameters for performance. Statements will restart only when all conflicting transactions have committed or rolled back (instead of retrying with an exponential backoff).