JDBC Lock acquire optimization

Thanks to the pair of `CLIENT_ID`/`CREATED_DATE`,
it is possible to know if the lock is expired, already held or can be reacquire.
I think that the pre delete row is unnecessary and redundant with these two columns.
The rows doesn't need to be purged because the number of row in the table is finite and equal to the number of locks.
At worst, the table will be able to be purged by `deleteExpired()`.

I propose to overwrite these two columns at acquire lock time instead of pre delete the row.
So `delete` then `update` or `insert` become `update` or `insert`.

One client held a lock.
- Another client will be able to hold the same lock only if:
1. the row doesn't exist. Done when the first client will call `unlock()`.
2. the lock expire in case of hardware shutdown. Done by "`OR CREATED_DATE<?`" in `updateQuery`.
- The same client will be able to reacquire it only if:
1. the row doesn't exist.
2. the lock is already held by him. Done by "`CLIENT_ID=? OR`" in `updateQuery`.

**Cost with PostgreSQL:**
```
EXPLAIN DELETE FROM INT_LOCK WHERE REGION='DEFAULT' AND LOCK_KEY='FOO' AND CREATED_DATE<'2020-07-14';
Delete on int_lock  (cost=0.14..8.17 rows=1 width=6)
  ->  Index Scan using int_lock_pk on int_lock  (cost=0.14..8.17 rows=1 width=6)
        Index Cond: (((region)::text = 'DEFAULT'::text) AND (lock_key = 'FOO'::bpchar))
        Filter: (created_date < '2020-07-14 00:00:00'::timestamp without time zone)

EXPLAIN UPDATE INT_LOCK SET CREATED_DATE='2020-07-15' WHERE REGION='DEFAULT' AND LOCK_KEY='FOO' AND CLIENT_ID=NULL;
Update on int_lock  (cost=0.00..11.40 rows=1 width=520)
  ->  Result  (cost=0.00..11.40 rows=1 width=520)
        One-Time Filter: false
        ->  Seq Scan on int_lock  (cost=0.00..11.40 rows=1 width=520)
```
```
EXPLAIN UPDATE INT_LOCK SET CLIENT_ID=NULL, CREATED_DATE='2020-07-15' WHERE REGION='DEFAULT' AND LOCK_KEY='FOO' AND (CLIENT_ID=NULL OR CREATED_DATE<'2020-07-14')
Update on int_lock  (cost=0.14..8.17 rows=1 width=372)
  ->  Index Scan using int_lock_pk on int_lock  (cost=0.14..8.17 rows=1 width=372)
        Index Cond: (((region)::text = 'DEFAULT'::text) AND (lock_key = 'FOO'::bpchar))
        Filter: (created_date < '2020-07-14 00:00:00'::timestamp without time zone)
```
This commit is contained in:
Alexandre Strubel
2020-07-13 11:49:54 +02:00
committed by Artem Bilan
parent 9c252be028
commit e4611d97a1
3 changed files with 42 additions and 10 deletions

View File

@@ -36,10 +36,13 @@ import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import javax.sql.DataSource;
/**
* @author Dave Syer
* @author Artem Bilan
* @author Stefan Vassilev
* @author Alexandre Strubel
*
* @since 4.3
*/
@@ -55,6 +58,9 @@ public class JdbcLockRegistryTests {
@Autowired
private LockRepository client;
@Autowired
private DataSource dataSource;
@BeforeEach
public void clear() {
this.registry.expireUnusedOlderThan(0);
@@ -127,6 +133,26 @@ public class JdbcLockRegistryTests {
}
}
@Test
public void testReentrantLockAfterExpiration() throws Exception {
DefaultLockRepository client = new DefaultLockRepository(dataSource);
client.setTimeToLive(1);
client.afterPropertiesSet();
JdbcLockRegistry registry = new JdbcLockRegistry(client);
Lock lock1 = registry.obtain("foo");
assertThat(lock1.tryLock()).isTrue();
Thread.sleep(100);
try {
Lock lock2 = registry.obtain("foo");
assertThat(lock2).isSameAs(lock1);
assertThat(lock2.tryLock()).isTrue();
lock2.unlock();
}
finally {
lock1.unlock();
}
}
@Test
public void testTwoLocks() throws Exception {
for (int i = 0; i < 10; i++) {