PESSIMISTIC_READ and PESSIMISTIC_WRITE

Readers–writer lock

A database system is a highly concurrent environment, therefore many concurrency theory idioms apply to database access as well. Concurrent changes must be serialized to preserve data integrity, so most database systems use a two-phase locking strategy, even if it’s usually supplemented by a Multiversion concurrency control mechanism.

Because a mutual exclusion locking would hinder scalability (treating reads and writes equally), most database systems use a readers-writer locking synchronization scheme, so that:

  • A shared (read) lock blocks writers, allowing multiple readers to proceed
  • An exclusive (write) lock blocks both readers and writers, making all write operations be applied sequentially

Because the locking syntax is not part of the SQL Standard, each RDBMS has opted for a different syntax:

Database name Shared lock statement Exclusive lock statement
Oracle FOR UPDATE FOR UPDATE
MySQL LOCK IN SHARE MODE FOR UPDATE
Microsoft SQL Server WITH (HOLDLOCK, ROWLOCK) WITH (UPDLOCK, ROWLOCK)
PostgreSQL FOR SHARE FOR UPDATE
DB2 FOR READ ONLY WITH RS FOR UPDATE WITH RS

Java Persistence abstraction layer hides the database specific locking semantics, offering a common API that only requires two Lock Modes. The shared/read lock is acquired using the PESSIMISTIC_READ Lock Mode Type, and the exclusive/write lock is requested using PESSIMISTIC_WRITE instead.

PostgreSQL row-level lock modes

For the next test cases, we are going to use PostgreSQL for it supports both exclusive and share explicit locking.

All the following tests will use the same concurrency utility, emulating two users: Alice and Bob. Each test scenario will verify a specific read/write locking combination.

Case 1: PESSIMISTIC_READ doesn’t block PESSIMISTIC_READ lock requests

The first test will check how two concurrent PESSIMISTIC_READ lock requests interact:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPessimisticReadDoesNotBlockPessimisticRead() throws InterruptedException {
    LOGGER.info("Test PESSIMISTIC_READ doesn't block PESSIMISTIC_READ");
    testPessimisticLocking(
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product);
            LOGGER.info("PESSIMISTIC_READ acquired");
        },
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product);
            LOGGER.info("PESSIMISTIC_READ acquired");
        }
    );
}

Running this test, we get the following output:

In this scenario, there is no contention whatsoever. Both Alice and Bob can acquire a shared lock without running into any conflict.

Case 2: PESSIMISTIC_READ blocks UPDATE implicit lock requests

The second scenario will demonstrate how the shared lock prevents a concurrent modification. Alice will acquire a shared lock and Bob will attempt to modify the locked entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testPessimisticReadBlocksUpdate() throws InterruptedException {
    LOGGER.info("Test PESSIMISTIC_READ blocks UPDATE");
    testPessimisticLocking(
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product);
            LOGGER.info("PESSIMISTIC_READ acquired");
        },
        (session, product) -> {
            product.setDescription("USB Flash Memory Stick");
            session.flush();
            LOGGER.info("Implicit lock acquired");
        }
    );
}

The test generates this output:

While Bob could select the Product entity, the UPDATE is delayed up until Alice transaction is committed (that’s why the UPDATE took 427ms to run).

Case 3: PESSIMISTIC_READ blocks PESSIMISTIC_WRITE lock requests

The same behaviour is exhibited by a secondary PESSIMISTIC_WRITE lock request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPessimisticReadBlocksPessimisticWrite() throws InterruptedException {
    LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE");
    testPessimisticLocking(
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product);
            LOGGER.info("PESSIMISTIC_READ acquired");
        },
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product);
            LOGGER.info("PESSIMISTIC_WRITE acquired");
        }
    );
}

Giving the following output:

Bob’s exclusive lock request waits for Alice’s shared lock to be released.

Case 4: PESSIMISTIC_READ blocks PESSIMISTIC_WRITE lock requests, NO WAIT fails fast

Hibernate provides a PESSIMISTIC_NO_WAIT timeout directive, which translates to a database specific NO_WAIT lock acquire policy.

The PostgreSQL NO WAIT directive is described as follows:

To prevent the operation from waiting for other transactions to commit, use the NOWAIT option. With NOWAIT, the statement reports an error, rather than waiting, if a selected row cannot be locked immediately. Note that NOWAIT applies only to the row-level lock(s) — the required ROW SHARE table-level lock is still taken in the ordinary way (see Chapter 13). You can use LOCK with the NOWAIT option first, if you need to acquire the table-level lock without waiting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPessimisticReadWithPessimisticWriteNoWait() throws InterruptedException {
    LOGGER.info("Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast");
    testPessimisticLocking(
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product);
            LOGGER.info("PESSIMISTIC_READ acquired");
        },
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).setTimeOut(Session.LockRequest.PESSIMISTIC_NO_WAIT).lock(product);
            LOGGER.info("PESSIMISTIC_WRITE acquired");
        }
    );
}

This test generates the following output:

Since Alice already holds a shared lock on the Product entity associated database row, Bob’s exclusive lock request fails immediately.

Case 5: PESSIMISTIC_WRITE blocks PESSIMISTIC_READ lock requests

The next test proves that an exclusive lock will always blocks a shared lock acquire attempt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPessimisticWriteBlocksPessimisticRead() throws InterruptedException {
    LOGGER.info("Test PESSIMISTIC_WRITE blocks PESSIMISTIC_READ");
    testPessimisticLocking(
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product);
            LOGGER.info("PESSIMISTIC_WRITE acquired");
        },
        (session, product) -> {
            session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product);
            LOGGER.info("PESSIMISTIC_WRITE acquired");
        }
    );
}

Generating the following output:

Bob’s shared lock request waits for Alice’s transaction to end, so that all acquired locks are released.

Case 6: PESSIMISTIC_WRITE blocks PESSIMISTIC_WRITE lock requests

An exclusive lock blocks an exclusive lock as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPessimisticWriteBlocksPessimisticWrite() throws InterruptedException {
    LOGGER.info("Test PESSIMISTIC_WRITE blocks PESSIMISTIC_WRITE");
    testPessimisticLocking(
            (session, product) -> {
                session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product);
                LOGGER.info("PESSIMISTIC_WRITE acquired");
            },
            (session, product) -> {
                session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product);
                LOGGER.info("PESSIMISTIC_WRITE acquired");
            }
    );
}

The test generates this output:

Bob’s exclusive lock request has to wait for Alice to release its lock.

Conclusion

Relational database systems use locks for preserving ACID guarantees, so it’s important to understand how shared and exclusive row-level locks inter-operate. An explicit pessimistic lock is a very powerful database concurrency control mechanism and you might even use it for fixing an optimistic locking race condition.

FROM HERE

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s