来道题
CREATE TABLE `goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`good_id` varchar(20) DEFAULT NULL,
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `goods_good_id_index` (`good_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","");
//part1
conn.setAutoCommit(false);
Statement statement = conn.createStatement();
statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");
conn.commit();
//part2
statement = conn.createStatement();
statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");
conn.setAutoCommit(true);
//part3
try {
statement = conn.createStatement();
statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");
int i = 1/0;
}catch (Exception ex){
System.out.println("there is an error");
}
conn.setAutoCommit(true);
//part4
conn.setAutoCommit(false);
try {
statement = conn.createStatement();
statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");
int i = 1/0;
}catch (Exception ex){
System.out.println("there is an error");
}
conn.setAutoCommit(true);
你举得这4段代码都提交了吗,为什么?
如果你知道这个知识点,那么本文对于你来说很容易理解。
一个知识点
首先,上面4段代码都会提交成功。
主要的知识点是, autocommit
的状态切换时,会对自动提交之前执行的内容。
看下这个方法的注释就知道了。
image.png
他这边说,如果事务执行过程中,如果 autocommit
状态改变了,会提交之前的事务。
额,这有个逻辑上的问题,如果autocommit
本身就是true,我们的语句不是直接就提交了么,那这个描述应该改成从false改成true的时候。
其实这段注释还有前半段。
image.png
针对DML和DDL语句,autocommit
=true的情况下,statement是立刻提交的。
而对于select语句,要等到关联的result set被关闭,对于存储过程....
而这个知识点太偏了,懂的朋友了解下,告诉我是啥..
所以我们这边的知识点严谨点来说就是: 对于DDL和DML语句,当autocommit从false切换为true时,事务会自动提交。
spring事务中的aftercommit
afterCommit是Spring事务机制中的一个回调钩子,用于在事务提交后做一些操作。
我们可以这么使用它
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization(){
public void afterCommit() {
//...执行一些操作
}
});
也可以通过@TransactionalEventListener间接使用它,它的底层原理就是上面这段代码
@TransactionalEventListener
public void handleEvent(Event event){
//...执行一些操作
}
重点是在事务提交后执行一些操作,也就是我题目中conn.commit()
之后再执行一些操作。
这个时候存在一个问题,如果这个操作是数据库相关的操作,会不会被提交。
根据我文章开篇的代码,你肯定就知道答案就是会提交,但是是autocommit的切换导致的提交。
额,其实并不是,对比2个常用db框架,Mybatis和JPA(Hibernate),Mybatis会提交,而Hibernate会丢失。
image.png
在afercomit的注释中,他也警告我们了,在aftercommit中做数据库操作可能不会被提交。如果你要做数据库操作,你需要在一个新的事务中,可以使用PROPAGATION_REQUIRES_NEW
隔离级别。
源码中的NOTE要仔细的看!!很重点
在不创建新事务的前提下,为什么对于Mybatis和JPA在aftercommit中执行操作,一个提交,一个不提交?开始我们的源码解析。
源码解析
Spring Transaction的核心逻辑封装在TransactionAspectSupport的invokeWithinTransaction方法中,而核心流程中重要的三个操作,获取/提交/回滚事务,由PlatformTransactionManager来实现。
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
PlatformTransactionManager使用了策略模式和模板方法模式,它的子类AbstractPlatformTransactionManager又对上面三个方法做了抽象,暴露了一一系列钩子方法让子类实现。
最常用的子类就是DataSourceTransactionManager和HibernateTransactionManager,分别对应Mybatis和JPA框架。
本文讲解的aftercommit同步钩子在AbstractPlatformTransactionManager的processCommit中被触发。
回顾我们上面展示的场景,我们在一个事务里,注册了一个aftercommit钩子,并且aftercommit里面,也会再次操作数据库,执行dml操作。
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
//...
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
//...假设事务提交成功
doCommit(status);
}
try {
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}
}
finally {
cleanupAfterCompletion(status);
}
在第一个事务doCommit成功,他会通过triggerAfterCommit触发它的aftercommit钩子逻辑,进行下一次事务操作,但是此时的Transaction还没有释放,并且它也不是newTransaction了。
为什么不是newTransaction,见以下代码
private TransactionStatus handleExistingTransaction(
TransactionDefinition definition, Object transaction, boolean debugEnabled)
throws TransactionException {
//...
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}
因为 status.isNewTransaction()
不成立,所以 doCommit(status);
不会执行。
doCommit
中会进行什么操作?
对于DataSourceTransactionManager,就是调用了Connection的commit方法,对事务进行提交。
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
虽然错失了doCommit这个机会,但是在cleanupAfterCompletion(status);
方法
private void cleanupAfterCompletion(DefaultTransactionStatus status) {
status.setCompleted();
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.clear();
}
if (status.isNewTransaction()) {
doCleanupAfterCompletion(status.getTransaction());
}
if (status.getSuspendedResources() != null) {
if (status.isDebug()) {
logger.debug("Resuming suspended transaction after completion of inner transaction");
}
Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
}
}
在doCleanupAfterCompletion的逻辑中,注意doCleanupAfterCompletion也是一个钩子,这个逻辑也由DataSourceTransactionManager实现
protected void doCleanupAfterCompletion(Object transaction) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
// Remove the connection holder from the thread, if exposed.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.unbindResource(obtainDataSource());
}
// Reset connection.
Connection con = txObject.getConnectionHolder().getConnection();
try {
if (txObject.isMustRestoreAutoCommit()) {
con.setAutoCommit(true);
}
DataSourceUtils.resetConnectionAfterTransaction(
con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
}
catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
}
if (txObject.isNewConnectionHolder()) {
if (logger.isDebugEnabled()) {
logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
}
DataSourceUtils.releaseConnection(con, this.dataSource);
}
txObject.getConnectionHolder().clear();
}
调用到了 con.setAutoCommit(true);
间接了提交了事务
然后我们再来看看HibernateTransactionManager对这个两个方法的实现
HibernateTransactionManager#doCommit
protected void doCommit(DefaultTransactionStatus status) {
HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
Transaction hibTx = txObject.getSessionHolder().getTransaction();
Assert.state(hibTx != null, "No Hibernate transaction");
if (status.isDebug()) {
logger.debug("Committing Hibernate transaction on Session [" +
txObject.getSessionHolder().getSession() + "]");
}
try {
//看这里
hibTx.commit();
}
catch (org.hibernate.TransactionException ex) {
// assumably from commit call to the underlying JDBC connection
throw new TransactionSystemException("Could not commit Hibernate transaction", ex);
}
catch (HibernateException ex) {
// assumably failed to flush changes to database
throw convertHibernateAccessException(ex);
}
catch (PersistenceException ex) {
if (ex.getCause() instanceof HibernateException) {
throw convertHibernateAccessException((HibernateException) ex.getCause());
}
throw ex;
}
}
HibernateTransactionManager#doCleanupAfterCompletion
protected void doCleanupAfterCompletion(Object transaction) {
HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
// Remove the session holder from the thread.
if (txObject.isNewSessionHolder()) {
TransactionSynchronizationManager.unbindResource(obtainSessionFactory());
}
// Remove the JDBC connection holder from the thread, if exposed.
if (getDataSource() != null) {
TransactionSynchronizationManager.unbindResource(getDataSource());
}
Session session = txObject.getSessionHolder().getSession();
if (this.prepareConnection && isPhysicallyConnected(session)) {
// We're running with connection release mode "on_close": We're able to reset
// the isolation level and/or read-only flag of the JDBC Connection here.
// Else, we need to rely on the connection pool to perform proper cleanup.
try {
Connection con = ((SessionImplementor) session).connection();
Integer previousHoldability = txObject.getPreviousHoldability();
if (previousHoldability != null) {
con.setHoldability(previousHoldability);
}
DataSourceUtils.resetConnectionAfterTransaction(
con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
}
catch (HibernateException ex) {
logger.debug("Could not access JDBC Connection of Hibernate Session", ex);
}
catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
}
}
if (txObject.isNewSession()) {
if (logger.isDebugEnabled()) {
logger.debug("Closing Hibernate Session [" + session + "] after transaction");
}
SessionFactoryUtils.closeSession(session);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Not closing pre-bound Hibernate Session [" + session + "] after transaction");
}
if (txObject.getSessionHolder().getPreviousFlushMode() != null) {
session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode());
}
if (!this.allowResultAccessAfterCompletion && !this.hibernateManagedSession) {
disconnectOnCompletion(session);
}
}
txObject.getSessionHolder().clear();
}
docommit里的逻辑还是用到了底层connection的commit,而在doCleanupAfterCompletion中,没有见到设置autocommit的身影。
所以在JPA中你在aftercommit中进行dml操作是会丢失的。
另外一个点是,如果你在aftercommit进行了事务操作,但是中间发生了异常,比如2条insert语句后,发生了异常,这两条insert会不会回滚?
答案是不会
回顾processCommit方法
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
//...
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
//...假设事务提交成功
doCommit(status);
}
try {
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}
}
finally {
cleanupAfterCompletion(status);
}
}
我们的aftercommit在triggerAfterCommit执行,这个方法里面抛出了异常,因为没有catch,异常会往上传递,在cleanupAfterCompletion里也没有处理异常,但是对于mybatis来讲,它改变了autocommit状态,所以更改被提交了。这是一个你想不到的坑。
最佳实践
- aftercommit或者说是transactionlistener,最好不要有dml操作
- 一但aftercommit中有事务操作,存在的风险是,一致性得不到保证,异常不会让这部分的事务回滚
demo
写了一个工程,用于测试mybatis和jpa中对于aftercommit中执行dml操作是否会提交
地址如下
https://github.com/shengchaojie/spring_tx_aftercommit_problem
参考资料
Spring 4.3 源码分析之 事务组件概述 - 简书