Replace transaction isolation synchronization with ReentrantLock

Closes gh-33546
This commit is contained in:
Juergen Hoeller 2024-09-27 11:36:21 +02:00
parent 8680c43368
commit 594ed95f3c
1 changed files with 28 additions and 17 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,6 +18,8 @@ package org.springframework.orm.jpa.vendor;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceException; import jakarta.persistence.PersistenceException;
@ -39,14 +41,18 @@ import org.springframework.transaction.TransactionException;
* JDBC and JPA operations in the same transaction, with cross visibility of * JDBC and JPA operations in the same transaction, with cross visibility of
* their impact. If this is not needed, set the "lazyDatabaseTransaction" flag to * their impact. If this is not needed, set the "lazyDatabaseTransaction" flag to
* {@code true} or consistently declare all affected transactions as read-only. * {@code true} or consistently declare all affected transactions as read-only.
* As of Spring 4.1.2, this will reliably avoid early JDBC Connection retrieval * This will reliably avoid early JDBC Connection retrieval and therefore keep
* and therefore keep EclipseLink in shared cache mode. * EclipseLink in shared cache mode.
* *
* <p><b>NOTE: This dialect supports custom isolation levels with limitations.</b> * <p><b>NOTE: This dialect supports custom isolation levels with limitations.</b>
* Consistent isolation level handling is only guaranteed when all Spring transaction * Consistent isolation level handling is only guaranteed when all Spring
* definitions specify a concrete isolation level, and as of 6.0.10 also when using * transaction definitions specify a concrete isolation level and when using the
* the default isolation level with non-readOnly and non-lazy transactions. See the * default isolation level with non-readOnly and non-lazy transactions; see the
* {@link #setLazyDatabaseTransaction "lazyDatabaseTransaction" javadoc} for details. * {@link #setLazyDatabaseTransaction "lazyDatabaseTransaction" javadoc} for details.
* Internal locking happens for transaction isolation management in EclipseLink's
* DatabaseLogin, at the granularity of the {@code EclipseLinkJpaDialect} instance;
* for independent persistence units with different target databases, use distinct
* {@code EclipseLinkJpaDialect} instances in order to minimize the locking impact.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 2.5.2 * @since 2.5.2
@ -58,6 +64,8 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
private boolean lazyDatabaseTransaction = false; private boolean lazyDatabaseTransaction = false;
private final Lock transactionIsolationLock = new ReentrantLock();
/** /**
* Set whether to lazily start a database resource transaction within a * Set whether to lazily start a database resource transaction within a
@ -94,13 +102,13 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
int currentIsolationLevel = definition.getIsolationLevel(); int currentIsolationLevel = definition.getIsolationLevel();
if (currentIsolationLevel != TransactionDefinition.ISOLATION_DEFAULT) { if (currentIsolationLevel != TransactionDefinition.ISOLATION_DEFAULT) {
// Pass custom isolation level on to EclipseLink's DatabaseLogin configuration // Pass custom isolation level on to EclipseLink's DatabaseLogin configuration.
// (since Spring 4.1.2 / revised in 5.3.28)
UnitOfWork uow = entityManager.unwrap(UnitOfWork.class); UnitOfWork uow = entityManager.unwrap(UnitOfWork.class);
DatabaseLogin databaseLogin = uow.getLogin(); DatabaseLogin databaseLogin = uow.getLogin();
// Synchronize on shared DatabaseLogin instance for consistent isolation level // Lock around shared DatabaseLogin instance for consistent isolation level
// set and reset in case of concurrent transactions with different isolation. // set and reset in case of concurrent transactions with different isolation.
synchronized (databaseLogin) { this.transactionIsolationLock.lock();
try {
int originalIsolationLevel = databaseLogin.getTransactionIsolation(); int originalIsolationLevel = databaseLogin.getTransactionIsolation();
// Apply current isolation level value, if necessary. // Apply current isolation level value, if necessary.
if (currentIsolationLevel != originalIsolationLevel) { if (currentIsolationLevel != originalIsolationLevel) {
@ -116,20 +124,26 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
databaseLogin.setTransactionIsolation(originalIsolationLevel); databaseLogin.setTransactionIsolation(originalIsolationLevel);
} }
} }
finally {
this.transactionIsolationLock.unlock();
}
} }
else if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) { else if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) {
// Begin an early transaction to force EclipseLink to get a JDBC Connection // Begin an early transaction to force EclipseLink to get a JDBC Connection
// so that Spring can manage transactions with JDBC as well as EclipseLink. // so that Spring can manage transactions with JDBC as well as EclipseLink.
UnitOfWork uow = entityManager.unwrap(UnitOfWork.class); UnitOfWork uow = entityManager.unwrap(UnitOfWork.class);
DatabaseLogin databaseLogin = uow.getLogin(); // Lock around shared DatabaseLogin instance for consistently picking up
// Synchronize on shared DatabaseLogin instance for consistently picking up
// the default isolation level even in case of concurrent transactions with // the default isolation level even in case of concurrent transactions with
// a custom isolation level (see above), as of 6.0.10 // a custom isolation level (see above).
synchronized (databaseLogin) { this.transactionIsolationLock.lock();
try {
entityManager.getTransaction().begin(); entityManager.getTransaction().begin();
uow.beginEarlyTransaction(); uow.beginEarlyTransaction();
entityManager.unwrap(Connection.class); entityManager.unwrap(Connection.class);
} }
finally {
this.transactionIsolationLock.unlock();
}
} }
else { else {
// Regular transaction begin with lazy database transaction. // Regular transaction begin with lazy database transaction.
@ -143,9 +157,6 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly)
throws PersistenceException, SQLException { throws PersistenceException, SQLException {
// As of Spring 4.1.2, we're using a custom ConnectionHandle for lazy retrieval
// of the underlying Connection (allowing for deferred internal transaction begin
// within the EclipseLink EntityManager)
return new EclipseLinkConnectionHandle(entityManager); return new EclipseLinkConnectionHandle(entityManager);
} }