Hello,大胸弟们,我们又又又见面了,今天攀哥继续为大家分享一下SpringBoot的教程,没点关注的宝宝,点一下关注。
🌲 事务管理的意义
🌿 身边事务的案例:
- 桃子向好友攀哥汇款5000元,汇款动作主要有两个
1) 桃子的银行账户上扣除5000元
2) 攀哥的银行账户上增加5000元
- 假设操作1)成功了,而操作2)失败了,桃子平白无故少了5000元,而攀哥的账户上也没有增加5000,桃子亏大发了,攀哥饿死了。。。
- 银行需要用技术保证操作1)和操作2) 整体的原子性(即1和2同时成功或者同时失败),由此数据库的事务需求就产生了。
🌿 事务的4大特性:
- 原子性: 一个事务中的所有操作,要么全部提交成功,要么全部失败,不能出现部分成功,部分失败的情况。
- 一致性:事务的执行不能影响数据库数据的完整性和一致性,数据必须从一个一致性状态到另一个一致性状态。
- 隔离性:不同事务之间互不干扰,比如并发环境中,一个事务的执行不能被其他事务干扰。桃子向攀哥汇款、景甜也向攀哥汇款,两个事务互不干扰。
- 持久性: 事务一旦提交,那么对数据库中数据的操作将永远保留,及时停电或者服务器宕机数据也不会丢失。
🌿 实现事务的分类
🍁 编程式事务管理
是指将事务管理代码嵌入到业务方法中去控制事务的提交和回滚,需要在每个业务中包含额外的事务管理代码。
🍁 声明式事务管理
是指将事务管理代码从业务方法中分离出去,以声名的方式实现事务控制,大多数情况下声名式事务比编程式事务管理更好用,主要是通过SpringAop框架去支持声名式事务。
Spring并不直接管理事务,而是通过内置事务管理器实现事务
🌲 Spring声明式事务的注解
Spring事务注解主要有@EnableTransactionManagement 和@Transactional
@EnableTransactionManagement 表示开启事务管理(目前SpringBoot已经支持事务管理,所以无需再到启动类中添加此注解了)
@Transactional 作用在方法上表示该方法会进行事务管理。
- 数据表如下:
DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp` (
`empno` int NOT NULL AUTO_INCREMENT,
`ename` varchar(20) DEFAULT NULL,
`job` varchar(20) DEFAULT NULL,
`manager` int DEFAULT NULL,
`hiredate` date DEFAULT NULL,
`salary` double DEFAULT NULL,
`comm` double DEFAULT NULL,
`deptno` int DEFAULT NULL,
PRIMARY KEY (`empno`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of emp
-- ----------------------------
INSERT INTO `emp` VALUES ('1', '郭靖', 'clerk', '4', '2022-02-03', '7000', '2000', '2');
INSERT INTO `emp` VALUES ('2', '黄蓉', 'saleman', '4', '2023-02-01', '6000', '5000', '3');
INSERT INTO `emp` VALUES ('3', '江湖骗子', 'saleman', '4', '2023-02-18', '7000', '3000', '3');
INSERT INTO `emp` VALUES ('6', '孙尚香', '业务人员', '3', '2023-05-23', '5000', '0', null);
INSERT INTO `emp` VALUES ('7', '赵四', '舞者', '1', '2023-05-30', '5000', '5000', '4');
INSERT INTO `emp` VALUES ('8', '张飞', '开发', '1', '2023-05-30', '8000', '2000', '3');
INSERT INTO `emp` VALUES ('9', '关羽', '运维', '1', '2023-05-30', '6000', '4000', '3');
INSERT INTO `emp` VALUES ('16', '孙尚香', '业务人员', '3', '2023-10-05', '5000', null, null);
INSERT INTO `emp` VALUES ('17', '孙尚香', '业务人员', '3', '2023-10-05', '5000', null, null);
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of dept
-- ----------------------------
INSERT INTO `dept` VALUES ('1', '销售部', '上海');
INSERT INTO `dept` VALUES ('2', '技术部', '深圳');
INSERT INTO `dept` VALUES ('3', '运营部', '武汉');
- 实体类Emp.java:
package com.moxuan.boot_02_mp.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Emp {
private Long empno;
private String ename;
private String job;
private Long manager;
private java.sql.Date hiredate;
private double salary;
private double comm;
private Long deptno;
}
- 实体类:Dept.java
package com.moxuan.boot_02_mp.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
private long id;
private String name;
private String location;
}
- DeptMapper 和EmpMapper
package com.moxuan.boot_02_mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.moxuan.boot_02_mp.entity.Emp;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmpMapper extends BaseMapper<Emp> {
}
package com.moxuan.boot_02_mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.moxuan.boot_02_mp.entity.Dept;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeptMapper extends BaseMapper<Dept> {
}
- TransactionService业务层
package com.moxuan.boot_02_mp.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.moxuan.boot_02_mp.entity.Emp;
import com.moxuan.boot_02_mp.mapper.DeptMapper;
import com.moxuan.boot_02_mp.mapper.EmpMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransactionService {
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptMapper deptMapper;
public void deleteDept(int id){
deptMapper.deleteById(id);
System.out.println(5/0);
deptMapper.selectList(null);
}
}
- 控制层代码:
package com.moxuan.boot_02_mp.controller;
import com.moxuan.boot_02_mp.service.TransactionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TransactionController {
@Autowired
private TransactionService service;
@RequestMapping("/tran1")
public String transaction(){
service.deleteDept(2);
return "";
}
}
运行之后,会发现,在deleteDept方法中,我们有两个操作数据库的手段,一个时删除数据,一个是查询数据。但是在删除数据之后,我们手动制造了一个异常,发生异常后,后面的查询语句将不会被执行。而异常发生之前的语句会执行,也就是删除操作会正常进行。
我们在业务层的方法上面添加@Transactional注解,代码如下:
@Service
public class TransactionService {
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptMapper deptMapper;
@Transactional
public void deleteDept(int id){
deptMapper.deleteById(id);
System.out.println(5/0);
deptMapper.selectList(null);
}
}
再次运行之后,会发现,当发生异常之后,会发现删除的sql语句虽然执行了,但是数据库中的数据却没有被删除。
这是因为方法添加了@Transactional注解后,该方法将会被纳入事务管理。而事务具有原子性,事务中的语句要么全部成功,要么全部失败。而删除语句在发生异常之前虽然执行成功了,而后面的查询语句,由于前面发生了异常并没有执行到,也就执行失败了,故而前面删除操作会被回滚。
此外@Transactional可以设置read-only属性,表示是否仅为可读。默认为false,比如下面代码:
@Transactional(readOnly = false)
public void deleteEmpByDept(int deptNO){
QueryWrapper<Emp> qw = new QueryWrapper<>();
qw.lambda().eq(Emp::getDeptno,deptNO);
empMapper.delete(qw);
}
我们修改一下控制器代码,调用此方法,代码如下:
@RestController
public class TransactionController {
@Autowired
private TransactionService service;
@RequestMapping("/tran1")
public String transaction(){
service.deleteEmpByDept(2);
return "";
}
}
运行之后,会发现当read-only为false的时候,可以正常删除。
接下来,我们将read-only修改为true,代码如下:
@Transactional(readOnly = true)
public void deleteEmpByDept(int deptNO){
QueryWrapper<Emp> qw = new QueryWrapper<>();
qw.lambda().eq(Emp::getDeptno,deptNO);
empMapper.delete(qw);
}
控制器代码不变,进行测试,测试之后发现会出如下异常:
表示当前连接仅为可读,不能作为修改。所以总结一下:
- 增删改操作,需要修改数据库数据的,read-only用false
- 查询操作,read-only用true
由于Spring事务的回滚会自动回滚发生的runtimeException异常,也就是当发生异常之后事务会自动检测到,比如下面案例中:
@Transactional
public void deleteDeptAndEmp(){
deleteDept(3);
System.out.println(5/0);
deleteEmpByDept(3);
}
我们在方法中想要删除部门号为3的部门以及部门号为3的所有员工,测试代码如下:
package com.moxuan.boot_02_mp.controller;
import com.moxuan.boot_02_mp.service.TransactionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TransactionController {
@Autowired
private TransactionService service;
@RequestMapping("/tran1")
public String transaction(){
service.deleteDeptAndEmp();
return "";
}
}
当运行项目之后,访问请求,会出现如下结果:
在发生异常之前,删除部门的操作执行了,但是去数据库看,部门号为3的部门依旧存在,说明最终事务回滚了。
那如果,我们将异常手动处理了,会出现什么样的状况???看如下代码,我们在deleteDept中捕获一下异常:
package com.moxuan.boot_02_mp.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.moxuan.boot_02_mp.entity.Emp;
import com.moxuan.boot_02_mp.mapper.DeptMapper;
import com.moxuan.boot_02_mp.mapper.EmpMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransactionService {
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptMapper deptMapper;
@Transactional
public void deleteDept(int id){
deptMapper.deleteById(id);
try {
System.out.println(5 / 0);
}catch (Exception e){
e.printStackTrace();
}
deptMapper.selectList(null);
}
@Transactional(readOnly = true)
public void deleteEmpByDept(int deptNO){
QueryWrapper<Emp> qw = new QueryWrapper<>();
qw.lambda().eq(Emp::getDeptno,deptNO);
empMapper.delete(qw);
}
@Transactional
public void deleteDeptAndEmp(){
deleteDept(3);
deleteEmpByDept(3);
}
}
然后在控制层调用deleteDeptAndEmp()方法,代码如下:
package com.moxuan.boot_02_mp.controller;
import com.moxuan.boot_02_mp.service.TransactionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TransactionController {
@Autowired
private TransactionService service;
@RequestMapping("/tran1")
public String transaction(){
service.deleteDeptAndEmp();
return "";
}
}
运行起来之后,会发现虽然发生了异常,但是部门号为3的部门和部门号为3的员工都会被删除,结果如下:
因此只有当显示触发抛出异常的时候,事务管理才能将其检测到。处理之后的异常将不会触发事务管理。
🍁 rollbackfor和noRollbackfor
运行时异常(RuntimeException):默认都回滚。 int a=1/0
编译时异常(RuntimeException以外的异常):默认不回滚。 throw IOException
在@Transactional注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚
先看看异常分类:
当然,我们也可以指定当发生某个指定异常的时候才发生回滚,也可以指定当发生某个异常的时候不回滚。
- rollbackFor 当发生哪些指定编译异常时发生回滚
/**
* rollbackFor 可以指定哪些编译型异常发生回滚
* @throws SQLException
* @throws IOException
*/
@Transactional(rollbackFor = {IOException.class,SQLException.class})
public void updateDept1() throws SQLException,IOException {
Dept dept = new Dept();
dept.setId(1L);
dept.setName("打铁部5");
deptMapper.updateById(dept);
// throw new IOException("IO异常");
throw new MyException("自定义异常");// 运行异常,默认都是回滚的
}
上面代码配置的
rollbackFor = {IOException.class,SQLException.class})
表示当发生IOException和SQLException的时候会回滚。
- noRollbackFor 当发生哪些指定的运行异常时,不发生回滚
/**
* noRollbackFor 可以指定哪些运行时异常不发生回滚
* @throws SQLException
* @throws IOException
*/
@Transactional(noRollbackFor = {MyException.class,ArithmeticException.class})
public void updateDept2() throws SQLException,IOException {
Dept dept = new Dept();
dept.setId(1L);
dept.setName("打铁部1");
deptMapper.updateById(dept);
throw new ArithmeticException("运行时异常");
}
- 由于运行时异常默认回滚,编译异常默认不回滚。如果需要同时指定哪些编译异常回滚和哪些运行时异常不会滚,该如何写?
- rollbackfor 和noRollbackfor可以同时使用
/**
* rollbackFor 可以指定哪些编译型异常发生回滚
* @throws SQLException
* @throws IOException
*/
@Transactional(rollbackFor = {IOException.class,SQLException.class} ,noRollbackFor = {MyException.class})
public void updateDept1() throws SQLException,IOException {
Dept dept = new Dept();
dept.setId(1L);
dept.setName("打铁部5");
deptMapper.updateById(dept);
// throw new IOException("IO异常");
throw new MyException("自定义异常");// 运行异常,默认都是回滚的
}
🌲SpringBoot 事务隔离级别
定义了一个事务不受其他事务影响的程度。
🌿 数据库事务隔离级别:
- 读未提交(Read Uncommitted):允许脏读,
在这个隔离级别下,一个事务可以读取另一个未提交的事务的数据。这种隔离级别的优点是并发性最高,但是数据的一致性最差。因为一个事务可能会读取到另一个事务未提交的数据,导致数据不一致。
事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读。
- 读已提交(Read Committed):
在这个隔离级别下,一个事务只能读取另一个已提交的事务的数据。这种隔离级别的优点是数据的一致性比较好,但是并发性比较差。因为一个事务必须等另一个事务提交之后才能读取它的数据。
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他买单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…
分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。(收费系统的两次读取)
- 可重复读(Repeatable Read):
在这个隔离级别下,一个事务可以多次读取同一行数据,而且每次读取的结果都是一样的。这种隔离级别的优点是数据的一致性比较好,但是并发性比较差。因为一个事务必须锁定读取的数据,以防止另一个事务修改它。
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
分析:可重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。
什么时候会出现幻读?
事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。
- 序列化(Serializable):
在这个隔离级别下,所有的事务都会被串行化执行。这种隔离级别的优点是数据的一致性最好,但是并发性最差。因为所有的事务都必须等待前一个事务执行完毕之后才能执行。
隔离级别越高,数据的一致性和隔离性就越好,但也会带来更多的性能开销和系统负担。在实际应用中,需要根据具体的业务需求和系统性能要求,选择合适的隔离级别。
🌿 四种隔离级别可能导致的问题:
🍁 问题描述
1、Serializable (串行化):最严格的级别,事务串行执行,资源消耗最大;
2、REPEATABLE READ(重复读) :保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但不能避免“幻读”,但是带来了更多的性能损失。
3、READ COMMITTED (提交读):大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”,但不能避免“幻读”和“不可重复读取”。该级别适用于大多数系统。
4、Read Uncommitted(未提交读) :事务中的修改,即使没有提交,其他事务也可以看得到,会导致“脏读”、“幻读”和“不可重复读取”。
🍁 什么是脏读?
所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。
也就是说,当前事务读到的数据是别的事务想要修改的但是没有修改成功的数据。
🍁 什么是不可重复读?
事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了
也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。
🍁 什么是幻读?
事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。
也就是说,当前事务读第一次取到的数据比后来读取到数据条目不一致。
🌿 SpringBoot中事务的隔离级别设置
- ISOLATION_DEFAULT : 采用数据库默认的事务隔离级别,与数据库隔离级别保持一致
- ISOLATION_READ_UNCOMMITTED : 对应数据库的读未提交(Read Uncommitted)
- ISOLATION_READ_COMMITTED :对应数据库的读已提交(Read Committed)
- ISOLATION_REPEATABLE_READ:对应数据库的可重复读(Repeatable Read)
- ISOLATION_SERIALIZABLE:序列化(Serializable)
设置隔离级别:在@Transactional中添加isolation
/**
* rollbackFor 可以指定哪些编译型异常发生回滚
* @throws SQLException
* @throws IOException
*/
@Transactional(isolation = Isolation.READ_COMMITTED ,rollbackFor = {IOException.class,SQLException.class} ,noRollbackFor = {MyException.class})
public void updateDept1() throws SQLException,IOException {
Dept dept = new Dept();
dept.setId(1L);
dept.setName("打铁部5");
deptMapper.updateById(dept);
// throw new IOException("IO异常");
throw new MyException("自定义异常");// 运行异常,默认都是回滚的
}