Skip to content

Commit 04c29f4

Browse files
mhyeon-leeschauder
mhyeon-lee
authored andcommitted
DATAJDBC-493 - Avoids deadlocks by acquiring lock on aggregate root table.
Introduces infrastructure to obtain locks and uses them to acquire locks on the table of the aggregate root before deleting references. Without this lock deletes access non root entities before the aggregate root, which is the opposite order of updates and thus may cause deadlocks. Original pull request: #196.
1 parent fa8b95c commit 04c29f4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1364
-57
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* Executes an {@link MutableAggregateChange}.
2828
*
2929
* @author Jens Schauder
30+
* @author Myeonghyeon Lee
3031
* @since 2.0
3132
*/
3233
class AggregateChangeExecutor {
@@ -77,6 +78,10 @@ private void execute(DbAction<?> action, JdbcAggregateChangeExecutionContext exe
7778
executionContext.executeDeleteRoot((DbAction.DeleteRoot<?>) action);
7879
} else if (action instanceof DbAction.DeleteAllRoot) {
7980
executionContext.executeDeleteAllRoot((DbAction.DeleteAllRoot<?>) action);
81+
} else if (action instanceof DbAction.AcquireLockRoot) {
82+
executionContext.executeAcquireLock((DbAction.AcquireLockRoot<?>) action);
83+
} else if (action instanceof DbAction.AcquireLockAllRoot) {
84+
executionContext.executeAcquireLockAllRoot((DbAction.AcquireLockAllRoot<?>) action);
8085
} else {
8186
throw new RuntimeException("unexpected action");
8287
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@
4242
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
4343
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
4444
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
45+
import org.springframework.data.relational.core.sql.LockMode;
4546
import org.springframework.data.util.Pair;
4647
import org.springframework.lang.Nullable;
4748
import org.springframework.util.Assert;
4849

4950
/**
5051
* @author Jens Schauder
5152
* @author Umut Erturk
53+
* @author Myeonghyeon Lee
5254
*/
5355
class JdbcAggregateChangeExecutionContext {
5456

@@ -164,6 +166,14 @@ <T> void executeMerge(DbAction.Merge<T> merge) {
164166
}
165167
}
166168

169+
<T> void executeAcquireLock(DbAction.AcquireLockRoot<T> acquireLock) {
170+
accessStrategy.acquireLockById(acquireLock.getId(), LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType());
171+
}
172+
173+
<T> void executeAcquireLockAllRoot(DbAction.AcquireLockAllRoot<T> acquireLock) {
174+
accessStrategy.acquireLockAll(LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType());
175+
}
176+
167177
private void add(DbActionExecutionResult result) {
168178
results.put(result.getAction(), result);
169179
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
* @author Thomas Lang
5151
* @author Christoph Strobl
5252
* @author Milan Milanov
53+
* @author Myeonghyeon Lee
5354
*/
5455
public class JdbcAggregateTemplate implements JdbcAggregateOperations {
5556

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.domain.Sort;
2525
import org.springframework.data.mapping.PersistentPropertyPath;
2626
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
27+
import org.springframework.data.relational.core.sql.LockMode;
2728

2829
/**
2930
* Delegates each methods to the {@link DataAccessStrategy}s passed to the constructor in turn until the first that does
@@ -33,6 +34,7 @@
3334
* @author Mark Paluch
3435
* @author Tyler Van Gorder
3536
* @author Milan Milanov
37+
* @author Myeonghyeon Lee
3638
* @since 1.1
3739
*/
3840
public class CascadingDataAccessStrategy implements DataAccessStrategy {
@@ -115,6 +117,24 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
115117
collectVoid(das -> das.deleteAll(propertyPath));
116118
}
117119

120+
/*
121+
* (non-Javadoc)
122+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
123+
*/
124+
@Override
125+
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
126+
collectVoid(das -> das.acquireLockById(id, lockMode, domainType));
127+
}
128+
129+
/*
130+
* (non-Javadoc)
131+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
132+
*/
133+
@Override
134+
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
135+
collectVoid(das -> das.acquireLockAll(lockMode, domainType));
136+
}
137+
118138
/*
119139
* (non-Javadoc)
120140
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
2424
import org.springframework.data.mapping.PersistentPropertyPath;
2525
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
26+
import org.springframework.data.relational.core.sql.LockMode;
2627
import org.springframework.lang.Nullable;
2728

2829
/**
@@ -33,6 +34,7 @@
3334
* @author Jens Schauder
3435
* @author Tyler Van Gorder
3536
* @author Milan Milanov
37+
* @author Myeonghyeon Lee
3638
*/
3739
public interface DataAccessStrategy extends RelationResolver {
3840

@@ -129,6 +131,23 @@ public interface DataAccessStrategy extends RelationResolver {
129131
*/
130132
void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
131133

134+
/**
135+
* Acquire Lock
136+
*
137+
* @param id the id of the entity to load. Must not be {@code null}.
138+
* @param lockMode the lock mode for select. Must not be {@code null}.
139+
* @param domainType the domain type of the entity. Must not be {@code null}.
140+
*/
141+
<T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType);
142+
143+
/**
144+
* Acquire Lock entities of the given domain type.
145+
*
146+
* @param lockMode the lock mode for select. Must not be {@code null}.
147+
* @param domainType the domain type of the entity. Must not be {@code null}.
148+
*/
149+
<T> void acquireLockAll(LockMode lockMode, Class<T> domainType);
150+
132151
/**
133152
* Counts the rows in the table representing the given domain type.
134153
*

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,16 @@
1818
import static org.springframework.data.jdbc.core.convert.SqlGenerator.*;
1919

2020
import java.sql.JDBCType;
21+
import java.sql.ResultSet;
22+
import java.sql.SQLException;
2123
import java.util.ArrayList;
2224
import java.util.Collections;
2325
import java.util.HashSet;
2426
import java.util.List;
2527
import java.util.Map;
2628
import java.util.function.Predicate;
2729

28-
import org.springframework.dao.DataRetrievalFailureException;
29-
import org.springframework.dao.EmptyResultDataAccessException;
30-
import org.springframework.dao.InvalidDataAccessApiUsageException;
31-
import org.springframework.dao.OptimisticLockingFailureException;
30+
import org.springframework.dao.*;
3231
import org.springframework.data.domain.Pageable;
3332
import org.springframework.data.domain.Sort;
3433
import org.springframework.data.jdbc.support.JdbcUtil;
@@ -41,7 +40,9 @@
4140
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
4241
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
4342
import org.springframework.data.relational.core.sql.IdentifierProcessing;
43+
import org.springframework.data.relational.core.sql.LockMode;
4444
import org.springframework.data.relational.core.sql.SqlIdentifier;
45+
import org.springframework.jdbc.core.ResultSetExtractor;
4546
import org.springframework.jdbc.core.RowMapper;
4647
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4748
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
@@ -62,6 +63,7 @@
6263
* @author Tom Hombergs
6364
* @author Tyler Van Gorder
6465
* @author Milan Milanov
66+
* @author Myeonghyeon Lee
6567
* @since 1.1
6668
*/
6769
public class DefaultDataAccessStrategy implements DataAccessStrategy {
@@ -237,6 +239,27 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
237239
.update(sql(propertyPath.getBaseProperty().getOwner().getType()).createDeleteAllSql(propertyPath));
238240
}
239241

242+
/*
243+
* (non-Javadoc)
244+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
245+
*/
246+
@Override
247+
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
248+
String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode);
249+
SqlIdentifierParameterSource parameter = createIdParameterSource(id, domainType);
250+
operations.queryForObject(acquireLockByIdSql, parameter, Object.class);
251+
}
252+
253+
/*
254+
* (non-Javadoc)
255+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
256+
*/
257+
@Override
258+
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
259+
String acquireLockAllSql = sql(domainType).getAcquireLockAll(lockMode);
260+
operations.query(acquireLockAllSql, Collections.emptyMap(), new NoMappingResultSetExtractor());
261+
}
262+
240263
/*
241264
* (non-Javadoc)
242265
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)
@@ -582,4 +605,14 @@ public T getBean() {
582605
return null;
583606
}
584607
}
608+
609+
/**
610+
* The type No mapping result set extractor.
611+
*/
612+
static class NoMappingResultSetExtractor implements ResultSetExtractor<Object> {
613+
@Override
614+
public Object extractData(ResultSet resultSet) throws SQLException, DataAccessException {
615+
return null;
616+
}
617+
}
585618
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.data.domain.Sort;
2020
import org.springframework.data.mapping.PersistentPropertyPath;
2121
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
22+
import org.springframework.data.relational.core.sql.LockMode;
2223
import org.springframework.util.Assert;
2324

2425
/**
@@ -28,6 +29,7 @@
2829
* @author Jens Schauder
2930
* @author Tyler Van Gorder
3031
* @author Milan Milanov
32+
* @author Myeonghyeon Lee
3133
* @since 1.1
3234
*/
3335
public class DelegatingDataAccessStrategy implements DataAccessStrategy {
@@ -107,6 +109,24 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
107109
delegate.deleteAll(propertyPath);
108110
}
109111

112+
/*
113+
* (non-Javadoc)
114+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
115+
*/
116+
@Override
117+
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
118+
delegate.acquireLockById(id, lockMode, domainType);
119+
}
120+
121+
/*
122+
* (non-Javadoc)
123+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
124+
*/
125+
@Override
126+
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
127+
delegate.acquireLockAll(lockMode, domainType);
128+
}
129+
110130
/*
111131
* (non-Javadoc)
112132
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,26 @@ String getFindOne() {
258258
return findOneSql.get();
259259
}
260260

261+
/**
262+
* Create a {@code SELECT count(id) FROM … WHERE :id = … (LOCK CLAUSE)} statement.
263+
*
264+
* @param lockMode Lock clause mode.
265+
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
266+
*/
267+
String getAcquireLockById(LockMode lockMode) {
268+
return this.createAcquireLockById(lockMode);
269+
}
270+
271+
/**
272+
* Create a {@code SELECT count(id) FROM … (LOCK CLAUSE)} statement.
273+
*
274+
* @param lockMode Lock clause mode.
275+
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
276+
*/
277+
String getAcquireLockAll(LockMode lockMode) {
278+
return this.createAcquireLockAll(lockMode);
279+
}
280+
261281
/**
262282
* Create a {@code INSERT INTO … (…) VALUES(…)} statement.
263283
*
@@ -359,6 +379,33 @@ private String createFindOneSql() {
359379
return render(select);
360380
}
361381

382+
private String createAcquireLockById(LockMode lockMode) {
383+
384+
Table table = this.getTable();
385+
386+
Select select = StatementBuilder //
387+
.select(getIdColumn()) //
388+
.from(table) //
389+
.where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) //
390+
.lock(lockMode) //
391+
.build();
392+
393+
return render(select);
394+
}
395+
396+
private String createAcquireLockAll(LockMode lockMode) {
397+
398+
Table table = this.getTable();
399+
400+
Select select = StatementBuilder //
401+
.select(getIdColumn()) //
402+
.from(table) //
403+
.lock(lockMode) //
404+
.build();
405+
406+
return render(select);
407+
}
408+
362409
private String createFindAllSql() {
363410
return render(selectBuilder().build());
364411
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.mybatis.spring.SqlSessionTemplate;
2727
import org.slf4j.Logger;
2828
import org.slf4j.LoggerFactory;
29+
import org.springframework.dao.EmptyResultDataAccessException;
2930
import org.springframework.data.domain.Pageable;
3031
import org.springframework.data.domain.Sort;
3132
import org.springframework.data.jdbc.core.convert.CascadingDataAccessStrategy;
@@ -41,6 +42,7 @@
4142
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
4243
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
4344
import org.springframework.data.relational.core.sql.IdentifierProcessing;
45+
import org.springframework.data.relational.core.sql.LockMode;
4446
import org.springframework.data.relational.core.sql.SqlIdentifier;
4547
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4648
import org.springframework.util.Assert;
@@ -60,6 +62,7 @@
6062
* @author Mark Paluch
6163
* @author Tyler Van Gorder
6264
* @author Milan Milanov
65+
* @author Myeonghyeon Lee
6366
*/
6467
public class MyBatisDataAccessStrategy implements DataAccessStrategy {
6568

@@ -248,6 +251,34 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
248251
sqlSession().delete(statement, parameter);
249252
}
250253

254+
/*
255+
* (non-Javadoc)
256+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
257+
*/
258+
@Override
259+
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
260+
String statement = namespace(domainType) + ".acquireLockById";
261+
MyBatisContext parameter = new MyBatisContext(id, null, domainType, Collections.emptyMap());
262+
263+
long result = sqlSession().selectOne(statement, parameter);
264+
if (result < 1) {
265+
throw new EmptyResultDataAccessException(
266+
String.format("The lock target does not exist. id: %s, statement: %s", id, statement), 1);
267+
}
268+
}
269+
270+
/*
271+
* (non-Javadoc)
272+
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
273+
*/
274+
@Override
275+
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
276+
String statement = namespace(domainType) + ".acquireLockAll";
277+
MyBatisContext parameter = new MyBatisContext(null, null, domainType, Collections.emptyMap());
278+
279+
sqlSession().selectOne(statement, parameter);
280+
}
281+
251282
/*
252283
* (non-Javadoc)
253284
* @see org.springframework.data.jdbc.core.DataAccessStrategy#findById(java.lang.Object, java.lang.Class)

0 commit comments

Comments
 (0)