前提
XXX平台导出的mysql建表语句没有指定存储引擎,而mysql默认使用MyISAM。
但是MyISAM是不支持事务的。在mysql中,唯有InnoDb是支持事务的。
目的
验证Spring提供的标签@Transactional,以及XXX提供的TransactionComponent组件在不同的存储引擎下的工作实况。
建表语句
借用XXX-BATCH工程中的“batch_cli_user”表部分字段进行实验。
CREATE TABLE `BATCH_CLI_USER` ( `protocol_no` VARCHAR(64) NOT NULL COMMENT '协议号', `user_id` VARCHAR(32) COMMENT '用户标识,录入人ID', `user_name` VARCHAR(80) COMMENT '用户名称', `contact` VARCHAR(32) COMMENT '联系人', `telephone` VARCHAR(16) COMMENT '电话号码', `mobile_no` VARCHAR(16) COMMENT '电话号码', CONSTRAINT `pk_BATCH_CLI_USER` PRIMARY KEY (`protocol_no`) ) ENGINE = MYISAM;
以及使用Innodb存储引擎的建表语句
CREATE TABLE `BATCH_CLI_USER` ( `protocol_no` VARCHAR(64) NOT NULL COMMENT '协议号', `user_id` VARCHAR(32) COMMENT '用户标识,录入人ID', `user_name` VARCHAR(80) COMMENT '用户名称', `contact` VARCHAR(32) COMMENT '联系人', `telephone` VARCHAR(16) COMMENT '电话号码', `mobile_no` VARCHAR(16) COMMENT '电话号码', CONSTRAINT `pk_BATCH_CLI_USER` PRIMARY KEY (`protocol_no`) ) ENGINE = INNODB;
实验思路
通过两次对相同主键的插入,若事务生效,则在第二次插入报主键冲突的时候,应当发生回滚,使第一条的插入语句也被回滚,若事务不生效,则查询后可以观察到该记录。
实验过程
测试代码
如果发送事务回滚,则预期为null正确。
@Test public void testRollbackForMysql() throws Exception { batchCliUserMapper.deleteByPrimaryKey("testProtocolNo004"); BatchCliUser batchCliUser = new BatchCliUser(); batchCliUser.setUserId("testUser004"); batchCliUser.setProtocolNo("testProtocolNo004"); batchCustomerInfoService.testRollbackForMysql(batchCliUser); BatchCliUser result1 = batchCliUserMapper.selectByPrimaryKey("testProtocolNo004"); Assert.assertTrue(null == result1); }
1. MyISAM与@Transactional
代码
@Override@Transactional(rollbackFor = Exception.class) public void testRollbackForMysql(BatchCliUser batchCliUser) throws Exception { // 开启事务,连续插入两次,看事务是否会回滚掉第一次的插入 LOGGER.info("开始第一次插入"); batchCliUserMapper.insertSelective(batchCliUser); LOGGER.info("开始第二次插入"); batchCliUserMapper.insertSelective(batchCliUser);}
结果
代码在第二次插入时抛出org.springframework.dao.DuplicateKeyException。但是在数据库中仍然可以查询到该记录,回滚并未生效。
2. MyISAM与TransactionComponent组件
代码
@Override public void testRollbackForMysql(BatchCliUser batchCliUser) throws Exception { // 显式开启事务,连续插入两次,看事务是否会回滚掉第一次的插入 try { transactionComponent.begin(); LOGGER.info("开始第一次插入"); batchCliUserMapper.insertSelective(batchCliUser); LOGGER.info("开始第二次插入"); batchCliUserMapper.insertSelective(batchCliUser); transactionComponent.commit(); } catch (Exception e) { transactionComponent.rollback(); throw e; } }
结果
与实验1相同,回滚并未生效。
3. INNODB与@Transactional
代码
与实验1相同
结果
成功回滚,数据库中查无记录。
4. INNODB与TransactionComponent组件
代码
与实验2相同
结果
与实验3相同,成功回滚,数据库中查无记录。
后续探究
TransactionTemplate
Spring提供的事务管理模板,验证可以事务回滚。【且相对XXX提供的组件,更方便设置隔离级别,事务传播特性等】
@Autowired public TransactionTemplate transactionTemplate; @Override public void testRollbackForMysql(BatchCliUser batchCliUser) throws Exception { // 显式开启事务,连续插入两次,看事务是否会回滚掉第一次的插入 transactionTemplate.execute(new TransactionCallbackWithoutResult(){ @Override protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { LOGGER.info("开始第一次插入"); batchCliUserMapper.insertSelective(batchCliUser); LOGGER.info("开始第二次插入"); batchCliUserMapper.insertSelective(batchCliUser); } }); }
xml中的Bean配置
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="testTransactionManager"></property> </bean>
深入探究
- 三种方式均可完成回滚,那么其到底有什么共同点,在DataSourceTransactionManager类中有如下方法,在debug时发现:
protected void doRollback(DefaultTransactionStatus status) { DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { this.logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); } try { con.rollback(); } catch (SQLException var5) { throw new TransactionSystemException("Could not roll back JDBC transaction", var5); } }
其核心代码在con.rollback()这一行中,交由各个Connection接口的实现类自己实现。
PS: @Transactional注解还需要配置代理,配置如下后:
<!-- 事务管理器 --> <bean id="testTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="gapsDataSource"/> </bean> <tx:annotation-driven transaction-manager="testTransactionManager" proxy-target-class="true"/>
配置好后@Transactional就可以正常回滚了。
- Mysql事务中到底怎么完成回滚?
调试con.rollback()方法中,可以看到调用了以下方法:
private void rollbackNoChecks() throws SQLException { try { synchronized(this.getConnectionMutex()) { if (!(Boolean)this.useLocalTransactionState.getValue() || this.session.getServerSession().inTransactionOnServer()) { this.session.execSQL((Query)null, "rollback", -1, (NativePacketPayload)null, false, this.nullStatementResultSetFactory, this.database, (ColumnDefinition)null, false); } } } catch (CJException var5) { throw SQLExceptionsMapping.translateException(var5, this.getExceptionInterceptor()); } }
- 方法中执行了execSQL((Query)null, “rollback”,***),去完成了回滚操作。
结论
- @Transactional、TransactionComponent、TransactionTemplate均可以完成在Mysql的INNODB存储引擎上的事务回滚,但不能在默认的MyISAM存储引擎完成回滚。
- TransactionComponent、TransactionTemplate使用方法类似,需要手动使用代码控制事务。@Transactional注解在开发上一般使用在实现类的方法上,使用方便,代码量少,粒度较显式使用会粗一点。
- 三种方法均需配置ResourceTransactionManager的Bean交于Spring管理事务。
- 开发时导出数据库建表语句建议直接显式声明其存储引擎,以防有些数据库并未设置默认引擎为INNODB。
(修改数据库默认存储引擎:在配置文件my.cnf中的 [mysqld] 下面加入default-storage-engine=INNODB)
default-storage-engine=INNODB
- 可以在数据库执行sql show engines查看默认的存储引擎: