Pessimistic locking is useful every now and then when you explicitly want to lock a given set of rows for a subsequent UPDATE, or for database-based inter-process synchronisation and locking. Normally, you would use the SQL Standard FOR UPDATE clause, which is supported in most databases.
A SQL standard example
-- This statement will produce a row-lock on three rows -- ... or fail / timeout if the row-lock cannot be acquired SELECT * FROM author WHERE id IN (3, 4, 5) FOR UPDATE -- This statement will produce cell-locks on two cells in three rows -- ... or fail / timeout if the cell-lock cannot be acquired SELECT * FROM author WHERE id in (3, 4, 5) FOR UPDATE OF first_name, last_name
An Oracle syntax extension example
Oracle ships with some useful extensions to the FOR UPDATE clause:
-- This statement will produce a row-lock on three rows -- skipping all locked records. This is very nice for queue tables where -- you never want to fail on locks, but just skip to the next record SELECT * FROM author WHERE id IN (3, 4, 5) FOR UPDATE SKIP LOCKED
Simulation of the FOR UPDATE clause
SQL Server supports FOR UPDATE only in cursors (as defined in the SQL standard). It also ships with proprietary SQL syntax extensions, such as WITH (updlock), that has some very peculiar “features”, such as locking the whole page of a record instead of just one record. The CUBRID database on the other hand doesn’t support pessimistic locking at all in SQL.
But this can be simulated with JDBC, as JDBC allows for specifying the flags TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE for any statement, and then using ResultSet.updateXXX() methods to produce a cell-lock / row-lock. Here’s a simplified example in JDBC:
PreparedStatement stmt = connection.prepareStatement( "SELECT * FROM author WHERE id IN (3, 4, 5)", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet rs = stmt.executeQuery(); while (rs.next()) { // UPDATE the primary key for row-locks, or any other // columns for cell-locks rs.updateObject(1, rs.getObject(1)); rs.updateRow(); // Do more stuff with this record }
Drawbacks of this approach
The main drawback of this approach is the fact that the database has to maintain a scrollable cursor, whose records are locked one by one. This can cause a major risk of deadlocks or race conditions if the JDBC driver can recover from the unsuccessful locking, if two Java threads execute the following statements:
-- thread 1 SELECT * FROM author ORDER BY id ASC; -- thread 2 SELECT * FROM author ORDER BY id DESC;
So use this technique with care, possibly only ever locking single rows!
An example using jOOQ to abstract this simulation
The following example shows how jOOQ integration tests test pessimistic locking for all SQL dialects, including SQL Server and CUBRID:
// Two factories with different connections Factory create1 = // ... Factory create2 = // ... // A synchronized list to check for correct execution order final Vector<String> execOrder = new Vector<String>(); try { // This is the test case: // 0.0s: Both threads start // 0.0s: t1 sleeps for 2s // 0.0s: t2 locks the AUTHOR table // 0.1s: t2 sleeps for 4s // 2.0s: t1 blocks on the AUTHOR table (potentially failing here) // 4.0s: t2 commits and unlocks AUTHOR // 4.1s: t1 (if blocked) now locks the AUTHOR table // For simplicity, no ReentrantLocks and signals are used for // synchronisation, but simple Thread.sleep() calls. final Thread t1 = new Thread(new Runnable() { @Override public void run() { Thread.sleep(2000); execOrder.add("t1-block"); try { create1 .select(AUTHOR.ID) .from(AUTHOR) .forUpdate() .fetch(); } // Some databases fail on locking, others lock for a while catch (Exception ignore) { } finally { execOrder.add("t1-fail-or-t2-commit"); } } }); final Thread t2 = new Thread(new Runnable() { @Override public void run() { execOrder.add("t2-exec"); Result<?> result2 = create2 .selectFrom(AUTHOR) .forUpdate() .fetch(); assertEquals(2, result2.size()); execOrder.add("t2-signal"); try { Thread.sleep(4000); } catch (Exception ignore) { } execOrder.add("t1-fail-or-t2-commit"); try { create2.getConnection().commit(); } catch (Exception e) {} } }); t1.start(); t2.start(); t1.join(); t2.join(); assertEquals(asList( "t2-exec", "t2-signal", "t1-block", "t1-fail-or-t2-commit", "t1-fail-or-t2-commit"), execOrder); }
The above test shows how the FOR UPDATE clause is implemented or simulated for most databases in jOOQ. Some databases use a timeout on the lock acquisition before failing, while others fail immediately (Oracle allows for specifying that in the FOR UPDATE WAIT / NOWAIT clause)
Filed under: java, sql Tagged: FOR UPDATE, java, JDBC, pessimistic locking, sql
