传统事务流程:
Connection connection = JdbcUtils.getConnection();
try {
//1. 先设置事务不要自动提交
connection.setAutoCommit(false);
//2. 进行各种 crud
//多个表的修改,添加 ,删除
select from 商品表 => 获取价格
//修改用户余额
update ...
//修改库存量
update
//3. 提交
connection.commit();
} catch (Exception e) {
//4. 回滚
conection.rollback();
}
使用 Spring 的声明式事务处理, 可以将上面三个子步骤分别写成一个方法,然后统一管理
1. 使用实例
1.1 创建 src\tx_ioc.xml 文件
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.userName}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
</bean>
<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 将上面的数据源分配给 jdbcTemplate -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="dataSourceTransactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启基于注解的声明式事务功能 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
<!-- 加入自动扫描包 dao -->
<context:component-scan base-package="com.hspedu.spring.tx.dao"/>
1.2 修改对象
修改 GoodsService.java,加入声明式事务注解
@Transactional
public void buyGoodsByTx(int user_id, int goods_id, int num) {
//查询到商品价格
Float goods_price = goodsDao.queryPriceById(goods_id);
//购买商品,减去余额
goodsDao.updateBalance(user_id, goods_price * num);
//模拟一个异常, 会发生数据库数据不一致现象
// int i = 10 / 0;
//更新库存
goodsDao.updateAmount(goods_id, num);
}
1.3 声明式事务机制
- 使用@Transactional 可以进行声明式事务控制;即将标识的方法中的,对数据库的操作作为一个事务管理
- @Transactional 底层使用的仍然是AOP机制
- 底层是使用动态代理对象来调用buyGoodsByTx
执行流程:
- 在执行buyGoodsByTx() 方法 先调用 事务管理器的 doBegin(),主要是将自动提交关掉
- 然后调用 buyGoodsByTx()
- 如果执行没有发生异常,则调用 事务管理器的 doCommit()
- 如果发生异常 调用事务管理器的 doRollback()
2. 事务的传播机制
当有多个事务处理并存时,如何控制?
2.1 事务的传播机制种类
- REQUIRED:如果现在有事务在运行,当前的方法就是在这个事务内运行
- REQUIRED_NEW:当前方法必须新启动一个事务,在自己的事务内运行
- SUPPORTS:如果现在有事务在运行,当前方法就在这个事务内运行,否则它可以不运行在事务中
- NOT_SUPPORTE:当前方法不应该运行在事务内,如果有事务,该方法将挂起
- MANDATORY:当前方法必须运行在事务内,如果没有运行在事务,则抛出异常
- NEVER:当前方法不可以运行在事务内,如果运行在事务,则抛出异常
- NESTED:如果现在有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则就启动一个新的事务,并运行在自己的事务里
重点分析 REQUIRED 和 REQUIRED_NEW
1)REQUIED(默认事务传播机制)
2)REQUIRES_NEW
3)事务的传播机制的设置方法
4)例子
如果设置为 REQUIRES_NEW:
buyGoods2 如果错误,不会影响到 buyGoods(),即它们的事务是独立的
如果设置为 REQUIRED:
buyGoods2 和 buyGoods 是一个整体,只要有方法的事务错误,那么两个方法都不会执行成功.!
3. 事务的隔离级别
这个概念参考 MySQL
3.1 说明
默认的隔离级别, 就是 mysql 数据库默认的隔离级别 一般为 REPEATABLE_READ
查看数据库默认的隔离级别:
SELECT @@global.tx_isolation
3.2 事务隔离级别的设置及测试
1)默认
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoodsByTxISOLATION(int user_id, int goods_id, int num) {
//查询到商品价格
Float goods_price = goodsDao.queryPriceById(goods_id);
System.out.println("第一次读取的价格 =" + goods_price);
//测试一下隔离级别,在同一个事务中,查询一下价格
goods_price = goodsDao.queryPriceById(goods_id);
System.out.println("第二次读取的价格 =" + goods_price);
}
过程:在读完第一次数据后,通过 mysql 进行修改该数据
结果:两次读取到的价格是一样的,不会受到 MySQL 修改影响
2)READ_COMMITTED 隔离级别情况
语法:
结果:两次读取到的价格是不一样的,数据是会受到 MySQL 修改影响
4. 事务的超时回滚
目的:如果一个事务执行的时间超过某个时间限制,就让该事务回滚
语法:
超出时间会抛出异常,事务回滚,原来的操作撤销
注:
- timeout = 2 表示 buyGoodsByTxTimeout 如果执行时间超过了2秒, 该事务就进行回滚
- 如果没有设置 timeout, 默认是 -1,表示使用事务的默认超时时间,或者不支持