Skip to content

Commit e4611d9

Browse files
Alexandre Strubelartembilan
authored andcommitted
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) ```
1 parent 9c252be commit e4611d9

File tree

3 files changed

+42
-10
lines changed

3 files changed

+42
-10
lines changed

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,21 @@ public class DefaultLockRepository implements LockRepository, InitializingBean {
7373

7474
private String deleteQuery = "DELETE FROM %sLOCK WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?";
7575

76-
private String deleteExpiredQuery = "DELETE FROM %sLOCK WHERE REGION=? AND LOCK_KEY=? AND CREATED_DATE<?";
76+
private String deleteExpiredQuery = "DELETE FROM %sLOCK WHERE REGION=? AND CREATED_DATE<?";
7777

7878
private String deleteAllQuery = "DELETE FROM %sLOCK WHERE REGION=? AND CLIENT_ID=?";
7979

80-
private String updateQuery = "UPDATE %sLOCK SET CREATED_DATE=? WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?";
80+
private String updateQuery =
81+
"UPDATE %sLOCK SET CLIENT_ID=?, CREATED_DATE=? WHERE REGION=? AND LOCK_KEY=? " +
82+
"AND (CLIENT_ID=? OR CREATED_DATE<?)";
8183

8284
private String insertQuery = "INSERT INTO %sLOCK (REGION, LOCK_KEY, CLIENT_ID, CREATED_DATE) VALUES (?, ?, ?, ?)";
8385

8486
private String countQuery =
8587
"SELECT COUNT(REGION) FROM %sLOCK WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=? AND CREATED_DATE>=?";
8688

89+
private String renewQuery = "UPDATE %sLOCK SET CREATED_DATE=? WHERE REGION=? AND LOCK_KEY=? AND CLIENT_ID=?";
90+
8791
/**
8892
* Constructor that initializes the client id that will be associated for
8993
* all the locks persisted by the store instance to a random {@link UUID}.
@@ -142,6 +146,7 @@ public void afterPropertiesSet() {
142146
this.updateQuery = String.format(this.updateQuery, this.prefix);
143147
this.insertQuery = String.format(this.insertQuery, this.prefix);
144148
this.countQuery = String.format(this.countQuery, this.prefix);
149+
this.renewQuery = String.format(this.renewQuery, this.prefix);
145150
}
146151

147152
@Override
@@ -154,11 +159,11 @@ public void delete(String lock) {
154159
this.template.update(this.deleteQuery, this.region, lock, this.id);
155160
}
156161

157-
@Transactional(isolation = Isolation.SERIALIZABLE, timeout = 1)
162+
@Transactional(isolation = Isolation.SERIALIZABLE)
158163
@Override
159164
public boolean acquire(String lock) {
160-
deleteExpired(lock);
161-
if (this.template.update(this.updateQuery, new Date(), this.region, lock, this.id) > 0) {
165+
if (this.template.update(this.updateQuery, this.id, new Date(), this.region, lock, this.id,
166+
new Date(System.currentTimeMillis() - this.ttl)) > 0) {
162167
return true;
163168
}
164169
try {
@@ -171,19 +176,18 @@ public boolean acquire(String lock) {
171176

172177
@Override
173178
public boolean isAcquired(String lock) {
174-
deleteExpired(lock);
175179
return this.template.queryForObject(this.countQuery, Integer.class, // NOSONAR query never returns null
176180
this.region, lock, this.id, new Date(System.currentTimeMillis() - this.ttl)) == 1;
177181
}
178182

179-
private void deleteExpired(String lock) {
180-
this.template.update(this.deleteExpiredQuery, this.region, lock,
181-
new Date(System.currentTimeMillis() - this.ttl));
183+
@Override
184+
public void deleteExpired() {
185+
this.template.update(this.deleteExpiredQuery, this.region, new Date(System.currentTimeMillis() - this.ttl));
182186
}
183187

184188
@Override
185189
public boolean renew(String lock) {
186-
return this.template.update(this.updateQuery, new Date(), this.region, lock, this.id) > 0;
190+
return this.template.update(this.renewQuery, new Date(), this.region, lock, this.id) > 0;
187191
}
188192

189193
}

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/LockRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public interface LockRepository extends Closeable {
3434

3535
void delete(String lock);
3636

37+
void deleteExpired();
38+
3739
boolean acquire(String lock);
3840

3941
boolean renew(String lock);

spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/lock/JdbcLockRegistryTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@
3636
import org.springframework.test.annotation.DirtiesContext;
3737
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3838

39+
import javax.sql.DataSource;
40+
3941
/**
4042
* @author Dave Syer
4143
* @author Artem Bilan
4244
* @author Stefan Vassilev
45+
* @author Alexandre Strubel
4346
*
4447
* @since 4.3
4548
*/
@@ -55,6 +58,9 @@ public class JdbcLockRegistryTests {
5558
@Autowired
5659
private LockRepository client;
5760

61+
@Autowired
62+
private DataSource dataSource;
63+
5864
@BeforeEach
5965
public void clear() {
6066
this.registry.expireUnusedOlderThan(0);
@@ -127,6 +133,26 @@ public void testReentrantLockInterruptibly() throws Exception {
127133
}
128134
}
129135

136+
@Test
137+
public void testReentrantLockAfterExpiration() throws Exception {
138+
DefaultLockRepository client = new DefaultLockRepository(dataSource);
139+
client.setTimeToLive(1);
140+
client.afterPropertiesSet();
141+
JdbcLockRegistry registry = new JdbcLockRegistry(client);
142+
Lock lock1 = registry.obtain("foo");
143+
assertThat(lock1.tryLock()).isTrue();
144+
Thread.sleep(100);
145+
try {
146+
Lock lock2 = registry.obtain("foo");
147+
assertThat(lock2).isSameAs(lock1);
148+
assertThat(lock2.tryLock()).isTrue();
149+
lock2.unlock();
150+
}
151+
finally {
152+
lock1.unlock();
153+
}
154+
}
155+
130156
@Test
131157
public void testTwoLocks() throws Exception {
132158
for (int i = 0; i < 10; i++) {

0 commit comments

Comments
 (0)