UnexpectedRollbackException简单思考

2019-12-25  本文已影响0人  追梦小蜗牛
family-decorating-their-christmas-tree-3303614.jpg

简介:

事务一直以来都是一个老生常谈的话题,要想真正的把它搞懂,还是要看源码,然后再实践。后续我会继续研究spring事务源码这块,有心得的话,会记录下来和大家一起分享、学习讨论。我只是大致跟踪了一下spring事务从头到尾执行的流程,涉及到的代码具体细节,还没有细细研究,这篇文章仅仅以demo的形式验证了一些事务的常见疑问,和UnexpectedRollbackException这个异常抛出来的场景。

类图:

事务.jpg

场景1:

代码:
@Transactional
public void test01() {
    jdbcTemplate.execute("insert into apple (name) values ('apple001')");
    test03();
    System.out.println("能走到这里么?");
}

public void test03() {
    jdbcTemplate.execute("insert into apple (name) values (null)");
}
日志:
--- [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Detected StandardServletMultipartResolver
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/apple", parameters={}
--- [nio-8082-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hao.datasourcelearn.controller.DatasourceControllerTest#test01()
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.hao.datasourcelearn.service.DatasourceJdbcTest.test01]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
--- [nio-8082-exec-1] com.zaxxer.hikari.util.DriverDataSource  : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@963096416 wrapping com.mysql.cj.jdbc.ConnectionImpl@6c5c1e05] for JDBC transaction
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@963096416 wrapping com.mysql.cj.jdbc.ConnectionImpl@6c5c1e05] to manual commit
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values ('apple001')]
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values (null)]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@963096416 wrapping com.mysql.cj.jdbc.ConnectionImpl@6c5c1e05]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@963096416 wrapping com.mysql.cj.jdbc.ConnectionImpl@6c5c1e05] after transaction
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
--- [nio-8082-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null] with root cause
结果:
回滚
java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.18.jar:8.0.18]
说明:

在同一个类里面的方法调用,test03方法的代码直接就运行在test01的事务里面,等价于把test03的代码直接放到test01里面。

场景2:

代码:
@Transactional
public void test01() {
    jdbcTemplate.execute("insert into apple (name) values ('apple001')");
    test03();
    System.out.println("能走到这里么?");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test03() {
    jdbcTemplate.execute("insert into apple (name) values (null)");
}
日志:
--- [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Detected StandardServletMultipartResolver
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/apple", parameters={}
--- [nio-8082-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hao.datasourcelearn.controller.DatasourceControllerTest#test01()
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.hao.datasourcelearn.service.DatasourceJdbcTest.test01]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
--- [nio-8082-exec-1] com.zaxxer.hikari.util.DriverDataSource  : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@491071113 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cdc2a4] for JDBC transaction
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@491071113 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cdc2a4] to manual commit
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values ('apple001')]
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values (null)]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@491071113 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cdc2a4]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@491071113 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cdc2a4] after transaction
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
--- [nio-8082-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null] with root cause
结果:
回滚
java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.18.jar:8.0.18]
说明:

在同一个service里面的方法之间的调用,是不牵涉到什么嵌套事务的,被调用方法上面的事务注解也不会生效的,因为spring的事务的实现机制是动态代理(CGlib),因为动态代理一般都是代理的类,只有通过service.test的形式调用,才有机会用到代理。

场景3:

代码:
@Service
public class DatasourceJdbcTest {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private Test2 test2;

    @Transactional
    public void test01() {
        jdbcTemplate.execute("insert into apple (name) values ('apple001')");
        test2.test02();
        System.out.println("能走到这里么?");
    }

}

@Service
public class Test2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test02() {
        jdbcTemplate.execute("insert into apple (name) values (null)");
    }
}
日志:
--- [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Detected StandardServletMultipartResolver
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/apple", parameters={}
--- [nio-8082-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hao.datasourcelearn.controller.DatasourceControllerTest#test01()
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.hao.datasourcelearn.service.DatasourceJdbcTest.test01]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
--- [nio-8082-exec-1] com.zaxxer.hikari.util.DriverDataSource  : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1973985077 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ce42990] for JDBC transaction
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1973985077 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ce42990] to manual commit
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values ('apple001')]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values (null)]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Participating transaction failed - marking existing transaction as rollback-only
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Setting JDBC transaction [HikariProxyConnection@1973985077 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ce42990] rollback-only
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@1973985077 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ce42990]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1973985077 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ce42990] after transaction
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
--- [nio-8082-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null] with root cause
结果:
回滚
java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.18.jar:8.0.18]
说明:

这个例子涉及到不同service的方法调用,就会涉及到嵌套事务了,由于两个方法上面的事务注解都是@Transactional,所以test02方法会运行在test01的事务里面,从日志里面也可以看的出来,Participating in existing transaction这句话就印证了。这里抛出的异常依然是SQLIntegrityConstraintViolationException,还没有抛出UnexpectedRollbackException这个异常,System.out.println("能走到这里么?"),这句话也木有打印证明test01没有执行到这里,就被test02抛出的异常打断了。

场景4:

代码:
@Service
public class DatasourceJdbcTest {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private Test2 test2;

    @Transactional
    public void test01() {
        jdbcTemplate.execute("insert into apple (name) values ('apple001')");
        test2.test02();
        System.out.println("能走到这里么?");
    }

}

@Service
public class Test2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test02() {
        try {
            jdbcTemplate.execute("insert into apple (name) values (null)");
        } catch (DataAccessException e) {
            System.out.println("插入数据库出错!");;
        }
    }
}
日志:
2019-12-25 19:45:22.653  INFO 32704 --- [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-12-25 19:45:22.653  INFO 32704 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2019-12-25 19:45:22.653 DEBUG 32704 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Detected StandardServletMultipartResolver
2019-12-25 19:45:22.657 DEBUG 32704 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
2019-12-25 19:45:22.657  INFO 32704 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
2019-12-25 19:45:22.661 DEBUG 32704 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/apple", parameters={}
2019-12-25 19:45:22.663 DEBUG 32704 --- [nio-8082-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hao.datasourcelearn.controller.DatasourceControllerTest#test01()
2019-12-25 19:45:22.671 DEBUG 32704 --- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.hao.datasourcelearn.service.DatasourceJdbcTest.test01]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2019-12-25 19:45:22.671  INFO 32704 --- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-12-25 19:45:22.672  WARN 32704 --- [nio-8082-exec-1] com.zaxxer.hikari.util.DriverDataSource  : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
2019-12-25 19:45:22.746  INFO 32704 --- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-12-25 19:45:22.747 DEBUG 32704 --- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@207098194 wrapping com.mysql.cj.jdbc.ConnectionImpl@7350e8d1] for JDBC transaction
2019-12-25 19:45:22.748 DEBUG 32704 --- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@207098194 wrapping com.mysql.cj.jdbc.ConnectionImpl@7350e8d1] to manual commit
2019-12-25 19:45:22.752 DEBUG 32704 --- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values ('apple001')]
2019-12-25 19:45:22.758 DEBUG 32704 --- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction
2019-12-25 19:45:22.760 DEBUG 32704 --- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values (null)]
插入数据库出错!
能走到这里么?
2019-12-25 19:45:22.825 DEBUG 32704 --- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit
2019-12-25 19:45:22.825 DEBUG 32704 --- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@207098194 wrapping com.mysql.cj.jdbc.ConnectionImpl@7350e8d1]
2019-12-25 19:45:22.830 DEBUG 32704 --- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@207098194 wrapping com.mysql.cj.jdbc.ConnectionImpl@7350e8d1] after transaction
结果:
commit
说明:

test01和test02的注解都是@Transactional,从日志上看,依然是同一个事务,如果test02捕获自己的异常,自己消化掉,那么整个事务就不会受到异常的影响,会commit。

场景5:

代码:
@Service
public class DatasourceJdbcTest {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private Test2 test2;

    @Transactional
    public void test01() {
        jdbcTemplate.execute("insert into apple (name) values ('apple001')");
        try {
            test2.test02();
        } catch (Exception e) {
            System.out.println("捕获到test02的异常");;
        }
        System.out.println("能走到这里么?");
    }

}

@Service
public class Test2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test02() {
        jdbcTemplate.execute("insert into apple (name) values (null)");
    }
}
日志:
--- [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Detected StandardServletMultipartResolver
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/apple", parameters={}
--- [nio-8082-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hao.datasourcelearn.controller.DatasourceControllerTest#test01()
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.hao.datasourcelearn.service.DatasourceJdbcTest.test01]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
--- [nio-8082-exec-1] com.zaxxer.hikari.util.DriverDataSource  : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@792507397 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] for JDBC transaction
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@792507397 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] to manual commit
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values ('apple001')]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values (null)]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Participating transaction failed - marking existing transaction as rollback-only
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Setting JDBC transaction [HikariProxyConnection@792507397 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] rollback-only
捕获到test02的异常
能走到这里么?
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Global transaction is marked as rollback-only but transactional code requested commit
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@792507397 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@792507397 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] after transaction
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
--- [nio-8082-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only] with root cause
结果:
回滚
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:871) ~[spring-tx-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:708) ~[spring-tx-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:631) ~[spring-tx-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385) ~[spring-tx-5.2.2.RELEASE.jar:5.2.2.RELEASE]
说明:

UnexpectedRollbackException这个异常出现了,当test02不捕获test01异常的时候,不会抛出这个异常,是因为程序直接就被异常打断了,不会执行commit方法,而这个异常的抛出就是在commit方法里面,但是当test02捕获test01异常的时候,神奇的事情出现了,这个异常出现了,为啥呢?下面是个人看代码的理解:从日志来看Participating in existing transaction依然是只有一个事务,但是可以逻辑上分为外层和内层事务。
从代码上看,其实这个异常是由unexpectedRollback这个属性值来控制的,为true就会抛出目标异常,而什么情况下为true呢,当test02捕获test01异常的时候,当内外层事务都执行完的时候,程序会执行commit操作,这commit操作里面会以参数的形式直接传个true:processRollback(defStatus, true);:关键就是这个true参数起到了关键作用,这篇文章不分析具体的细节,只是说明一下大概,具体的事务代码详细分析,后续会陆续写一下的,感觉挺多的,还是一点一点的拆分着写比较好,一次写一个主题或者一段代码。

@Override
    public final void commit(TransactionStatus status) throws TransactionException {
        if (status.isCompleted()) {
            throw new IllegalTransactionStateException(
                    "Transaction is already completed - do not call commit or rollback more than once per transaction");
        }

        DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
        if (defStatus.isLocalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Transactional code has requested rollback");
            }
            processRollback(defStatus, false);
            return;
        }

        if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
            }
            processRollback(defStatus, true);
            return;
        }

        processCommit(defStatus);
    }

其实从技术上来说如果想避免抛出这个异常UnexpectedRollbackException,有两种方法,首先这个场景一般都是在嵌套业务里面出现,那么我们可以在test02方法上面开启一个新事务,或者把test01的异常在test02方法上继续上抛,让框架处理。

场景6:

代码:
@Service
public class DatasourceJdbcTest {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private Test2 test2;

    @Transactional
    public void test01() {
        jdbcTemplate.execute("insert into apple (name) values ('apple001')");
        try {
            test2.test02();
        } catch (Exception e) {
            throw e;
        }
        System.out.println("能走到这里么?");
    }

}

@Service
public class Test2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test02() {
        jdbcTemplate.execute("insert into apple (name) values (null)");
    }
}
日志:
--- [nio-8082-exec-2] o.s.web.servlet.DispatcherServlet        : GET "/apple", parameters={}
--- [nio-8082-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hao.datasourcelearn.controller.DatasourceControllerTest#test01()
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.hao.datasourcelearn.service.DatasourceJdbcTest.test01]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1832385864 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] for JDBC transaction
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1832385864 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] to manual commit
--- [nio-8082-exec-2] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values ('apple001')]
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction
--- [nio-8082-exec-2] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values (null)]
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Participating transaction failed - marking existing transaction as rollback-only
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Setting JDBC transaction [HikariProxyConnection@1832385864 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] rollback-only
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@1832385864 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8]
--- [nio-8082-exec-2] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1832385864 wrapping com.mysql.cj.jdbc.ConnectionImpl@301132b8] after transaction
--- [nio-8082-exec-2] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
--- [nio-8082-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [insert into apple (name) values (null)]; Column 'name' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null] with root cause
结果:
回滚
java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.StatementImpl.executeInternal(StatementImpl.java:764) ~[mysql-connector-java-8.0.18.jar:8.0.18]
说明:

test02把这个异常抛出去,让框架捕获处理,test02后面的代码也不用执行了,也就不会执行后续的commit,不会抛出UnexpectedRollbackException。

场景7:

代码:
@Service
public class DatasourceJdbcTest {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private Test2 test2;

    @Transactional
    public void test01() {
        jdbcTemplate.execute("insert into apple (name) values ('apple001')");
        test2.test02();
        System.out.println("能走到这里么?");
    }

}

@Service
public class Test2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test02() {
        jdbcTemplate.execute("insert into apple (name) values (null)");
    }
}
日志:
--- [nio-8082-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Detected StandardServletMultipartResolver
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
--- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/apple", parameters={}
--- [nio-8082-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hao.datasourcelearn.controller.DatasourceControllerTest#test01()
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.hao.datasourcelearn.service.DatasourceJdbcTest.test01]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
--- [nio-8082-exec-1] com.zaxxer.hikari.util.DriverDataSource  : Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.
--- [nio-8082-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1410196551 wrapping com.mysql.cj.jdbc.ConnectionImpl@27bf96e7] for JDBC transaction
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1410196551 wrapping com.mysql.cj.jdbc.ConnectionImpl@27bf96e7] to manual commit
--- [nio-8082-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [insert into apple (name) values ('apple001')]
--- [nio-8082-exec-1] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [com.hao.datasourcelearn.service.Test2.test02]
结果:
回滚
java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.StatementImpl.executeInternal(StatementImpl.java:764) ~[mysql-connector-java-8.0.18.jar:8.0.18]
    at com.mysql.cj.jdbc.StatementImpl.execute(StatementImpl.java:648) ~[mysql-connector-java-8.0.18.jar:8.0.18]
说明:

从日志打印来看,Suspending current transaction, creating new transaction with name [com.hao.datasourcelearn.service.Test2.test02],是两个不同的事务,不是嵌套事务,虽然抛出异常,但是不会被marked as rollback-only,也就不会抛出UnexpectedRollbackException。但是这个异常会被抛出到父级test01里面,会影响test01的事务提交的。

总结:

这篇文章主要偏重于从例子上验证一些常见的疑问,没有对具体的源码进行分析,只是从代码的角度简单的解释了一下UnexpectedRollbackException产生的原因和场景。感觉读源码不容易呀,要想完全搞懂一个,确实需要很多精力和耐心,真的要死磕到底,才能有所收获,路漫漫其修远兮,吾将上下而求索,后面继续......欢迎大家指点错误,一起讨论,多多指教。觉察即自由。

上一篇 下一篇

猜你喜欢

热点阅读