事务的四大特性:
概念:
事务 是一组操作的集合,它是不可分割的工作单元。事务会把所有操作作为一个整体,一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
注意:
默认MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。
事务的特性:
原子性:
事务是最小的执行单元,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用。不允许部分成功和失败。
一致性:
确保从一个正确的状态转换到另外一个正确的状态。举例,张三把钱转账给李四100元,张三少了100,李四多了100.但是他俩的钱加起来的钱数,还是和转账之前加起来的钱数相同。
隔离性:
并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
持久性:
事务被提交之后,对数据库中数据的改变是持久的,即使数据库发生故障,也不会对其有影响。
操作:
事务执行的三步操作:
开启事务、提交事务/回滚事务
-- 开启事务
start transaction; / begin;
-- 1. 保存员工基本信息
insert into emp values (39, 'Tom', '123456', '汤姆', 1, '13300001111', 1, 4000, '1.jpg', '2023-11-01', 1, now(), now());
-- 2. 保存员工的工作经历信息
insert into emp_expr(emp_id, begin, end, company, job) values (39,'2019-01-01', '2020-01-01', '百度', '开发'), (39,'2020-01-10', '2022-02-01', '阿里', '架构');
-- 提交事务(全部成功) / 回滚事务(有一个失败)
commit; / rollback;
事务之间的相互影响:
脏读,不可重复读,幻读,丢失更新
通过我们的案例,来演示数据库事务:
在EmpController编写,添加员工的接口:
/**
* 新增员工的数据
* @param emp
* @return
* @throws Exception
*/
@PostMapping
public Result add(@RequestBody Emp emp) throws Exception {
log.info("新增员工数据:{}",emp);
empService.add(emp);
return Result.success();
}
编写EmpService接口:
/**
* 新增员工信息
* @param emp
*/
void add(Emp emp) throws Exception;
编写 EmpServiceImpl的实现类:
@Override
public void add(Emp emp) throws Exception {
//先添加员工的基本信息
emp.setCreateTime(LocalDateTime.now()); //赋值初始值
emp.setUpdateTime(LocalDateTime.now()); //赋值初始值
empMapper.addEmp(emp);
//在添加员工的工作经历信息
List<EmpExpr> exprList = emp.getExprList();
if (!CollectionUtils.isEmpty(exprList)){ //当工作经历不是空的时候,在进行添加
exprList.forEach(expr ->{
expr.setEmpId(emp.getId());
});
}
// 批量添加员工的经历
empExprMapper.insertBatch(exprList);
}
注意,我们在添加员工的时候,还需要添加员工的工作经历。
@Insert("insert into emp (username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) values" +
"(#{username},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
void addEmp(Emp emp);
还需要编写 EmpExprMapper接口:
/**
* 批量添加员工经历的数据
* @param exprList
*/
void insertBatch(List<EmpExpr> exprList);
主键返回:
在添加员工的工作经历的时候,我们还需要添加一个字段的值就是 emp_id 但是我们改怎么获取到这个刚添加好的主键id的??
主键返回:
在注解上使用:
@option注解
常用的属性值:
useGeneratedKeys:是否使用主键返回。
keyProperty:返回的id绑定那个属性
示例:
@Options(useGeneratedKeys = true,keyProperty = "id") //使用主键返回,并把返回的主键赋值给id属性,emp对象的 id属性
@Insert("insert into emp (username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) values" +
"(#{username},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
void addEmp(Emp emp);
xml中使用:
<insert id="addUser" useGeneratedKeys="true" keyProperty="id">
insert into tb_user(username,password) values(#{username},#{password})
</insert>
编写 EmpExprMapper接口:
/**
* 批量添加员工经历的数据
* @param exprList
*/
void insertBatch(List<EmpExpr> exprList);
编写EmpExprMapper.xml
<insert id="insertBatch">
insert into emp_expr(emp_id,begin,end,company,job) values
<foreach collection="exprList" item="expr" separator=",">
(#{expr.empId},#{expr.begin},#{expr.end},#{expr.company},#{expr.job})
</foreach>
</insert>
在Api测试我们成功添加了,员工的基本信息和员工的工作经历信息:
接下来,我做一些改动,添加员工的基本信息成功之后,手写一个运行时异常的bug
点击提交,然后看看会发生什么情况。
可以看到,员工的基本信息,添加成功了。
但是员工的工作经历,添加失败了。
为什么会失败呢?看看idea的看控制台
可以发现,控制台出现了异常 除0异常。
但是我们想一想,添加员工基本信息的时候,就要把员工的工作经历信息添加上去,这是才能保证数据的完整性和一致性,不能一个成功一个失败。
这时我们想到了数据库的事务,如果添加员工和添加员工的工作经历都成功了,那么我们才向数据库执行提交 commit,如果其中一个失败了,我们就回滚事务。roooback
我们可以通过spring事务管理,来解决这个问题。
Spring事务管理:
事务控制:
注解: @Transactional
作用:将当前方法交给spring事务进行管理,方法执行前,开启事务,成功执行后提交事务。出现异常回滚事务。
位置:业务(service)层的方法上面,类上,接口上。
作用在接口上:
作用在类上:
作用在方法上:
@Transactional
@Override
public PageBean getList(Integer page, Integer pageSize) {
PageHelper.startPage(page,pageSize);
List<Emp> empList = empMapper.PageList(); //PageHelper后面的第一条SQL语句
System.out.println(empList);
Page<Emp> emps = (Page<Emp>) empList;
return new PageBean(emps.getTotal(),emps.getResult());
}
虽然在方法,接口还有实体类上面都可以添加@Transaction注解,但是我的建议是,在一个方法上面添加,因为有的操作,只涉及到了一张表的操作,也不用添加事务,就像添加一张表,要是成功就成功了,失败就失败了。不会保存在数据库里面的。因此不会造成数据的不完整性。
spring事务管理的日志输出:
开启Spring事务管理的debug级别日志,就可以看到控制台中事务开启、提交、回滚的日志了
在application.properties配置问价里面添加
开启spring事务管理的debug级别日志
logging.level.org.springframework.jdbc.support.JdbcTransactionManager=debug
在添加员工的基本信息的方法上面添加注解:
@Transactional
@Override
public void add(Emp emp) throws Exception {
try {
//先添加员工的基本信息
emp.setCreateTime(LocalDateTime.now()); //赋值初始值
emp.setUpdateTime(LocalDateTime.now()); //赋值初始值
empMapper.addEmp(emp);
//在添加员工的工作经历信息
List<EmpExpr> exprList = emp.getExprList();
if (!CollectionUtils.isEmpty(exprList)){ //当工作经历不是空的时候,在进行添加
exprList.forEach(expr ->{
expr.setEmpId(emp.getId());
});
}
int i = 1/ 0 ;
// 批量添加员工的经历
empExprMapper.insertBatch(exprList);
}finally {
EmpLog empLog = new EmpLog(null,LocalDateTime.now(),emp.toString()); //添加日志
empLogService.insertLog(empLog);
}
}
在测试一下:
可以发现这个时候,已经报错了。
看看数据库里面添加成功里面数据没有:
可以发现员工的基本信息也没有添加进去,说明spring事务生效了。让我们来看一下控制台
在执行添加员工的信息的这个方法时,开始了事务,执行添加员工的基本信息后,SQL是执行成功的,但是里面出现了一个除0的异常,后面的添加员工的工作经历的SQL语句,就不执行了。所以在这一个事务中,一个执行成功了,一个执行失败了,事务就没有commit提交,而是rollback回滚了,所以我们在数据库里面并没有看到有数据添加到数据库里面的表中。
现在·我们把这个除0异常注释掉,看看程序执行会不会报错,数据能不能添加到数据库里面。
前后端联调:
可以看到数据添加成功了。
看看ideal的控制台
事务提交了
看看数据库里面的数据,emp表
在看看emp_expr表
数据也添加成功了
事务进阶:
属性-rollbackFor
- 默认情况下,只有出现 RuntimeException 才回滚异常。
- rollbackFor属性用于控制出现何种异常类型,回滚事务。
如果我们在代码中,添加一个编译时异常,这个时候,spring事务还会回滚吗?
我们可以测试一下,在添加员工的基本信息成功之后,在中间throws一个异常,然后在添加员工的工作经历信息。看看程序会发生什么。
@Transactional
@Override
public void add(Emp emp) throws Exception {
try {
//先添加员工的基本信息
emp.setCreateTime(LocalDateTime.now()); //赋值初始值
emp.setUpdateTime(LocalDateTime.now()); //赋值初始值
empMapper.addEmp(emp);
//在添加员工的工作经历信息
List<EmpExpr> exprList = emp.getExprList();
if (!CollectionUtils.isEmpty(exprList)){ //当工作经历不是空的时候,在进行添加
exprList.forEach(expr ->{
expr.setEmpId(emp.getId());
});
}
if (true){
throw new Exception();
}
// 批量添加员工的经历
empExprMapper.insertBatch(exprList);
}finally {
EmpLog empLog = new EmpLog(null,LocalDateTime.now(),emp.toString()); //添加日志
empLogService.insertLog(empLog);
}
}
服务器端出现异常:
看看数据库里面的数据。
员工的基本信息还是添加成功了
这是因为,默认情况下,只有出现 RuntimeException 才回滚异常。
可以发现在执行添加员工信息的时候,它commit提交了。
这个时候,需要在@Transaction注解里面添加 rollbaclFor属性了。
@Transactional(rollbackFor = {Exception.class}) //开启事务 spring事务默认只能识别到运行时异常,要是想识别到Exception的异常,需要使用rollbackFor
我们在重启服务器测试:
发现数据添加失败了,但是数据库里面没有新增员工的基本信息。
数据表里面刚刚添加的数据。
我们把刚刚 手动写的异常删掉,在运行程序。
可以发现刚刚添加的数据成功了,
看看数据库里面的数据,emp表
查看emp_expr表。数据添加成功了。