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.
This commit is contained in:
mhyeon-lee
2020-02-22 14:54:20 +09:00
committed by Jens Schauder
parent fa8b95c5f3
commit 04c29f4004
42 changed files with 1364 additions and 57 deletions

View File

@@ -19,6 +19,7 @@ import org.springframework.data.relational.core.dialect.AbstractDialect;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
import org.springframework.data.relational.core.dialect.LimitClause;
import org.springframework.data.relational.core.dialect.LockClause;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
/**
@@ -27,6 +28,7 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing;
* @author Mark Paluch
* @author Milan Milanov
* @author Jens Schauder
* @author Myeonghyeon Lee
*/
public class NonQuotingDialect extends AbstractDialect implements Dialect {
@@ -39,6 +41,11 @@ public class NonQuotingDialect extends AbstractDialect implements Dialect {
return HsqlDbDialect.INSTANCE.limit();
}
@Override
public LockClause lock() {
return HsqlDbDialect.INSTANCE.lock();
}
@Override
public IdentifierProcessing getIdentifierProcessing() {
return IdentifierProcessing.create(new IdentifierProcessing.Quoting(""), IdentifierProcessing.LetterCasing.AS_IS);

View File

@@ -46,6 +46,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.Aliased;
import org.springframework.data.relational.core.sql.LockMode;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
@@ -96,16 +97,45 @@ public class SqlGeneratorUnitTests {
SoftAssertions softAssertions = new SoftAssertions();
softAssertions.assertThat(sql) //
.startsWith("SELECT") //
.contains("dummy_entity.id1 AS id1,") //
.contains("dummy_entity.x_name AS x_name,") //
.contains("dummy_entity.x_other AS x_other,") //
.contains("ref.x_l1id AS ref_x_l1id") //
.contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") //
.contains("ON ref.dummy_entity = dummy_entity.id1") //
.contains("WHERE dummy_entity.id1 = :id") //
// 1-N relationships do not get loaded via join
.doesNotContain("Element AS elements");
.startsWith("SELECT") //
.contains("dummy_entity.id1 AS id1,") //
.contains("dummy_entity.x_name AS x_name,") //
.contains("dummy_entity.x_other AS x_other,") //
.contains("ref.x_l1id AS ref_x_l1id") //
.contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") //
.contains("ON ref.dummy_entity = dummy_entity.id1") //
.contains("WHERE dummy_entity.id1 = :id") //
// 1-N relationships do not get loaded via join
.doesNotContain("Element AS elements");
softAssertions.assertAll();
}
@Test // DATAJDBC-493
public void getAcquireLockById() {
String sql = sqlGenerator.getAcquireLockById(LockMode.PESSIMISTIC_WRITE);
SoftAssertions softAssertions = new SoftAssertions();
softAssertions.assertThat(sql) //
.startsWith("SELECT") //
.contains("dummy_entity.id1") //
.contains("WHERE dummy_entity.id1 = :id") //
.contains("FOR UPDATE") //
.doesNotContain("Element AS elements");
softAssertions.assertAll();
}
@Test // DATAJDBC-493
public void getAcquireLockAll() {
String sql = sqlGenerator.getAcquireLockAll(LockMode.PESSIMISTIC_WRITE);
SoftAssertions softAssertions = new SoftAssertions();
softAssertions.assertThat(sql) //
.startsWith("SELECT") //
.contains("dummy_entity.id1") //
.contains("FOR UPDATE") //
.doesNotContain("Element AS elements");
softAssertions.assertAll();
}

View File

@@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.DatabaseProfileValueSource;
@@ -34,7 +35,6 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.annotation.IfProfileValue;
import org.springframework.test.annotation.ProfileValueSourceConfiguration;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
@@ -123,6 +123,131 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
assertThat(exceptions).isEmpty();
}
@Test // DATAJDBC-493
public void updateConcurrencyWithDelete() throws Exception {
DummyEntity entity = createDummyEntity();
entity = repository.save(entity);
Long targetId = entity.getId();
assertThat(targetId).isNotNull();
List<DummyEntity> concurrencyEntities = createEntityStates(entity);
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager);
List<Exception> exceptions = new CopyOnWriteArrayList<>();
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for all threads to wait on.
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on until all threads are done.
// update
concurrencyEntities.stream() //
.map(e -> new Thread(() -> {
try {
startLatch.countDown();
startLatch.await();
transactionTemplate.execute(status -> repository.save(e));
} catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an IncorrectUpdateSemanticsDataAccessException.
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
return;
}
exceptions.add(ex);
} finally {
doneLatch.countDown();
}
})) //
.forEach(Thread::start);
// delete
new Thread(() -> {
try {
startLatch.countDown();
startLatch.await();
transactionTemplate.execute(status -> {
repository.deleteById(targetId);
return null;
});
} catch (Exception ex) {
exceptions.add(ex);
} finally {
doneLatch.countDown();
}
}).start();
doneLatch.await();
assertThat(exceptions).isEmpty();
assertThat(repository.findById(entity.id)).isEmpty();
}
@Test // DATAJDBC-493
public void updateConcurrencyWithDeleteAll() throws Exception {
DummyEntity entity = createDummyEntity();
entity = repository.save(entity);
List<DummyEntity> concurrencyEntities = createEntityStates(entity);
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager);
List<Exception> exceptions = new CopyOnWriteArrayList<>();
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for all threads to wait on.
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on until all threads are done.
// update
concurrencyEntities.stream() //
.map(e -> new Thread(() -> {
try {
startLatch.countDown();
startLatch.await();
transactionTemplate.execute(status -> repository.save(e));
} catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an IncorrectUpdateSemanticsDataAccessException.
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
return;
}
exceptions.add(ex);
} finally {
doneLatch.countDown();
}
})) //
.forEach(Thread::start);
// delete
new Thread(() -> {
try {
startLatch.countDown();
startLatch.await();
transactionTemplate.execute(status -> {
repository.deleteAll();
return null;
});
} catch (Exception ex) {
exceptions.add(ex);
} finally {
doneLatch.countDown();
}
}).start();
doneLatch.await();
assertThat(exceptions).isEmpty();
assertThat(repository.count()).isEqualTo(0);
}
private List<DummyEntity> createEntityStates(DummyEntity entity) {
List<DummyEntity> concurrencyEntities = new ArrayList<>();

View File

@@ -20,7 +20,9 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.relational.core.dialect.AbstractDialect;
import org.springframework.data.relational.core.dialect.ArrayColumns;
import org.springframework.data.relational.core.dialect.LimitClause;
import org.springframework.data.relational.core.dialect.LockClause;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -28,6 +30,7 @@ import org.springframework.util.ClassUtils;
* An SQL dialect for the ANSI SQL standard.
*
* @author Milan Milanov
* @author Myeonghyeon Lee
* @since 2.0
*/
public class AnsiDialect extends AbstractDialect {
@@ -78,6 +81,27 @@ public class AnsiDialect extends AbstractDialect {
}
};
private static final LockClause LOCK_CLAUSE = new LockClause() {
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LockClause#getLock(LockOptions)
*/
@Override
public String getLock(LockOptions lockOptions) {
return "FOR UPDATE";
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.LimitClause#getClausePosition()
*/
@Override
public Position getClausePosition() {
return Position.AFTER_ORDER_BY;
}
};
private final AnsiArrayColumns ARRAY_COLUMNS = new AnsiArrayColumns();
/*
@@ -89,6 +113,15 @@ public class AnsiDialect extends AbstractDialect {
return LIMIT_CLAUSE;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.Dialect#lock()
*/
@Override
public LockClause lock() {
return LOCK_CLAUSE;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.Dialect#getArraySupport()