上一篇博客中讲解了关于事务的两种使用方式包括@Transactional的详解。
@Transactional 注解当中的三个常⻅属性:
1. rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
2. Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED
关于第二点和第三点还没有讲解完,这一篇博客来讲解关于事务的隔离级别和传播机制。
1. 事务的隔离级别
事务有4大特性(ACID),原子性、持久性、一致性和隔离性,具体概念如下:
原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatableread)和串行化(Serializable)。
2. Spring中设置事务的隔离级别
![](https://img-blog.csdnimg.cn/direct/9410e855825a43c795a79e2a72852e23.png)
- READ UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。
- READ COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据。因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。
- REPEATABLE READ:可重复读,是 MySQL 的默认事务隔离级别,它能确保同一事务多次查询只的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读(Phantom Read)。
- SERIALIZABLE:序列化,事务最高隔离级别,它会强制事务排序,使之不会发生冲突、从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。
在数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
- Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。
- Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低。
![](https://img-blog.csdnimg.cn/direct/da54d78c20eb46b489f1eedfa258e01d.png)
3. Spring事务传播机制
3.1 什么是事务的传播机制
⽐如公司流程管理执⾏任务之前, 需要先写执⾏⽂档, 任务执⾏结束, 再写总结汇报
此时A部⻔有⼀项⼯作, 需要B部⻔的⽀援, 此时B部⻔是直接使⽤A部⻔的⽂档, 还是新建⼀个⽂档呢?
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题
![](https://img-blog.csdnimg.cn/direct/2cc7c3597abb432a812dfa54424291a6.png)
3.2 事务的传播机制有哪些
@Transactional 注解支持事务传播机制的设置,通过 propagation 属性来指定传播行为。
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION REQUIRED。
3.3 Spring事务传播机制演示
3.3.1 REQUIRED(默认值)
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
比如现在我们有两个银行账户A001和A002,进行转账交易。一个转账的业务可分为两个步骤:
1.A001从自己的账户扣款;
2.A002的账户加上A001扣的款。
如果有任何一步骤失败了,都应该全部回滚。因为如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。两步操作肯定是在同一事务中。
controller代码:
package com.example.transactiondemo.controller;
import com.example.transactiondemo.service.BankService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/bank")
public class BankController {
@Autowired
private BankService bankService;
@PostMapping("/transfer")
@Transactional(propagation = Propagation.REQUIRED)
public String transfer(@RequestParam String fromAccount, @RequestParam String toAccount, @RequestParam double amount) {
//从账户A001扣钱
bankService.transfer1(fromAccount, amount);
//给账户A002加钱
bankService.transfer2(toAccount, amount);
return "Transfer successful";
}
}
service代码:
package com.example.transactiondemo.service;
import com.example.transactiondemo.entity.Account;
import com.example.transactiondemo.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class BankService {
@Resource
private AccountMapper accountMapper;
@Transactional(propagation = Propagation.REQUIRED)
public void transfer1(String fromAccountNumber, double amount) {
Account fromAccount = accountMapper.findByAccountNumber(fromAccountNumber);
if (fromAccount == null ) {
throw new IllegalArgumentException("Invalid account number");
}
if (fromAccount.getBalance() < amount) {
throw new IllegalArgumentException("Insufficient balance in account: " + fromAccountNumber);
}
fromAccount.setBalance(fromAccount.getBalance() - amount);
accountMapper.updateAccount(fromAccount);
}
@Transactional(propagation = Propagation.REQUIRED)
public void transfer2(String toAccountNumber, double amount) {
Account toAccount = accountMapper.findByAccountNumber(toAccountNumber);
if (toAccount == null) {
throw new IllegalArgumentException("Invalid account number");
}
toAccount.setBalance(toAccount.getBalance() + amount);
accountMapper.updateAccount(toAccount);
// 手动引入异常来测试事务回滚
if (amount > 500) {
throw new RuntimeException("Transfer amount exceeds limit, transaction will be rolled back");
}
}
}
注意这个手动加入的异常是伪代码,没有实际意义,为了演示效果。
Mapper代码:
package com.example.transactiondemo.mapper;
import com.example.transactiondemo.entity.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface AccountMapper {
Account findByAccountNumber(@Param("accountNumber") String accountNumber);
void updateAccount(Account account);
}
xml:
<?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.example.transactiondemo.mapper.AccountMapper">
<select id="findByAccountNumber" resultType="com.example.transactiondemo.entity.Account">
SELECT id, account_number AS accountNumber, balance
FROM account
WHERE account_number = #{accountNumber}
</select>
<update id="updateAccount">
UPDATE account
SET balance = #{balance}
WHERE id = #{id}
</update>
</mapper>
使用POSTMAN测试:
第一次转账100应该成功,没有触发异常都正常提交:
第二次转账600应该失败,触发了异常(手动),数据库中没有任何变化:
因为,上述操作的执行流程大概是:
其他事务传播机制的使用都大同小异,主要还是根据不同的场景来觉得使用什么类型的传播机制。
总 结