Spring 由于事务空闲,启动事务回滚不会在 PostgreSQL 数据库上触发
Spring boot transaction rollback doesn't fire on PostgreSQL database because of idle transaction
我尝试执行事务性操作并故意抛出异常以验证回滚是否已完成但回滚未被执行。
PostgreSQL 数据库版本为 12.1-1,基于 Docker。
这是包含 @Transactional
注释的服务:
@Service
public class MyTestService {
@Autowired
private DocumentDataDao documentDataDao;
@Transactional
public void test() {
DocumentData data = new DocumentData();
data.setData(UUID.randomUUID().toString());
documentDataDao.create(data);
throw new IllegalArgumentException("Test rollback");
}
}
create
函数正在使用 NamedParameterJdbcTemplate
插入数据:
String statement = String.format("INSERT INTO %s (%s) VALUES (%s) RETURNING %s", tableName,
String.join(",", insertingColumns), String.join(",", values),
String.join(",", returningColumns));
return getNamedJdbcTemplate().queryForObject(statement, parameters, getRowMapper());
并且 test
函数是从另一个服务调用的:
@Service
public class ApplicationStartupListener {
private Logger log = LoggerFactory.getLogger(ApplicationStartupListener.class);
@Autowired
private MyTestService testService;
@PostConstruct
public void init() {
try {
testService.test();
} catch (Exception e) {
log.error("fail to start", e);
}
}
}
在调试时我发现如果没有执行回滚,那是因为事务是 IDLE
.
这是 PgConnection
中的 rollback
函数,executeTransactionCommand
没有被执行:
public void rollback() throws SQLException {
checkClosed();
if (autoCommit) {
throw new PSQLException(GT.tr("Cannot rollback when autoCommit is enabled."),
PSQLState.NO_ACTIVE_SQL_TRANSACTION);
}
if (queryExecutor.getTransactionState() != TransactionState.IDLE) {
executeTransactionCommand(rollbackQuery);
}
}
任何关于为什么事务被标记为空闲并停止执行回滚方法的提示都将不胜感激。
编辑 (1)
作为@M。 Deinum 提到,不能保证在使用 @PostConstruct
时已经创建了事务代理。这就是为什么我用 ApplicationRunner
:
进行测试的原因
@Component
public class AppStartupRunner implements ApplicationRunner {
@Autowired
private MyTestService testService;
@Override
public void run(ApplicationArguments args) throws Exception {
testService.test();
}
}
但这也没有用。
我还尝试 运行 在应用程序启动后片刻 test
方法,方法是使用 RestController
并向其发送 HTTP 请求,但仍然相同问题。
@RestController
public class AppController {
@Autowired
private MyTestService testService;
@GetMapping("/test")
public ResponseEntity<Object> test() {
testService.test();
return ResponseEntity.ok().build();
}
}
编辑 (2)
我将 PostgreSQL JDBC 版本从 42.2.2 升级到 42.2.18(目前最新),但尝试回滚时连接仍然空闲。
编辑 (3)
我在 git 存储库中重现了这个问题:https://github.com/Martin-Hogge/spring-boot-postgresql-transactional-example/tree/master。
我检查了要在单个应用程序中使用多个模式(数据源、jdbc 模板)的体系结构。 @Transactional
仅管理名为 HikariPool-1
的应用程序默认数据源。当您调用 rest 方法时,将创建名为 HikariPool-2
的新 hikari 池。您的操作在 HikariPool-2
,但 @Transactional
仅管理 HikariPool-1
。
@Transactional
的事务管理器参数不能动态更改。因此,您可以定义一个新的自定义注释来管理您的事务。或者您可以使用 TransactionTemplate
而不是注释。
我创建了一个简单的自定义事务管理方面。它正在使用定义的 dao 数据源和事务生命周期。
测试服务
@Service
public class MyTestService {
@Autowired
private DocumentDataDao documentDataDao;
@CustomTransactional(DocumentDataDao.class)
public void test() {
DocumentData data = new DocumentData();
data.setData(UUID.randomUUID().toString());
documentDataDao.create(data);
throw new IllegalArgumentException("Test rollback");
}
}
自定义交易
package com.example.transactional;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomTransactional {
Class<? extends BaseDao<?>> value();
}
自定义事务方面
package com.example.transactional;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class CustomTransactionalAspect implements ApplicationContextAware {
private ApplicationContext applicationContext;
private Map<Class<? extends BaseDao<?>>, BaseDao<?>> classMap = new HashMap<>();
@Around("@annotation(customTransactional)")
public Object customTransaction(ProceedingJoinPoint joinPoint, CustomTransactional customTransactional) throws Throwable {
BaseDao<?> baseDao = getBaseDao(customTransactional.value());
// custom transaction management
return baseDao.getConnectionHandler().getTransactionTemplate().execute(status -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
/**
* Search {@link BaseDao} class on spring beans
*
* @param clazz Target dao class type
* @return Spring bean object
*/
private BaseDao<?> getBaseDao(Class<? extends BaseDao<?>> clazz) {
return classMap.computeIfAbsent(clazz, c -> applicationContext.getBean(c));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
连接处理程序
我为事务操作添加了transactionTemplate
public class ConnectionHandler {
private NamedParameterJdbcTemplate namedJdbcTemplate;
private JdbcTemplate jdbcTemplate;
private TransactionTemplate transactionTemplate;
private String schema;
public ConnectionHandler(DataSource dataSource, String schema) {
this.namedJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.schema = schema;
this.transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
}
public NamedParameterJdbcTemplate getNamedJdbcTemplate() {
return namedJdbcTemplate;
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public String getSchema() {
return schema;
}
public TransactionTemplate getTransactionTemplate() {
return transactionTemplate;
}
}
BaseDao
将 getConnectionHandler
的修饰符更改为 public
。
public ConnectionHandler getConnectionHandler() {
return getDataSource().getConnection(getSchemaName());
}
pom.xml
您可以删除 postgresql.version
、spring-jdbc.version
和 HikariCP.version
。问题与版本无关。为方面操作添加 spring-boot-starter-aop
依赖项。
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
我尝试执行事务性操作并故意抛出异常以验证回滚是否已完成但回滚未被执行。
PostgreSQL 数据库版本为 12.1-1,基于 Docker。
这是包含 @Transactional
注释的服务:
@Service
public class MyTestService {
@Autowired
private DocumentDataDao documentDataDao;
@Transactional
public void test() {
DocumentData data = new DocumentData();
data.setData(UUID.randomUUID().toString());
documentDataDao.create(data);
throw new IllegalArgumentException("Test rollback");
}
}
create
函数正在使用 NamedParameterJdbcTemplate
插入数据:
String statement = String.format("INSERT INTO %s (%s) VALUES (%s) RETURNING %s", tableName,
String.join(",", insertingColumns), String.join(",", values),
String.join(",", returningColumns));
return getNamedJdbcTemplate().queryForObject(statement, parameters, getRowMapper());
并且 test
函数是从另一个服务调用的:
@Service
public class ApplicationStartupListener {
private Logger log = LoggerFactory.getLogger(ApplicationStartupListener.class);
@Autowired
private MyTestService testService;
@PostConstruct
public void init() {
try {
testService.test();
} catch (Exception e) {
log.error("fail to start", e);
}
}
}
在调试时我发现如果没有执行回滚,那是因为事务是 IDLE
.
这是 PgConnection
中的 rollback
函数,executeTransactionCommand
没有被执行:
public void rollback() throws SQLException {
checkClosed();
if (autoCommit) {
throw new PSQLException(GT.tr("Cannot rollback when autoCommit is enabled."),
PSQLState.NO_ACTIVE_SQL_TRANSACTION);
}
if (queryExecutor.getTransactionState() != TransactionState.IDLE) {
executeTransactionCommand(rollbackQuery);
}
}
任何关于为什么事务被标记为空闲并停止执行回滚方法的提示都将不胜感激。
编辑 (1)
作为@M。 Deinum 提到,不能保证在使用 @PostConstruct
时已经创建了事务代理。这就是为什么我用 ApplicationRunner
:
@Component
public class AppStartupRunner implements ApplicationRunner {
@Autowired
private MyTestService testService;
@Override
public void run(ApplicationArguments args) throws Exception {
testService.test();
}
}
但这也没有用。
我还尝试 运行 在应用程序启动后片刻 test
方法,方法是使用 RestController
并向其发送 HTTP 请求,但仍然相同问题。
@RestController
public class AppController {
@Autowired
private MyTestService testService;
@GetMapping("/test")
public ResponseEntity<Object> test() {
testService.test();
return ResponseEntity.ok().build();
}
}
编辑 (2)
我将 PostgreSQL JDBC 版本从 42.2.2 升级到 42.2.18(目前最新),但尝试回滚时连接仍然空闲。
编辑 (3)
我在 git 存储库中重现了这个问题:https://github.com/Martin-Hogge/spring-boot-postgresql-transactional-example/tree/master。
我检查了要在单个应用程序中使用多个模式(数据源、jdbc 模板)的体系结构。 @Transactional
仅管理名为 HikariPool-1
的应用程序默认数据源。当您调用 rest 方法时,将创建名为 HikariPool-2
的新 hikari 池。您的操作在 HikariPool-2
,但 @Transactional
仅管理 HikariPool-1
。
@Transactional
的事务管理器参数不能动态更改。因此,您可以定义一个新的自定义注释来管理您的事务。或者您可以使用 TransactionTemplate
而不是注释。
我创建了一个简单的自定义事务管理方面。它正在使用定义的 dao 数据源和事务生命周期。
测试服务
@Service
public class MyTestService {
@Autowired
private DocumentDataDao documentDataDao;
@CustomTransactional(DocumentDataDao.class)
public void test() {
DocumentData data = new DocumentData();
data.setData(UUID.randomUUID().toString());
documentDataDao.create(data);
throw new IllegalArgumentException("Test rollback");
}
}
自定义交易
package com.example.transactional;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomTransactional {
Class<? extends BaseDao<?>> value();
}
自定义事务方面
package com.example.transactional;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class CustomTransactionalAspect implements ApplicationContextAware {
private ApplicationContext applicationContext;
private Map<Class<? extends BaseDao<?>>, BaseDao<?>> classMap = new HashMap<>();
@Around("@annotation(customTransactional)")
public Object customTransaction(ProceedingJoinPoint joinPoint, CustomTransactional customTransactional) throws Throwable {
BaseDao<?> baseDao = getBaseDao(customTransactional.value());
// custom transaction management
return baseDao.getConnectionHandler().getTransactionTemplate().execute(status -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
/**
* Search {@link BaseDao} class on spring beans
*
* @param clazz Target dao class type
* @return Spring bean object
*/
private BaseDao<?> getBaseDao(Class<? extends BaseDao<?>> clazz) {
return classMap.computeIfAbsent(clazz, c -> applicationContext.getBean(c));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
连接处理程序
我为事务操作添加了transactionTemplate
public class ConnectionHandler {
private NamedParameterJdbcTemplate namedJdbcTemplate;
private JdbcTemplate jdbcTemplate;
private TransactionTemplate transactionTemplate;
private String schema;
public ConnectionHandler(DataSource dataSource, String schema) {
this.namedJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.schema = schema;
this.transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
}
public NamedParameterJdbcTemplate getNamedJdbcTemplate() {
return namedJdbcTemplate;
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public String getSchema() {
return schema;
}
public TransactionTemplate getTransactionTemplate() {
return transactionTemplate;
}
}
BaseDao
将 getConnectionHandler
的修饰符更改为 public
。
public ConnectionHandler getConnectionHandler() {
return getDataSource().getConnection(getSchemaName());
}
pom.xml
您可以删除 postgresql.version
、spring-jdbc.version
和 HikariCP.version
。问题与版本无关。为方面操作添加 spring-boot-starter-aop
依赖项。
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>