一 简介
@Transactional注解包含众多的属性,下面会依次介绍这些属性的作用
- isolation 事务的隔离级别
- propagation 事务的传播行为
- timeout 事务超时时间 当事务执行超时后,会自动终止并回滚 秒为单位
- readOnly 设置事务为只读事务
- rollbackFor 哪些异常需要进行回滚(class 数组)
- rollbackForClassName 哪些异常需要进行回滚(String 数组)
- noRollbackFor 哪些异常事务可以不回滚(class 数组)
- noRollbackForClassName 哪些异常事务可以不回滚 (String 数组)
二 timeout属性
示例
@Service
public class PayService {
@Autowired
private PayDao payDao;
@Autowired
private StockDao stockDao;
public void handleOtherBiz(){
throw new RuntimeException("支付业务处理异常");
}
@Transactional(timeout = 3) //事务超时3秒后自动回滚
public void pay(Pay pay) throws Exception{
Stock stock = stockDao.getStockByProductId(pay.getProductId()); //获取现在的库存
if(stock.getNumber() >= pay.getNumber()){ //表示现在还有库存 可以购买
stockDao.reduceStock(stock.getNumber() - pay.getNumber(), stock.getProductId()); //
Thread.sleep(10000);
payDao.addPay(pay); //新增支付记录
}
}
}
三 readOnly属性
readOnly 表示事务是只读事务,如果事务中出现了非只读事务是会报错的
示例
@Service
public class PayService {
@Autowired
private PayDao payDao;
@Autowired
private StockDao stockDao;
/**
* 该方法执行会报错 因为该方法内不仅仅只有读sql也有写sql的操作
* @param pay
* @throws Exception
*/
@Transactional(timeout = 3, readOnly = true)
public void pay(Pay pay) throws Exception{
Stock stock = stockDao.getStockByProductId(pay.getProductId()); //获取现在的库存
if(stock.getNumber() >= pay.getNumber()){ //表示现在还有库存 可以购买
stockDao.reduceStock(stock.getNumber() - pay.getNumber(), stock.getProductId()); //
payDao.addPay(pay); //新增支付记录
}
}
}
四 noRollbackFor和 noRollbackForClassName
需要知道的是spring事务处理时针对运行时异常是默认是会回滚的,但是对于编译时异常(非RuntimeException)是不会回滚的,如果我们需要将运行时异常进行忽略 可以通过noRollbackFor属性来实现
示例一
@Transactional(timeout = 3, readOnly = false)
public void pay(Pay pay) throws Exception{
Stock stock = stockDao.getStockByProductId(pay.getProductId()); //获取现在的库存
if(stock.getNumber() >= pay.getNumber()){ //表示现在还有库存 可以购买
stockDao.reduceStock(stock.getNumber() - pay.getNumber(), stock.getProductId()); //
payDao.addPay(pay); //新增支付记录
throw new FileNotFoundException(); //抛出编译时异常时 不会回滚数据库
//throw new RuntimeException(); //抛出运行时异常时 会回滚数据库
}
}
示例二
@Transactional(timeout = 3, readOnly = false, noRollbackFor = ArithmeticException.class)
public void pay(Pay pay) throws Exception{
Stock stock = stockDao.getStockByProductId(pay.getProductId()); //获取现在的库存
if(stock.getNumber() >= pay.getNumber()){ //表示现在还有库存 可以购买
stockDao.reduceStock(stock.getNumber() - pay.getNumber(), stock.getProductId()); //
payDao.addPay(pay); //新增支付记录
//throw new FileNotFoundException();
int a = 5/0; //发生ArithmeticException异常,但是数据库不会被回滚
}
}
五 rollbackFor和 rollbackForClassName
rollbackFor和rollbackForClassName能将事务默认不回滚的检查时异常进行强制回滚操作
示例
@Transactional(timeout = 3, readOnly = false, rollbackFor = FileNotFoundException.class)
public void pay(Pay pay) throws Exception{
Stock stock = stockDao.getStockByProductId(pay.getProductId()); //获取现在的库存
if(stock.getNumber() >= pay.getNumber()){ //表示现在还有库存 可以购买
stockDao.reduceStock(stock.getNumber() - pay.getNumber(), stock.getProductId()); //
payDao.addPay(pay); //新增支付记录
throw new FileNotFoundException(); //虽然发生了非运行时异常 但仍然会回滚
}
}
六 isolation
isolation指的是事务的隔离级别,假设现在有两个事务 transaction01、transaction02并发执行
- 脏读 一个事务读取了另外一个事务未提交的数据 简称
读未提交
transaction01将某条记录的age值从20变成了30
transaction02读取了transaction01更新后的值30
transaction01回滚 age变成了20
此时transaction02读到的30就是脏数据 简称脏读
不可重复读 一个事务读取了另一个事务已经提交的数据 简称读已提交
transaction01读取了age值为20
transaction02修改age的值为30
transaction01再次读取时age变成了30
此时称之为不可重复读
幻读
transaction01读取了student表中的一些记录
transaction02此时向student表新增了一些记录
transaction01再次读取student表发现多了一些记录
此时称之为幻读
在事务的隔离级别中,脏读是一定不允许发生的,不可重复读和幻读在某些场景下是可以允许的。
事务的隔离级别包括
- 读未提交(READ UNCOMMITTED) 隔离级别最低 会出现脏读、幻读、不可重复读
- 读已提交(READ COMMITTED) 可以防止脏读,不能防止幻读和不可重复读
- 可重复读(REPEATABLE READ) 可以防止脏读、不可重复读,但是不能防止幻读 对于mysql来说 隔离级别为可重复读时 并不会出现幻读的情况 所以对于mysql来说隔离级别为可重复读就行了
- 串行化(SERIALIZABLE) 禁止事务并行,可以防止脏读、幻读、不可重复读
mysql默认的事务隔离级别为 可重复读
七 propagation
propagation表示事务的传播行为 事务的传播行为用来表述如果多个事务嵌套运行的时候,子事务是否需要和大事务共用一个事务。
序号 | 传播属性 | 描述 | 备注 |
---|---|---|---|
1 | REQUIRED | 如果有事务正在运行,当前方法就在这个事务内运行,否则就启动一个新事务并在自己的事务内运行 | 这种传播行为比较常见 表示方法必须运行在事务中,没有就自己创建事务,有事务(指的是上一级方法有事务)就在该事务中运行不再另外创建事务了 |
2 | REQUIRES_NEW | 当前方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行应当将它挂起 | 表示方法必须运行在事务中,且必须运行在自己创建的事务中,上一级方法的事务,此时会被挂起 也比较常见 |
3 | SUPPORTS | 如果有事务正在运行,当前方法就在这个事务中运行,否则它可以不运行在事务中 | 不怎么常用 |
4 | NOT_SUPPORTED | 当前方法不应该运行在事务中,如果有正在运行的事务,将它挂起 | 不怎么常用 |
5 | MANDATORY | 当前方法必须运行在事务内部,如果没有正在运行的事务就抛出异常 | 不怎么常用 |
6 | NEVER | 当前方法不应该运行在事务中,如果有事务就抛出异常 | 不怎么常用 |
7 | NESTED | 如果有事务正在运行,当前方法就应该在这个事务的嵌套事务内运行,否则就启动一个新事务,并在它自己的事务内运行 |
示例
//一段伪代码用于说明事务传播行为 假设下面的方法都包含事务(即方法上都添加了@Transaction注解) 方法都不try catch
A(){
//B事务传播行为为REQUIRED
B(){
//REQUIRES_NEW
C(){}
//REQUIRED
D(){}
}
//REQUIRES_NEW
E(){
//REQUIRED
F(){}
//REQUIRES_NEW
G(){}
}
//REQUIRED
H(){
}
a = 1 / 0; //如果此处抛异常 那么 A、H、B、D会回滚 而C、E、F、G不会回滚
}
如果是同一个类中的方法之间的相互调用 进行的事务嵌套 事务传播行无效 原因就是事务方法的执行实际上都是通过生成代理对象调用事务方法 如果是在同一个类中的调用,只会有一个方法是通过代理对象调用的 其余的方法都是真实角色的方法
@Component
public class AService{
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void a(){}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b(){}
@Transactional(propagation = Propagation.REQUIRED)
public void c(){
a(); //真实角色的方法 此时这个方法的执行并没有通过aop
b(); //同上
a = 1/0; //执行c方法的时候会发现 a 方法 b 方法并没有如预想的那般 单独创建事务 而是一起呗回滚了 原因就是此时c方法调用的是本类对象的方法 而不是事务方法
}
}