本次研究总结包括两种事务管理方式:
第一种是编程式的事务管理(但是仍然需要使用spring配置,因此不能算是纯编程式(就是使用SqlSession进行提交回滚))。
第二种是注解式的事务管理(使用@Transactional标记事务管理的方法,并使用异常事件来控制提交回滚)。
本次研究总结使用的事务管理类:
org.springframework.jdbc.datasource.DataSourceTransactionManager
Maven+Springmvc+mybatis项目
项目如何创建,本次不进行表述,这里只介绍关键配置
数据库表
CREATE TABLE `transaction` (
`transaction_id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`value_one` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`value_two` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`value_three` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`value_fore` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`value_five` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`create_date` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_date` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`transaction_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
Spring+mybatis配置文件(事务相关部分)
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 事务注解驱动,标注@Transactional的类和方法将具有事务性 -->
<tx:annotation-driven/>
实体类(注意,篇幅关系,get,set等基础方法没有列出)
public class Transaction {
private Long transactionId;
private String valueOne;
private String valueTwo;
private String valueThree;
private String valueFore;
private String valueFive;
private Date createDate;
private Date updateDate;
}
Mybatis数据库操作接口以及xml(都是mybatis自动生成功能生成出来的代码,可以忽略,这里只展示使用到的接口方法)
public interface TransactionMapper {
int insertSelective(Transaction record);
}
public interface ExtTransactionMapper extends TransactionMapper {
}
注意,上面之所以将原生(就是mybatis自动生成功能生成的代码)的mapper类继承一次的原因,因为在开发过程中,我们总会遇到以下两种情况:1、因系统业务需要,原生的DML操作不能满足需求。2、因系统业务需要,数据库表结构需要变更(DDL操作),并且需要重新生成mybatis的代码。
当我们遇到这两种情况的时候,如果都对原生的代码进行操作,就会出现互相覆盖的情况,导致辛苦码的代码丢失。因此使用继承的方式进行代码扩展,可以避免上述情况。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.jtexplorer.mapper.TransactionMapper" >
<insert id="insertSelective" parameterType="com.jtexplorer.entity.Transaction" >
<selectKey resultType="java.lang.Long" keyProperty="transactionId" order="AFTER" >
SELECT LAST_INSERT_ID()
</selectKey>
insert into transaction
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="valueOne != null" >
value_one,
</if>
<if test="valueTwo != null" >
value_two,
</if>
<if test="valueThree != null" >
value_three,
</if>
<if test="valueFore != null" >
value_fore,
</if>
<if test="valueFive != null" >
value_five,
</if>
<if test="createDate != null" >
create_date,
</if>
<if test="updateDate != null" >
update_date,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
<if test="valueOne != null" >
#{valueOne,jdbcType=VARCHAR},
</if>
<if test="valueTwo != null" >
#{valueTwo,jdbcType=VARCHAR},
</if>
<if test="valueThree != null" >
#{valueThree,jdbcType=VARCHAR},
</if>
<if test="valueFore != null" >
#{valueFore,jdbcType=VARCHAR},
</if>
<if test="valueFive != null" >
#{valueFive,jdbcType=VARCHAR},
</if>
<if test="createDate != null" >
#{createDate,jdbcType=TIMESTAMP},
</if>
<if test="updateDate != null" >
#{updateDate,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.jtexplorer.mapper.ExtTransactionMapper" >
</mapper>
service接口
public interface TransactionService {
int insertSelective(Transaction record);
int insertTransactionTest(Transaction record);
int insertTransactionTestOne(Transaction record,Integer rORc);
}
二 编程式事务管理
事务管理封装的工具类
@Service
public class TransactionalUtils {
@Resource(name="transactionManager")
private DataSourceTransactionManager transactionManager;
/**
* 获取事务定义
*/
private DefaultTransactionDefinition def;
private TransactionStatus status;
/**
* 事务开始
*/
public void start(){
def= new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
status = transactionManager.getTransaction(def);
}
/**
* 事务回滚(不能和事务提交同时使用,要么提交要么回滚)
*/
public void rollBack(){
transactionManager.rollback(status);
}
/**
* 事务提交(不能喝回滚同时使用,要么提交要么回滚)
*/
public void commit(){
transactionManager.commit(status);
}
}
Controller类
@Slf4j
@RestController
@SuppressWarnings("SpringJavaAutowiringInspection")
@RequestMapping(value = "/mini/transaction")
public class TransactionController {
@Resource
private TransactionService transactionService;
@Resource
TransactionalUtils transactionalUtils;
@PostMapping(value = "/insertTransactionTest")
public JsonResult insertTransactionTest(@ModelAttribute Transaction transaction,
@RequestParam(required = false, defaultValue = "1") Integer rORc) {
JsonResult jsonResult = new JsonResult();
transactionalUtils.start();
int n = transactionService.insertTransactionTest(transaction);
if (rORc == 0) {
transactionalUtils.rollBack();
jsonResult.buildNew(false, n, null, null, null);
} else {
transactionalUtils.commit();
jsonResult.buildNew(true, n, null, null, null);
}
transactionalUtils.start();
transaction.setValueOne("11111111");
transactionService.insertTransactionTestOne(transaction, rORc);
return jsonResult;
}
}
Service接口的实现类中本次使用的方法
@Override
public int insertTransactionTest(Transaction record) {
int n = insertSelective(record);
return n;
}
@Override
public int insertTransactionTestOne (Transaction record,Integer rORc) {
int n = insertSelective(record);
if (rORc == 0 ) {
transactionalUtils.rollBack();
} else {
transactionalUtils.commit();
}
return n;
}
准备工作完成之后,开启项目,并使用postMan(也可以使用其他类似的工具)请求该接口:
本次测试使用的,看代码的逻辑是这样的:rORc为0的时候,两次插入操作都被回滚。
以下是运行结果:
控制台输出的运行结果:
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J] will be managed by Spring
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - ooo Using Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - ==> Preparing: insert into transaction ( value_one, value_two, value_three, value_fore, value_five ) values ( ?, ?, ?, ?, ? )
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - ==> Parameters: 1(String), 2(String), 3(String), 4(String), 5(String)
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - <== Updates: 1
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ooo Using Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ==> Preparing: SELECT LAST_INSERT_ID()
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ==> Parameters:
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - <== Total: 1
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6906cb13]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6906cb13]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6906cb13]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J] after transaction
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [null]: PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J] for JDBC transaction
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J] to manual commit
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b1325cf]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J] will be managed by Spring
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - ooo Using Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - ==> Preparing: insert into transaction ( value_one, value_two, value_three, value_fore, value_five ) values ( ?, ?, ?, ?, ? )
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - ==> Parameters: 11111111(String), 2(String), 3(String), 4(String), 5(String)
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective - <== Updates: 1
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ooo Using Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J]
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ==> Preparing: SELECT LAST_INSERT_ID()
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ==> Parameters:
2019-04-10 16:49:40 DEBUG [http-nio-8080-exec-1] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - <== Total: 1
结果表明两次运行的结果都是正确的。也就是说两次运行都成功插入了数据。那么我们看一下数据库中,表里是否出现了新的数据(数据库查看软件:navicat)
综合以上两个结果,我们可以得出结论:数据库确实成功插入了数据,但是没有提交,两次事务都被回滚了。
接下来我们测试提交的情况:
查看数据库:
注:上述过程是否是两个事务,是否能够独立进行提交和回滚操作,我已经测试过了,测试方案就是修改回滚和提交的条件,让两次插入的提交判断条件不相同,就可以了。此处不再详述
这个注解用于开启事物管理,注意@Transactional注解的使用前提是该方法所在的类是一个Spring Bean,因此(1)中的@Service注解是必须的。换句话说,假如你给方法加了@Transactional注解却没有给类加@Service、@Repository、@Controller、@Component四个注解其中之一将类声明为一个Spring的Bean,那么对方法的事物管理,是不会起作用的。
@Transactional注解,它可以精细到具体的类甚至具体的方法上(区别是同一个类,对方法的事物管理配置会覆盖对类的事务管理配置),另外,声明式事物中的一些属性,在@Transaction注解中都可以进行配置,下面总结一下常用的一些属性
(1) @Transactional(propagation = Propagation.REQUIRED)
最重要的先说,propagation属性表示的是事物的传播特性,一共有以下几种:
事物传播特性 |
作 用 |
Propagation.REQUIRED |
方法运行时如果已经处在一个事物中,那么就加入到这个事物中,否则自己新建一个事物,REQUIRED是默认的事物传播特性 |
Propagation.NOT_SUPPORTED |
如果方法没有关联到一个事物,容器不会为它开启一个事物,如果方法在一个事物中被调用,该事物会被挂起直到方法调用结束再继续执行 |
Propagation.REQUIRES_NEW |
不管是否存在事物,该方法总会为自己发起一个新的事物,如果方法已经运行在一个事物中,则原有事物挂起,新的事物被创建 |
Propagation.MANDATORY |
该方法只能在一个已经存在的事物中执行,业务方法不能发起自己的事物,如果在没有事物的环境下被调用,容器抛出异常 |
Propagation.SUPPORTS |
该方法在某个事物范围内被调用,则方法成为该事物的一部分,如果方法在该事物范围内被调用,该方法就在没有事物的环境下执行 |
Propagation.NEVER |
该方法绝对不能在事物范围内执行,如果在就抛出异常,只有该方法没有关联到任何事物,才正常执行 |
Propagation.NESTED |
如果一个活动的事物存在,则运行在一个嵌套的事物中。如果没有活动事物,则按REQUIRED属性执行,它只对DataSourceTransactionManager事物管理器有效 |
由于没有指定propagation属性,因此事物传播特性为默认的REQUIRED
(2)@Transactional(isolation = Isolation.DEFAULT)
事物隔离级别,这个不细说了,可以参看事物及事物隔离级别一文。
(3)@Transactional(readOnly = true)
该事物是否为一个只读事物,配置这个属性可以提高方法执行效率。
(4)@Transactional(rollbackFor = {ArrayIndexOutOfBoundsException.class, NullPointerException.class})
遇到方法抛出ArrayIndexOutOfBoundsException、NullPointerException两种异常会回滚数据,仅支持RuntimeException的子类。
(5)@Transactional(noRollbackFor = {ArrayIndexOutOfBoundsException.class, NullPointerException.class})
这个和上面的相反,遇到ArrayIndexOutOfBoundsException、NullPointerException两种异常不会回滚数据,同样也是仅支持RuntimeException的子类。
(6)@Transactional(rollbackForClassName = {"NullPointerException"})、@Transactional(noRollbackForClassName = {"NullPointerException"})
这两个放在一起说了,和上面的(4)、(5)差不多,无非是(4)、(5)是通过.class来指定要回滚和不要回滚的异常,这里是通过字符串形式的名字来制定要回滚和不要回滚的异常。
(7)@Transactional(timeout = 30)
事物超时时间,单位为秒。
(8)@Transactional(value = "tran_1")
value这个属性主要就是给某个事物一个名字而已,这样在别的地方就可以使用这个事物的配置。
知识点介绍完毕,接下来介绍我们的试验
首先是controller接口:
/**
* 注解式事务管理
*
* @return JsonResult
*/
@MethodLog(remark = "注解式事务管理")
@PostMapping(value = "/annotationTransactional")
public JsonResult annotationTransactional(@ModelAttribute Transaction transaction,
@RequestParam(required = false, defaultValue = "1") Integer rORc) {
JsonResult jsonResult = new JsonResult();
try {
int n = transactionService.insertTransactionTestOne(transaction, rORc);
if (n > 0) {
jsonResult.setSuccess(true);
}
} catch (Exception e) {
jsonResult.setSuccess(false);
jsonResult.setFailReason(e.toString());
}
return jsonResult;
}
Service接口的实现类中本次使用的方法:
@Override
@Transactional
public int insertTransactionTestOne (Transaction record,Integer rORc) {
insertSelective(record);
int n = 1/rORc;
return n;
}
请求该接口
查看数据库:
接下来,我们测试回滚:
首先查询控制台输出:
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J] will be managed by Spring
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective - ooo Using Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J]
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective - ==> Preparing: insert into transaction ( value_one, value_two, value_three, value_fore, value_five ) values ( ?, ?, ?, ?, ? )
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective - ==> Parameters: 1(String), 2(String), 3(String), 4(String), 5(String)
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective - <== Updates: 1
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ooo Using Connection [jdbc:mysql://192.168.123.199:3306/shopping_basket?useSSL=false&serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true, UserName=root@desktop-13jt1e1, MySQL Connector/J]
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ==> Preparing: SELECT LAST_INSERT_ID()
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - ==> Parameters:
2019-04-10 17:16:26 DEBUG [http-nio-8080-exec-5] com.jtexplorer.mapper.TransactionMapper.insertSelective!selectKey - <== Total: 1
然后查看数据库结果:
数据库中只有第一次使用测试插入的数据,但是控制台信息显示确实有一条数据插入成功了。因此我们可以得出结论,事务已经回滚了。
两种方式的实现使用过程在上文实验中已经介绍的很清楚。
编程式事务管理试验,得出的结论如下:
优点:
缺点:
注解式事务管理试验,得出的结论如下:
优点:
缺点:
两种方式在线程层面上的区别目前还没有进行研究。
{{ cmt.username }}
{{ cmt.content }}
{{ cmt.commentDate | formatDate('YYYY.MM.DD hh:mm') }}