Spring注解事务传播与隔离级别的案例分析
案例源码
本文前面都是一些基础概念,猴急的同志可以直接跳转到案例关键代码查看案例说明。
概念
什么是事务
数据库事务是构成单一逻辑工作单元的操作集合:
是数据库操作的最小工作单元,
是作为单个逻辑工作单元执行的一系列操作;
这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;
是一组不可再分割的操作集合(工作逻辑单元)。
银行存款示例:A账户存款1000元
操作集合:
步骤1:A账户增加1000元。
银行转账示例:B账户向A账户转转1000元
操作集合:
步骤1:A账户增加1000元;
步骤2:B账户减少1000元。
事务特性(ACID)
- 原子性(Atomicity):
事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。 - 一致性(Consistency):
事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。 - 隔离性(Isolation):
并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。 - 持久性(Durability):
事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
事务的隔离级别
并发事务存在的问题
-
脏读
脏读是指一个事务读取了另一个事务未提交的数据。
-
不可重复读
不可重复读是指一个事务对同一数据的读取结果前后不一致。脏读和不可重复读的区别在于:前者读取的是事务未提交的脏数据,后者读取的是事务已经提交的数据,只不过因为数据被其他事务修改过导致前后两次读取的结果不一样。
-
幻读
幻读是指事务读取某个范围的数据时,因为其他事务的操作导致前后两次读取的结果不一致。幻读和不可重复读的区别在于,不可重复读是针对确定的某一行数据而言,而幻读是针对不确定的多行数据。因而幻读通常出现在带有查询条件的范围查询中。
隔离级别
- 事务具有隔离性,理论上来说事务之间的执行不应该相互产生影响,其对数据库的影响应该和它们串行执行时一样。
- 然而完全的隔离性会导致系统并发性能很低,降低对资源的利用率,因而实际上对隔离性的要求会有所放宽,这也会一定程度造成对数据库一致性要求降低
- SQL标准为事务定义了不同的隔离级别,从低到高依次是
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 串行化(SERIALIZABLE)
隔离级别和并发事务问题关系
常用数据库默认隔离级别
演示案例
演示环境
一台云服务器主机上安装mysql作为演示环境。
表结构Tbl_user_money
CREATE TABLE `tbl_user_money` (
`TBL_ID` varchar(36) NOT NULL COMMENT '表ID',
`USER_NO` varchar(64) NOT NULL COMMENT '用户帐号',
`MONEY` decimal(10,0) DEFAULT '0' COMMENT '存款金额',
PRIMARY KEY (`TBL_ID`),
UNIQUE KEY `UI_RECORD_ID` (`USER_NO`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户存款表';
基本操作
-
使用连接工具登录服务器,登录mysql
进入docker容器运行mysql的bash
docker exec -it mysql3306 bash -
登录连接mysql
mysql -uroot -pffcsffcs -
设置数据库为study
use study; -
查询表
select * from tbl_user_money;
-
查看当前会话的隔离级别
show variables like 'transaction_isolation';
- 查看全局的隔离级别
show global variables like 'transaction_isolation';
- 设置当前会话的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 查看会话的自动提交模式,默认事务是自动提交的。
show variables like 'autocommit';
- 关闭会话的自动提交模式
SET SESSION AUTOCOMMIT = OFF;
关闭会话自动提交模式
以下演示都需要设置会话的自动提交=OFF
SET SESSION AUTOCOMMIT = OFF;
脏读演示示例
- 会话的事务隔离级别为读未提交(READ UNCOMMITTED)会发生脏读。
打开两个会话,分别设置会话的事务隔离级别为读未提交(READ UNCOMMITTED)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
- 会话的事务隔离级别为读已提交(READ COMMITTED)不会发生脏读。
打开两个会话,分别设置会话的事务隔离级别为读已提交(READ COMMITTED)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
不可重复读演示示例
- 会话的事务隔离级别为读已提交(READ COMMITTED)会发生不可重复读。
打开两个会话,分别设置会话的事务隔离级别为读已提交(READ COMMITTED)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 会话的事务隔离级别为可重复读(REPEATABLE READ)不会发生不可重复读。
打开两个会话,分别设置会话的事务隔离级别为可重复读(REPEATABLE READ)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
幻读演示示例
- 会话的事务隔离级别为可重复读(REPEATABLE READ)会发生幻读。
打开两个会话,分别设置会话的事务隔离级别为可重复读(REPEATABLE READ)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
时间点6理论上按照幻读的原理读取到的应该是3条记录,但是实际测试却发现读取到的还是2条记录。这个是什么原因?
这个是mysql数据库对可重复读的隔离级别做了特殊处理。
MVCC
多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读取隔离级别的实现。
快照读和当前读
通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。
select 快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就不会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。
当前读
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致上面我们测试的那种情况。
select的当前读需要手动的加锁:
select * from table where ? lock in share mode;
select * from table where ? for update;
当前读的方式来演示
Spring注解事务管理
环境准备
mysql数据源配置
spring.datasource.url=jdbc:mysql://111.229.142.101:3306/study?useUnicode=true
spring.datasource.username=study
spring.datasource.password=study123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
建表SQL
CREATE TABLE `tbl_user_money` (
`TBL_ID` varchar(36) NOT NULL COMMENT '表ID',
`USER_NO` varchar(64) NOT NULL COMMENT '用户帐号',
`MONEY` decimal(10,2) DEFAULT '0' COMMENT '存款金额',
PRIMARY KEY (`TBL_ID`),
UNIQUE KEY `UI_RECORD_ID` (`USER_NO`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户存款表';
CREATE TABLE `tbl_goods_store` (
`GOODS_ID` varchar(36) NOT NULL COMMENT '商品ID',
`GOODS_NAME` varchar(36) NOT NULL COMMENT '商品名称',
`PRICE` decimal(10,2) DEFAULT '0.00' COMMENT '单价',
`UNIT` varchar(32) DEFAULT NULL,
`AMOUNT` decimal(10,2) DEFAULT '0' COMMENT '数量',
PRIMARY KEY (`GOODS_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品库存表';
表数据SQL
INSERT INTO `tbl_goods_store` (`GOODS_ID`,`GOODS_NAME`,`PRICE`,`UNIT`,`AMOUNT`) VALUES ('1','一次性医用口罩',3.98,'个',1000);
INSERT INTO `tbl_goods_store` (`GOODS_ID`,`GOODS_NAME`,`PRICE`,`UNIT`,`AMOUNT`) VALUES ('2','N95口罩',49.80,'个',100);
INSERT INTO `tbl_goods_store` (`GOODS_ID`,`GOODS_NAME`,`PRICE`,`UNIT`,`AMOUNT`) VALUES ('3','75%酒精',19.80,'瓶',100);
INSERT INTO `tbl_user_money` (`TBL_ID`,`USER_NO`,`MONEY`) VALUES ('1','zhangsan',6000);
INSERT INTO `tbl_user_money` (`TBL_ID`,`USER_NO`,`MONEY`) VALUES ('2','lisi',1000);
使用springboot生态快速搭建演示项目
Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>cn.chenk.study</groupId>
<artifactId>studyspring</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<name>studyspring</name>
<description>studyspring</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- 这里指定打war包的时不再需要tomcat相关的包,但是本地运行时必须注释掉,否则会报错
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.5</version>
</dependency>
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
</project>
application.properties
#服务端口
server.port=80
#数据源配置
spring.datasource.url=jdbc:mysql://111.229.142.101:3306/study?useUnicode=true
spring.datasource.username=study
spring.datasource.password=study123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#开启swagger服务
swagger.enable=true
# mybatis基础配置
mybatis.mapper-locations=classpath:cn/chenk/study/studyspring/*/mapper/*.xml
项目代码
见studyspring.rar
@Transactional注解
Isolation隔离级别类型
Propagation传播行为类型
spring事务回滚规则
当所拦截的方法有指定异常抛出,事务才会自动进行回滚。
指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。
默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。
Spring事务拦截原理-动态代理
目标对象Target.class
public void methodA(){ dosomethingA(); }
@Transactional
public void methodB(){ dosomethingB(); }
代理对象Proxy.class
// 持有目标对象的引用
Target target ;
public void methodA(){
target.methodA();
}
public void methodB(){
try{
beginTransactional();
target.methodB ();
commit;
}catch(Exception e){
Rollback();
}
}
从上图可以看到spring容器在碰到 拦截注解Transactional时候,会为每个目标对象生成一个代理对象,我们客户端调用的时候,实际上调用的是代理对象。
比如我们要调用目标对象的methodB方法,实际上先调用的是代理对象Proxy的methodB方法,Proxy的methodB方法再调用目标对象的methodB方法。
案例分析
GoodsStoreServiceImpl.java
package cn.chenk.study.studyspring.transaction.service.impl;
import java.math.BigDecimal;
import java.util.Map;
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 cn.chenk.study.studyspring.common.PubUtils;
import cn.chenk.study.studyspring.common.mapper.TableMapper;
import cn.chenk.study.studyspring.common.service.AbstractMainBillService;
import cn.chenk.study.studyspring.transaction.mapper.TblGoodsStoreMapper;
@Service
public class GoodsStoreServiceImpl extends AbstractMainBillService {
@Autowired
private TblGoodsStoreMapper tblGoodsStoreMapper;
@Override
protected String getMainFormKeyName() {
return "goodsId";
}
@Override
protected TableMapper getMainTableMapper() {
return this.tblGoodsStoreMapper;
}
/**
* 出货
* @param goodsId
* @param saleCnt
*/
public void saleGoods(String goodsId,double saleCnt) {
Map<String,Object> goodsStoreMap = getMainTableMapper().select(goodsId) ;
BigDecimal amount = PubUtils.getObjectFromMap(goodsStoreMap, "amount", BigDecimal.class) ;
goodsStoreMap.put("amount", amount.add(new BigDecimal(-saleCnt)) );
this.save(goodsStoreMap);
}
@Transactional(propagation = Propagation.REQUIRED)
public void saleGoods_REQUIRED(String goodsId,double saleCnt) {
saleGoods(goodsId, saleCnt);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saleGoods_REQUIRES_NEW(String goodsId,double saleCnt) {
saleGoods(goodsId, saleCnt);
}
}
UserMoneyServiceImpl.java
package cn.chenk.study.studyspring.transaction.service.impl;
import java.math.BigDecimal;
import java.util.Map;
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 cn.chenk.study.studyspring.common.PubUtils;
import cn.chenk.study.studyspring.common.mapper.TableMapper;
import cn.chenk.study.studyspring.common.service.AbstractMainBillService;
import cn.chenk.study.studyspring.transaction.mapper.TblUserMoneyMapper;
@Service
public class UserMoneyServiceImpl extends AbstractMainBillService {
@Autowired
private TblUserMoneyMapper tblUserMoneyMapper;
@Override
protected String getMainFormKeyName() {
return "tblId";
}
@Override
protected TableMapper getMainTableMapper() {
return this.tblUserMoneyMapper;
}
/**
* - 付款
* @param tblId
* @param payMoney
*/
public void payMoney(String tblId,double payMoney) {
Map<String,Object> userMoneyMap = getMainTableMapper().select(tblId) ;
BigDecimal money = PubUtils.getObjectFromMap(userMoneyMap, "money", BigDecimal.class) ;
userMoneyMap.put("money", money.add(new BigDecimal(-payMoney) ));
this.save(userMoneyMap);
}
@Transactional(propagation = Propagation.REQUIRED)
public void payMoney_REQUIRED(String tblId,double payMoney) {
payMoney(tblId, payMoney);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void payMoney_REQUIRES_NEW(String tblId,double payMoney) {
payMoney(tblId, payMoney);
}
}
案例关键代码
TransactionServiceImpl.java
package cn.chenk.study.studyspring.transaction.service.impl;
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 cn.chenk.study.studyspring.transaction.service.TransactionService;
@Service
public class TransactionServiceImpl implements TransactionService {
@Autowired
private UserMoneyServiceImpl userMoneyServiceImpl;
@Autowired
private GoodsStoreServiceImpl goodsStoreServiceImpl;
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void demo1() throws Exception{
// demo1方法已经开启事务T1
// payMoney_REQUIRED 会加入该事务T1
userMoneyServiceImpl.payMoney_REQUIRED("1",3.98);
// saleGoods_REQUIRED 会加入该事务T1
goodsStoreServiceImpl.saleGoods_REQUIRED("1",1.0);
// 抛出异常全部回滚
buildRuntimeException() ;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void demo2() throws Exception{
// // demo2方法已经开启事务T1
// payMoney_REQUIRES_NEW 会新建事务T2
userMoneyServiceImpl.payMoney_REQUIRES_NEW("1",3.98);
// saleGoods_REQUIRED 会加入该事务T1
goodsStoreServiceImpl.saleGoods_REQUIRED("1",1.0);
// 因为payMoney_REQUIRES_NEW操作是独立开启的事务中操作,所以此处抛出异常 ,不影响上面操作payMoney_REQUIRES_NEW的提交
// 但是saleGoods_REQUIRED会回滚。
buildRuntimeException() ;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void demo3() throws Exception{
userMoneyServiceImpl.payMoney_REQUIRED("1",3.98);
goodsStoreServiceImpl.saleGoods_REQUIRED("1",1.0);
// 抛出默认回滚规则的异常
// 事务会被回滚
throw new RuntimeException("手动抛出的运行时异常") ;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void demo4() throws Exception {
userMoneyServiceImpl.payMoney_REQUIRED("1",3.98);
goodsStoreServiceImpl.saleGoods_REQUIRED("1",1.0);
// 抛出非默认回滚规则的异常
// 事务会不会被回滚
throw new Exception("手动抛出的checked异常") ;
}
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
@Override
public void demo5() throws Exception {
userMoneyServiceImpl.payMoney_REQUIRED("1",3.98);
goodsStoreServiceImpl.saleGoods_REQUIRED("1",1.0);
// 事务会被回滚
throw new Exception("手动抛出的checked异常") ;
}
/**
* 该方法没有声明事务开启,调用该类中开启事务的方法
* 事务不会开启
*/
@Override
public void demo6() throws Exception{
demo6_1() ;
}
@Transactional(propagation = Propagation.REQUIRED)
public void demo6_1() {
userMoneyServiceImpl.payMoney("1",3.98);
// 由于是demo6方法调用的,事务其实是没有开启,所以抛出异常,payMoney也已经提交了
buildRuntimeException() ;
goodsStoreServiceImpl.saleGoods("1",1.0);
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void demo7() throws Exception{
// 调用同一个对象的另外一个开启新事务的方法
demo7_1();
goodsStoreServiceImpl.saleGoods("1",1.0);
// demo7_1执行的操作还没提交 所以事务全部回滚
buildRuntimeException() ;
}
/**
* 声明为新事务的方式,但是由于是同一个对象中的demo7调用,所以不会开启新的事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void demo7_1() {
userMoneyServiceImpl.payMoney("1",3.98);
}
@Override
public void demo8() throws Exception{
// 调用另外一个对象,可以开启新新事物T1
userMoneyServiceImpl.payMoney_REQUIRED("1",3.98);
// 抛出异常,不会影响上述操作payMoney_REQUIRED的提交
// buildRuntimeException() ;
// 调用另外一个对象,可以开启新新事物T2
goodsStoreServiceImpl.saleGoods_REQUIRED("1",1.0);
// 抛出异常,不会影响上述操作payMoney_REQUIRED和saleGoods_REQUIRED的提交
buildRuntimeException() ;
}
private void buildRuntimeException() {
throw new RuntimeException("手动抛出的运行时异常") ;
}
}