事务: 是一组操作的集合,是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败
事务的操作
需求:解散部门(删除部门),同时删除该部门下的员工
我们之前业务逻辑仅仅删除了部门,并没有删除该部门下的员工,此时造成数据的不完整、不一致。
@Delete("delete from dept where id= #{id}")
void deleteById(Integer id);
下面进行完善
SQL
DeptMapper
@Delete("delete from dept where id= #{id}")
void deleteById(Integer id);
EmpMapper
/**
* 根据部门ID删除该部门下的员工数据
* @param id 部门id
*/
@Delete("delete from emp where dept_id =#{id}")
void deleteByDeptId(Integer id);
业务代码
@Override
public void deleteById(Integer id) {
//根据id删除部门数据
deptMapper.deleteById(id);
//根据部门id删除员工数据
empMapper.deleteByDeptId(id);
}
假如在业务代码执行过程中出现异常了,会发生什么情况?
可能会发生部门删除了但是部门里面员工没有被删除。此时造成数据的不一致。
为了保证数据一致性,我们要保证删除部门和删除员工同时成功或者同时失败,也就是说这两步操作都在同一个事务当中
位置: 业务(Service)层方法上、类上、接口上
作用: 将当前方法交给Spring进行事务管理,方法执行前开启事务;成功执行完毕,提交事务;出现异常,回滚事务
我们一般添加在业务层执行多次数据访问操作的方法上
@Transactional
@Override
public void deleteById(Integer id) {
//根据id删除部门数据
deptMapper.deleteById(id);
//根据部门id删除员工数据
empMapper.deleteByDeptId(id);
}
logging:
level:
org.springframework.jdbc.support JdbcTransactionManager: debug
比如说我们手动throw了一个Exception,并不会出现回滚的情况,而是直接将事务提交了
@Transactional
@Override
public void deleteById(Integer id) throws Exception {
//根据id删除部门数据
deptMapper.deleteById(id);
if (true) {
throw new Exception("出错了");
}
//根据部门id删除员工数据
empMapper.deleteByDeptId(id);
}
rollbackFor 属性用于控制出现哪一种异常类型的时候,进行回滚事务
这样配置后,所有的异常都会进行事务的回滚
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteById(Integer id) throws Exception {
//根据id删除部门数据
deptMapper.deleteById(id);
if (true) {
throw new Exception("出错了");
}
//根据部门id删除员工数据
empMapper.deleteByDeptId(id);
}
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值 | 含义 |
---|---|
REQUIRED | [默认值] 需要事务,有则加入(b事务加入到a事务),无则创建新事务 |
**QEQUIRES_NEW ** | 需要创建新事务,无论有无,总是创建新事务(b创立一个新事务),如果创建新事务,当前事务进行挂起,等新事务完成后再进行当前事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务(a事务先挂起先执行b事务,b事务完成后再执行a事务) |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… | … |
需求:解散部门时,无论成功还是失败,都要记录操作日志
步骤:
① 解散部门: 删除部门、删除部门下的员工
② 记录日志到数据库表中
create table dept_log(
id int auto_increment comment '主键ID' primary key,
create_time datetime null comment '操作时间',
description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
日志信息实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
private Integer id;
private LocalDateTime createTime;
private String description;
}
日志插入SQL
@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
void insert(DeptLog log);
}
日志插入业务代码
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional //事务传播行为:有事务就加入、没有事务就新建事务
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
删除部门业务代码
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteById(Integer id) throws Exception {
//根据id删除部门数据
deptMapper.deleteById(id);
//根据部门id删除员工数据
empMapper.deleteByDeptId(id);
// TODO 记录操作日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
//调用其他业务类中的方法
deptLogService.insert(deptLog);
}
此时方法调用有两个 @Transactional注解
此时涉及事务传播行为
进行测试,发现数据库中并不存在日志信息,是什么原因?
两个方法都有 @Transactional注解,采用的是默认事务传播行为,需要事务,有则加入(b事务加入到a事务),无则创建新事务。
很显然insert事务会加入到deleteById事务,但是在deleteById业务执行时发生异常,进行回滚,那同属于一个事务的insert也会回滚,导致数据库中没有记录。
所以现在我们需要修改一下事务传播行为
propagation = Propagation.REQUIRES_NEW,表示无论deleteById中是否有事务,insert方法中都会新开启一个新的事物
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
当在deleteById事务中开启了insert事务,此时deleteById事务会被挂起,进行insert事务,当insert事务进行完成后继续运行deleteById事务
propagation = Propagation.REQUIRES_NEW,表示无论deleteById中是否有事务,insert方法中都会新开启一个新的事物
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
当在deleteById事务中开启了insert事务,此时deleteById事务会被挂起,进行insert事务,当insert事务进行完成后继续运行deleteById事务
Spring的事务分为:编程式事务控制 和 声明式事务控制
Spring提供了事务控制的类和方法,使用编码的方式对业务代码进行事务控制,事务控制代码和业务操作代码耦合到了一起,开发中不使用
Spring将事务控制的代码封装,对外提供了Xml和注解配置方式,通过配置的方式完成事务的控制,可以达到事务控制与业务操作代码解耦合,开发中推荐使用
Spring事务编程相关的类主要有如下三个
是一个接口标准,实现类都具备事务提交、回滚和获得事务对象的功能,不同持久层框架可能会有不同实现方案
封装事务的隔离级别、传播行为、过期时间等属性信息
存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等
搭建一个转账的环境,dao层一个转出钱的方法,一个转入钱的方法,service层一个转账业务方法,内部分别调用dao层转出钱和转入钱的方法,准备工作如下:
Maven坐标
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.19version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>6.0.6version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.4.6version>
dependency>、
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>3.0.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.23version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.3.7version>
dependency>
jdbc连接信息
jdbc.driver = com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/itcast
jdbc.username=root
jdbc.password=root
public interface AccountMapper {
// 加钱
@Update("update account set money=money+#{money} where id=#{id}")
public void incrMoney(@Param("id") String account,@Param("money") Integer money);
// 减钱
@Update("update account set money=money-#{money} where id=#{id}")
public void decrMoney(@Param("id") String account,@Param("money") Integer money);
}
public interface AccountService {
void transferMoney(String outAccount,String inAccount , Integer money);
}
实现类
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public void transferMoney(String outAccount, String inAccount, Integer money) {
accountMapper.decrMoney(outAccount,money);
accountMapper.incrMoney(inAccount,money);
}
}
xml配置文件
<context:component-scan base-package="com.zhangjingqi">context:component-scan>
<context:property-placeholder location="classpath:jdbc.properties">context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
bean>
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource">property>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.zhangjingqi.mapper">property>
bean>
public class AccountTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
accountService.transferMoney("1","2",500);
}
}
我们要把AccountServiceImpl类中的转账方法设置在一个事务之中
下面的代码是两个事务
@Override
public void transferMoney(String outAccount, String inAccount, Integer money) {
accountMapper.decrMoney(outAccount,money);
accountMapper.incrMoney(inAccount,money);
}
以使用AOP对Service的方法进行事务的增强
分析
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.2.13.RELEASEversion>
dependency>
其实这个坐标我们之前已经导入过了,这个坐标下面有事务相关的坐标
如果有所忘记的话,可以看一下这篇文章 SpringBoot——IOC与AOP
xml配置文件新配置信息
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.zhangjingqi.service.impl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut">aop:advisor>
aop:config>
故意在impl层出现异常
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public void transferMoney(String outAccount, String inAccount, Integer money) {
accountMapper.decrMoney(outAccount,money);
int c = 3/0;
accountMapper.incrMoney(inAccount,money);
}
}
进行测试
出现了异常,但是两个人的钱并没有扣,很完美 就是我们想要的结果
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
accountService.transferMoney("1","2",500);
我们再看一下xml中配置的平台事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
其中DataSourceTransactionManager是一个类,实现了很多接口,其中一个接口的父接口是PlatformTransactionManager接口(一个平台事务管理器),接口中有提交和回滚的方法
PlatformTransactionManager接口的实现类是选哪一个,取决于当前DAO层使用的框架是什么样的
比如如果使用的是最简单的JDBC、Mybatis,那实现类就是DataSourceTransactionManager
如果DAO层使用的是Hibernate,那对应的平台管理器就是HibernateTransactionManager
刚刚我们在配置类中是这么配置的
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
tx:attributes>
tx:advice>
底层用到的事务的操作都在transactionManager中封装这
主要看一下事务的属性
忘记的话可以看一下下面这个文章MySQL基础 — 多表查询以及事务管理
<tx:attributes>
<tx:method name="方法名称"
isolation="隔离级别,解决事务并发问题"
propagation="传播行为,事务嵌套问题"
read-only="只读状态"
timeout="超时时间,单位是秒,访问数据库有一个时间,不能一直在查询,一般设置为-1,表示没有超时时间,因为数据库自己有自己的超时时间"/>
tx:attributes>
下面这个操作是配置不同方法的不同属性,name代表方法名称,*代表通配符,下面这个配置的意思就是所有的方法都用当前这一套事务的配置,如果没配置事务的属性,就是默认值
<tx:method name="*"/>
也就是说可以配置好几套。比如下面,没有配置的参数就按照默认
<tx:method name="transferMoney">tx:method>
<tx:method name="registAccount">tx:method>
但是方法很多的时候一个一个添加又不太现实,所以我们的命名一定要规范,这样就能使用通配符进行匹配
比如下面是匹配add开头的方法
<tx:method name="add*">tx:method>
主要看一下下面这个配置的原理
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.zhangjingqi.service.impl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut">aop:advisor>
aop:config>
首先找到对应的处理器
找到对应的标签点进去
发现TxAdviceBeanDefinitionParser类中没有parse方法,但是有一个doparse方法,代表着调用他爹的
然后看一下AbstractSingleBeanDefinitionParser类,也没有parse方法,
那就再看一下AbstractSingleBeanDefinitionParser类的爹AbstractBeanDefinitionParser类中有没有,发现是有的
并且调用了一个parseInternal方法
点过去发现parseInternal方法是抽象的,说明具体的实现在他儿子那里
AbstractBeanDefinitionParser类的儿子是AbstractSingleBeanDefinitionParser类,里面确实有parseInternal方法的具体实现
这个地方感觉太难了,看不下去了,就到这里吧
标题一就是注解方式声明事务
就是用注解代替通知的配置、织入的配置,也就是代替下面两部分
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.zhangjingqi.service.impl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut">aop:advisor>
aop:config>
注解方式,依然可以使用xml中对应的参数
@Transactional可以使用在类上,也可以使用在方法上
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation =
Propagation.REQUIRED,readOnly = false,timeout = 5)
仍然需要xml配置,开启事务开关
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
下图中我们已经选出合适的切点了
为什么还要在事务管理这个地方再配置一下对应哪个方法?
因为我们在切点中筛选出来的方法很多,所实现的业务也不一样
那如果不在事务管理再分或者说筛选的话,那所有的方法事务配置的属性都一个样子了,这可能不会符合现实
切点表达式,是过滤哪些方法可以进行事务增强
事务属性信息的name,是指定哪个方法要进行哪些事务属性的配置