JAVAEE之事务和事务传播机制

1.事务

1.1 事务的概念

事务是⼀组操作的集合, 是⼀个不可分割的操作.
事务会把所有的操作作为⼀个整体, ⼀起向数据库提交或者是撤销操作请求.
所以这组操作要么同时成功, 要么同时失败.

1.2 需要事务的原因

  • 转账的时候,要么同时成功,要么同时失败;若不是,会使用户有资金损失
  • 秒杀系统

1.3 事务的操作 

事务的操作主要有三步:
1. 开启事务:start transaction/ begin (⼀组操作前开启事务)
2. 提交事务: commit (这组操作全部成功, 提交事务)
3. 回滚事务: rollback (这组操作中间任何⼀个操作出现异常, 回滚事务)

2. Spring 中事务的实现

Spring 中的事务操作分为两类:
1. 编程式事务(⼿动写代码操作事务).
2. 声明式事务(利⽤注解⾃动开启和提交事务).
在学习事务之前, 我们先准备数据和数据的访问代码
需求: ⽤⼾注册, 注册时在⽇志表中插⼊⼀条操作记录.

数据准备:

DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
USE trans_test;
-- 用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
 `id` INT NOT NULL AUTO_INCREMENT,
 `user_name` VARCHAR (128) NOT NULL,
 `password` VARCHAR (128) NOT NULL,
 `create_time` DATETIME DEFAULT now(),
 `update_time` DATETIME DEFAULT now() ON UPDATE now(),
 PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '用户表';

-- 操作日志表
DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info (
 `id` INT PRIMARY KEY auto_increment,
 `user_name` VARCHAR ( 128 ) NOT NULL,
 `op` VARCHAR ( 256 ) NOT NULL,
 `create_time` DATETIME DEFAULT now(),
 `update_time` DATETIME DEFAULT now() ON UPDATE now() 
) DEFAULT charset 'utf8mb4';
代码准备:
1. 创建项⽬ spring-trans, 引⼊Spring Web, Mybatis, mysql等依赖
2. 配置⽂件
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  configuration: # 配置打印 MyBatis⽇志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true #配置驼峰⾃动转换
3. 实体类
4. Mapper
5. Service
6.controller

2.1 Spring 编程式事务

Spring ⼿动操作事务和上⾯ MySQL 操作事务类似, 有 3 个重要操作步骤:
开启事务(获取事务)
提交事务
回滚事务
SpringBoot 内置了两个对象:
1. DataSourceTransactionManager 事务管理器.
⽤来获取事务(开启事务), 提交或回滚事务
2. TransactionDefinition 是事务的属性,
在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus
根据代码进行学习:

1.观察事务提交

postman: http://127.0.0.1:8080/user/registry?name=admin&password=admin
观察数据库的结果, 数据插⼊成功
2.观察事务回滚
观察数据库, 虽然程序返回"注册成功", 但数据库并没有新增数据.
以上代码虽然可以实现事务, 但操作也很繁琐, 有没有更简单的实现⽅法呢?
接下来我们学习声明式事务

2.2 Spring 声明式事务 @Transactional

声明式事务的实现很简单, 只需要在需要事务的⽅法上添加 @Transactional 注解就可以实现了.
⽆需⼿动开启事务和提交事务, 进⼊⽅法时⾃动开启事务,
⽅法执⾏完会⾃动提交事务,
如果中途发⽣了没有处理的异常会⾃动回滚事务.

 

运⾏程序, 发现数据插⼊成功.

修改程序, 使之出现异常

 

运⾏程序:
发现虽然⽇志显⽰数据插⼊成功, 但数据库却没有新增数据, 事务进⾏了回滚.

我们⼀般会在业务逻辑层当中来控制事务, 因为在业务逻辑层当中, ⼀个业务功能可能会包含多个数
据访问的操作.
在业务逻辑层来控制事务, 我们就可以将多个数据访问操作控制在⼀个事务范围内.
@Transactional 作⽤
@Transactional 可以⽤来修饰⽅法或类:
修饰⽅法时: 只有修饰public ⽅法时才⽣效(修饰其他⽅法时不会报错, 也不⽣效)[推荐]
修饰类时: 对 @Transactional 修饰的类中所有的 public ⽅法都⽣效.
⽅法/类被 @Transactional 注解修饰时, 在⽬标⽅法执⾏开始之前, 会⾃动开启事务, ⽅法执⾏结束
之后, ⾃动提交事务.
如果在⽅法执⾏过程中, 出现异常, 且异常未被捕获, 就进⾏事务回滚操作.
如果异常被程序捕获, ⽅法就被认为是成功执⾏, 依然会提交事务.

修改上述代码, 对异常进⾏捕获  

运⾏程序, 发现虽然程序出错了, 但是由于异常被捕获了, 所以事务依然得到了提交.

 

如果需要事务进⾏回滚, 有以下两种⽅式:
1. 重新抛出异常

 

2.手动回滚事务 

使⽤ TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并
使⽤ setRollbackOnly 设置 setRollbackOnly

 

3. @Transactional 详解

通过上⾯的代码, 我们学习了 @Transactional 的基本使⽤.
接下来我们学习 @Transactional注解的使⽤细节.
我们主要学习 @Transactional 注解当中的三个常⻅属性:
1. rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
2. Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED

3.1 rollbackFor

@Transactional 默认只在遇到运⾏时异常和Error时才会回滚, ⾮运⾏时异常不回滚.
即Exception的⼦类中, 除了RuntimeException及其⼦类.

 

如果我们需要所有异常都回滚,
需要来配置 @Transactional 注解当中的 rollbackFor 属性,
通过 rollbackFor 这个属性指定出现何种异常类型时事务进⾏回滚.
结论:
• 在Spring的事务管理中,默认只在遇到运⾏时异常RuntimeException和Error时才会回滚.
• 如果需要回滚指定类型的异常, 可以通过rollbackFor属性来指定.

3.2 事务隔离级别

3.2.1 MySQL 事务隔离级别

SQL 标准定义了四种隔离级别, MySQL 全都⽀持. 这四种隔离级别分别是:
1. 读未提交(READ UNCOMMITTED):
读未提交, 也叫未提交读. 该隔离级别的事务可以看到其他事务中未提交的数据.
因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数据称之为脏数据, 这个问题称之为脏读.
2. 读提交(READ COMMITTED):
读已提交, 也叫提交读. 该隔离级别的事务能读取到已经提交事务的数据,
该隔离级别不会有脏读的问题.但由于在事务的执⾏中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读
3. 可重复读(REPEATABLE READ):
事务不会读到其他事务对已有数据的修改, 即使其他事务已提交.
也就可以确保同⼀事务多次查询的结果⼀致, 但是其他事务新插⼊的数据, 是可以感知到的.
这也就引发了幻读问题. 可重复读, 是 MySQL 的默认事务隔离级别.
⽐如此级别的事务正在执⾏时, 另⼀个事务成功的插⼊了某条数据, 但因为它每次查询的结果都是⼀样的, 所以会导致查询不到这条数据, ⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因). 明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去, 这个现象叫幻读.
4. 串⾏化(SERIALIZABLE): 序列化, 事务最⾼隔离级别. 它会强制事务排序, 使之不会发⽣冲突, 从⽽解决了脏读, 不可重复读和幻读问题, 但因为执⾏效率低, 所以真正使⽤的场景并不多.
在数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:

3.2.2 Spring 事务隔离级别

Spring 中事务隔离级别有5 种:
1. Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.
2. Isolation.READ_UNCOMMITTED : 读未提交, 对应SQL标准中 READ UNCOMMITTED
3. Isolation.READ_COMMITTED : 读已提交,对应SQL标准中 READ COMMITTED
4. Isolation.REPEATABLE_READ : 可重复读, 对应SQL标准中 REPEATABLE READ
5. Isolation.SERIALIZABLE : 串⾏化, 对应SQL标准中 SERIALIZABLE

Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置

 

3.3 Spring 事务传播机制

3.3.1 什么是事务传播机制

事务传播机制就是: 多个事务⽅法存在调⽤关系时, 事务是如何在这些⽅法间进⾏传播的.
⽐如有两个⽅法A, B都被 @Transactional 修饰,
A⽅法调⽤B⽅法.
A⽅法运⾏时, 会开启⼀个事务.
当A调⽤B时, B⽅法本⾝也有事务, 此时B⽅法运⾏时, 是加⼊A的事务, 还是创建⼀个新的事务呢?
这个就涉及到了事务的传播机制.
⽐如公司流程管理
执⾏任务之前, 需要先写执⾏⽂档, 任务执⾏结束, 再写总结汇报
此时A部⻔有⼀项⼯作, 需要B部⻔的⽀援, 此时B部⻔是直接使⽤A部⻔的⽂档, 还是新建⼀个⽂档呢?

 事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题

⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题

3.3.2 事务的传播机制的分类

@Transactional 注解⽀持事务传播机制的设置, 通过 propagation 属性来指定传播⾏为.
Spring 事务传播机制有以下 7 种:
1. Propagation.REQUIRED : 默认的事务传播级别. 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务.
2. Propagation.SUPPORTS : 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的⽅式继续运⾏.
3. Propagation.MANDATORY :强制性. 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则抛出异常.
4. Propagation.REQUIRES_NEW : 创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起. 也就是说不管外部⽅法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开
启⾃⼰的事务, 且开启的事务相互独⽴, 互不⼲扰.
5. Propagation.NOT_SUPPORTED : 以⾮事务⽅式运⾏, 如果当前存在事务, 则把当前事务挂起(不⽤).
6. Propagation.NEVER : 以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常.
7. Propagation.NESTED : 如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏. 如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED

 

⽐如⼀对新⼈要结婚了, 关于是否需要房⼦
1. Propagation.REQUIRED : 需要有房⼦. 如果你有房, 我们就⼀起住, 如果你没房, 我们就⼀起买房. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务)
2. Propagation.SUPPORTS : 可以有房⼦. 如果你有房, 那就⼀起住. 如果没房, 那就租房. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的⽅式继续运⾏)
3. Propagation.MANDATORY : 必须有房⼦. 要求必须有房, 如果没房就不结婚. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则抛出异常)
4. Propagation.REQUIRES_NEW : 必须买新房. 不管你有没有房, 必须要两个⼈⼀起买房. 即使有房也不住. (创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起)
5. Propagation.NOT_SUPPORTED : 不需要房. 不管你有没有房, 我都不住, 必须租房.(以⾮事务⽅式运⾏, 如果当前存在事务, 则把当前事务挂起)
6. Propagation.NEVER : 不能有房⼦. (以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常)
7. Propagation.NESTED : 如果你没房, 就⼀起买房. 如果你有房, 我们就以房⼦为根据地,做点下⽣意. (如果如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏. 如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED )

3.3.3 Spring 事务传播机制使⽤和各种场景演⽰

对于以上事务传播机制,我们重点关注以下两个就可以了:
1. REQUIRED(默认值)
2. REQUIRES_NEW
3.3.3.1 REQUIRED(加⼊事务)
看下⾯代码实现:
1. ⽤⼾注册, 插⼊⼀条数据
2. 记录操作⽇志, 插⼊⼀条数据(出现异常)
观察 propagation = Propagation.REQUIRED 的执⾏结果
对应的UserService和LogService都添加上
@Transactional(propagation =Propagation.REQUIRED )
//正确

//制造错误 

运⾏程序, 发现数据库没有插⼊任何数据.
流程描述:
1. p1 ⽅法开始事务
2. ⽤⼾注册, 插⼊⼀条数据 (执⾏成功) (和p1 使⽤同⼀个事务)
3. 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (和p1 使⽤同⼀个事务)
4. 因为步骤3出现异常, 事务回滚. 步骤2和3使⽤同⼀个事务, 所以步骤2的数据也回滚了

3.3.4 NESTED和REQUIRED区别

  • 整个事务如果全部执⾏成功, ⼆者的结果是⼀样的.
  • 如果事务⼀部分执⾏成功, REQUIRED加⼊事务会导致整个事务全部回滚. NESTED嵌套事务可以实现局部回滚, 不会影响上⼀个⽅法中执⾏的结果.

 

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值