事务概览
事务是一组操作
的集合,它是一个不可分割的工作单位。 事务会将所有的操作作为一个整体一起向系统提交或撤销操作请求,换句话说:这些操作要么同时成功、要么同时失败。
具体案例
我们先看一个需求:现在有两张数据库表:员工表emp
和员工工作经历表emp_expr
,现在需要完成新增一个员工的需求,我们需要在员工表中添加员工的基本信息
和在员工工作经历表中添加员工的工作经历
,这个业务操作就包含了两个操作,根据需求可以先写一个Service
方法:
/*** 新增员工* @param emp 员工实体对象*/
@Override
public void addEmp(Emp emp) {// 为新增员工补全属性emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());// 新增员工empMapper.insertEmp(emp);// 批量添加员工工作经历Integer empId = emp.getId();List<EmpExpr> exprList = emp.getExprList();// CollectionUtils.isEmpty不但会检查集合是否为空,还会检查集合是否为nullif (!CollectionUtils.isEmpty(exprList)) {exprList.forEach(empExpr -> empExpr.setEmpId(empId));empExprMapper.insertBatch(exprList);}
}
这个Service
中的方法先会给Emp员工实体对象补全信息,然后将这些信息添加到数据库表emp
中:
/*** 新增员工* @param emp 员工实体对象*/
@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})")
@Options(useGeneratedKeys = true, keyProperty = "id")
// 添加员工工作经历时需要使用员工id,MyBatis可以设置主键返回,可以返回id主键
void insertEmp(Emp emp);
mapper方法通过@Options注解中的useGeneratedKeys
属性将id主键返回(因为Service方法需要使用id主键)。
假如该新增员工的工作经历
不为空,则会将该员工的工作经历都添加到员工工作经历表emp_expr
中。此时,问题就出现了——因为在这个Service方法中包含了两个操作(一个是添加员工、一个是添加员工工作经历),这两个操作都是一个不可分割的工作单位,但假如在添加完员工之后,方法抛出异常,该方法就会结束,员工信息已经成功保存到了数据库中,然而对应的工作经历没有保存!这就导致了数据的不一致性。
事务操作
事务操作主要分为三步操作:
开启事务
在需要保证数据一致性
的一组操作开始之前,需要先开启事务(start)。
提交事务
如果这一组操作全部执行成功
,则可以提交事务(commit)。
回滚事务
假如这一组操作中有任何一个操作失败
,都必须回滚事务(rollback) 撤销操作,以保证数据一致性。
事务的操作在生活中的使用是十分广泛的:比如银行转账
,如果不使用事务,就会出现转出的账户扣款了,但是转入的账户并没有收到钱,这样的重大BUG
,对于这种涉及多个操作的操作,事务管理是十分重要的。
Spring事务管理
通过上述新增员工的案例可以发现:由于没有使用事务机制,所以说出现了员工信息保存了,但是员工工作经历保存失败的问题
,从而造成数据不一致的问题。其产生的原因是在Service方法中,保存员工之后方法抛出了异常,导致方法结束,保存工作经历的代码就不会执行了。 为了保证数据的一致性,这两个操作要么全部成功、要么全部失败,此时我们就需要使用事务
。
由于我们是使用的springboot
框架构建的项目,所以说想要使用事务并不需要我们手动实现,Spring框架中已经将事务控制的代码封装完毕,我们只需要使用@Transactional注解
即可开启事务控制。
Transactional注解
@Transactional注解
会在当前使用的方法上开启事务,方法全部执行完毕之后提交事务,假如该方法在执行过程中出现了异常,那么就会进行事务回滚来撤销本次操作。@Transactional注解
可以写在业务层类上
,代表该类中的所有方法都交给spring进行事务管理;@Transactional注解
写在接口上
,代表当前接口的所有实现类中的所有方法都交给spring进行事务管理;@Transactional注解
常见于方法上
,代表当前方法交给spring进行事务管理。上述案例中,我们就可以在方法上加上@Transactional注解
将其交给spring事务管理:
/*** 新增员工* @param emp 员工实体对象*/
@Override
@Transactional // 开启Spring事务机制
public void addEmp(Emp emp) {// 为新增员工补全属性emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());// 新增员工empMapper.insertEmp(emp);// 批量添加员工工作经历Integer empId = emp.getId();List<EmpExpr> exprList = emp.getExprList();// CollectionUtils.isEmpty不但会检查集合是否为空,还会检查集合是否为nullif (!CollectionUtils.isEmpty(exprList)) {exprList.forEach(empExpr -> empExpr.setEmpId(empId));empExprMapper.insertBatch(exprList);}
}
我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。我们还可以在application.properties
配置文件中开启事务管理日志:
这样能够更好的使用spring事务管理。
rollbackFor属性
@Transactional
注解可以将方法交给spring进行事务管理,但是在默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务,虽然大部分情况下方法的异常都是运行时异常
,但是为了以防万一,我们希望方法在出现任何类型的异常
时都能回滚事务,此时就需要用到@Transactional
注解中的rollbackFor
属性来控制回滚了:
@Transactional(rollbackFor = Exception.class)
此时,我们就通过rollbackFor
属性指定了方法出现任何异常都将回滚事务。
propagation属性
propagation
属性是用于配置事务的传播行为的。事务的传播行为是指:当一个事务方法被另一个事务方法调用时,该事务方法该如何进行事务控制。
例如:此时有两个事务方法方法A
和方法B
,两个方法都交给spring进行了事务管理。在A方法运行的时候
,首先会开启一个事务,在A方法当中又调用了B方法,但是B方法自身也具有事务
,那么B方法在运行的时候,到底是加入到A方法的事务
当中来,还是B方法在运行的时候新建一个事务
?这就是事务的传播行为。我们可以通过propagation
属性控制事务的传播行为:
对于事务传播行为,只需要关注REQUIRED(默认)
和REQUIRES_NEW
即可。大部分情况下,我们只需要使用REQUIRED
传播行为即可,但是当我们不希望事务之间互相影响(比如下订单前需要记录日志,但是不论下订单成功与否,都需要成功记录日志)就可以使用REQUIRES_NEW
传播行为。
事务四大特性
原子性(Atomicity)
原子性是指事务包装的一组SQL是一个不可分割的工作单元
,事务中的操作要么全部成功、要么全部失败。
一致性(Consistency)
一致性是指一个事务完成之后数据必须处于一致性状态。如果事务执行成功完成,那么数据库的所有变化生效;如果事务执行出现任何错误,那么数据库的所有变化都会回滚(rollback)
,数据库将返回到原始状态。
隔离性(Isolation)
隔离性是指当多用户并发访问数据库时,一个用户的事务不能被其他用户的事务干扰,多个并发的事务之间互相隔离
。
持久性(Durability)
持久性是指一个事务一但被提交或者回滚
,对于数据库的改变是永久性的。
事务的四大特性也简称为ACID
。
总结
当一个方法中包含有多个操作,此时建议开启事务
,事务管理可以确保所有的操作要么全部完成、要么全部回滚,可以保证数据的一致性、完整性
。对于后端操作数据库是十分重要的技术。