【问题标题】:Spring boot transaction rollback doesn't fire on PostgreSQL database because of idle transaction由于空闲事务,Spring Boot 事务回滚不会在 PostgreSQL 数据库上触发
【发布时间】:2021-04-14 02:24:56
【问题描述】:

我尝试执行事务操作并故意抛出异常,以验证是否已完成回滚但未执行回滚。

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

这是来自PgConnectionexecuteTransactionCommandrollback 函数没有被执行:

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

【问题讨论】:

  • 你试过这个 rollbackFor => @Transactional(rollbackFor = Exception.class)
  • 是的,我得到相同的结果 -> 没有回滚。
  • 在使用@PostConstruct 时不能保证已经创建了事务代理。您应该使用适当的侦听器或使用ApplicationRunner,它会在一切开始并完全初始化后执行。
  • 感谢您的见解。不幸的是,这不是问题 - 我用详细信息编辑了帖子。
  • 并且idle事务没有问题,并不表示有n个未处理的事务。 idle in transaction 表示一个打开的事务。

标签: java postgresql spring-boot transactions


【解决方案1】:

我检查了您希望在单个应用程序中使用多个模式(数据源、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.versionspring-jdbc.versionHikariCP.version。问题与版本无关。为切面操作添加spring-boot-starter-aop 依赖。

<dependencies>
...
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
</dependencies>

【讨论】:

  • @MHogge 我开发了像你一样的自定义 JPA 实现(而不是休眠)。我希望,你会成功。
猜你喜欢
  • 2020-03-11
  • 2018-11-05
  • 2019-06-07
  • 2019-03-30
  • 2020-02-06
  • 2018-09-03
  • 2016-10-07
  • 2017-11-07
  • 2018-01-01
相关资源
最近更新 更多