1.事务传播行为列举
事务传播行为有7种,不是与数据库交互,数据库定义的,而是spring框架自带的,是spring通过aop实现的,下边是7种事务传播行为:
REQUIRED:如果当前没有事务,就新建一个事务。如果当前存在事务,则加入这个事务。
NESTED:如果当前没有事务,就新建一个事务。如果当前事务存在,则执行一个嵌套事务。
REQUIRES_NEW:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。
SUPPORTS:如果当前没有事务,就以非事务方式执行。 如果当前有事务,则使用事务。
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
MANDATORY:以事务方式执行,如果当前没有事务,就抛出异常。
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
我们最常用的是前三个REQUIRED、NESTED、REQUIRES_NEW,下边我们用一系列业务场景来详解这三个事务传播行为。
2.事务传播行为场景示例
2.1业务场景1
首先有这样一个场景,我们有个注册业务,注册时需要记录登录账号、密码、身份证号、姓名、手机号这5个信息,涉及三张表,分别是t_user_account、t_user_idcard、t_user_phone,刚开始的业务要求这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚(这些异常是我用作数据校验时抛出的,正常生产上会先进行数据校验,校验成功了再更改数据库,我这里为了演示@Transactional的回滚效果,先更改数据,如果校验不通过,便会通过spring的事务进行回滚)。
2.2业务代码实现
spring bean配置文件
我们在bean配置文件中开启事务
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--3 1.组件扫描 2.引入外部属性文件 3.配置数据源 4.创建jdbcTemplate对象,注入数据源-->
<!--组件扫描-->
<context:component-scan base-package="com.atguigu.spring6.txSpread"></context:component-scan>
<!--引入外部属性文件,创建数据源对象-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--配置数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="username" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--创建jdbcTemplate对象,注入数据源-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--
开启事务的注解驱动
通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
-->
<!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
VO
public class UserVO {
private Long id;
private String userAccount;
private String userPwd;
private String phoneNum;
private String userName;
private String IDCard;
public UserVO(Long id, String userAccount, String userPwd, String phoneNum, String userName, String IDCard) {
this.id = id;
this.userAccount = userAccount;
this.userPwd = userPwd;
this.phoneNum = phoneNum;
this.userName = userName;
this.IDCard = IDCard;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserAccount() {
return userAccount;
}
public void setUserAccount(String userAccount) {
this.userAccount = userAccount;
}
public String getUserPwd() {
return userPwd;
}
public void setUserPwd(String userPwd) {
this.userPwd = userPwd;
}
public String getPhoneNum() {
return phoneNum;
}
public void setPhoneNum(String phoneNum) {
this.phoneNum = phoneNum;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getIDCard() {
return IDCard;
}
public void setIDCard(String IDCard) {
this.IDCard = IDCard;
}
}
controller
register()方法用于实现这个业务,分别调用userService的insertId()方法用于插入一条新记录的主键和register()方法用于插入登录账号、密码、手机号、身份证号、姓名。
@Controller
public class UserController {
@Autowired
private UserService userService;
/**
* 注册用户
* @param userVO
*/
public void register(UserVO userVO) {
userService.insertId(userVO);
try {
userService.register(userVO);
} catch (Exception e) {
e.printStackTrace();
}
}
}
service
public interface UserService {
//插入新用户的id
void insertId(UserVO userVO);
//插入用户账号、密码、手机号、姓名、身份证号
void register(UserVO userVO);
}
serviceImpl
这里主要说register方法,在register方法中,我们调用userDao中的insertAccountAndPwd方法和insertIDcardAndName方法分别进行账号密码的插入与身份证号、姓名的插入,在register方法中直接调用jdbcTemplate进行手机号的插入。
这里有一点需要说明,我这里选择抛出的是RuntimeException,该异常是运行时异常,如果不做异常回滚的配置,默认非运行时异常(比如我刚开始抛出的是Exception),则不会发生回滚。
@Service
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private UserDao userDao;
@Override
public void insertId(UserVO userVO) {
String sql1 = "insert into t_user_account (user_id) values (?)";
jdbcTemplate.update(sql1,userVO.getId());
String sql2 = "insert into t_user_idcard (user_id) values (?)";
jdbcTemplate.update(sql2,userVO.getId());
String sql3 = "insert into t_user_phone (user_id) values (?)";
jdbcTemplate.update(sql3,userVO.getId());
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void register(UserVO userVO) {
//插入账号密码
userDao.insertAccountAndPwd(userVO);
//再插入手机号
String sql1 = "update t_user_phone set phone_num = ? where user_id = ?";
jdbcTemplate.update(sql1,userVO.getPhoneNum(),userVO.getId());
if (userVO.getPhoneNum() == null) {
throw new RuntimeException("手机号码不能为空");
}
//插入身份证号和姓名
userDao.insertIDcardAndName(userVO);
}
}
dao
public interface UserDao {
//插入账户和密码
void insertAccountAndPwd(UserVO userVO);
//插入身份证号和姓名
void insertIDcardAndName(UserVO userVO);
}
daoImpl
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void insertAccountAndPwd(UserVO userVO) {
String sql1 = "update t_user_account set user_account = ?, user_pwd = ? where user_id = ?";
jdbcTemplate.update(sql1,userVO.getUserAccount(),userVO.getUserPwd(),userVO.getId());
if (userVO.getUserAccount() == null) {
throw new RuntimeException("账号不能为空");
}
if (userVO.getUserPwd() == null) {
throw new RuntimeException("密码不能为空");
}
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void insertIDcardAndName(UserVO userVO) {
//先将新用户的身份证号和姓名插入
String sql1 = "update t_user_idcard set ID_card = ?, user_name = ? where user_id = ?";
jdbcTemplate.update(sql1,userVO.getIDCard(),userVO.getUserName(),userVO.getId());
if (userVO.getIDCard() == null) {
throw new RuntimeException("身份证号不能为空");
}
if (userVO.getUserName() == null) {
throw new RuntimeException("姓名不能为空");
}
}
}
测试方法
@SpringJUnitConfig(locations = "classpath:txSpreadBean.xml")
public class TestTxSpread {
@Autowired
private UserController userController;
@Test
public void test2() {
UserVO userVO1 = new UserVO(1L,"花花","123456",null,"花木兰","511387197807180001");
userController.register(userVO1);
UserVO userVO2 = new UserVO(2L,"花花","123456","13612312345","花木兰",null);
userController.register(userVO2);
}
}
2.3内围方法与外围方法定义
这里我先来给大家规整一下代码,涉及事务传播行为要点的是UserServiceImpl的register方法,这里为了后续方便指认,我就称UserServiceImpl的register方法为外围方法,而它先调用了UserDaoImpl的insertAccountAndPwd方法进行账号和密码的插入,然后在register方法中直接调用jdbcTemplate进行手机号的插入,最后再调用userDao中的insertIDcardAndName方法进行身份证号、姓名的插入,这里我就称UserDaoImpl的insertAccountAndPwd方法和insertIDcardAndName方法为两个内围方法。
register方法:是外围方法,除了调用了insertAccountAndPwd方法和insertIDcardAndName方法外,直接调用jdbcTemplate进行手机号的插入。
insertAccountAndPwd方法:是内围方法,进行账号密码的插入。
insertIDcardAndName方法:是内围方法,进行身份证号、姓名的插入。
2.4业务场景1实现及测试
当前业务需求:
我们要实现登录账号、密码、手机号、身份证号、姓名,这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚。
根据这个业务及我们当前代码结构,我们需要在这一个外围方法和两个内围方法中都添加@Transactional注解,因为要求有异常,只要有数据库改动的代码全部回滚。
事务传播行为选择:
这里我们将@Transactional的事务传播属性propagation值设为Propagation.REQUIRED,这个值我们说过了是默认的,同时它的含义是:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这正好符合我们业务需求:外围方法已经是一个事务后,被调用的两个内围方法也加入到这个事务中,就实现了只要有任意一个异常,只会插入一个id,其余需要插入的登录账号、密码、手机号、身份证号、姓名都回滚。
测试:
上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现只要一个方法内有异常,这三个方法都会回滚,数据库里没有一个表中有新记录。
2.5业务场景2实现及测试
当前业务需求:
接下来,我们业务逻辑改为注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。
那么这里我们仍保持内围方法insertIDcardAndName方法为事务,外围方法register和内围方法insertAccountAndPwd也为事务,但是要保证insertIDcardAndName方法出现异常时不能导致外围方法回滚,导致外围回滚有两种途径,一个是内围方法出现异常后会抛出给外围方法,然后外围方法的@Transactional感知到异常回滚,另一个是外围方法与内围方法本事是一个事务,那么内围方法回滚的时候自然会使外围方法也会滚。
事务传播行为选择:
所以这里我们首先要在外围方法中将内围方法insertIDcardAndName的异常捕获,接着不能再使用REQUIRED为事务传播属性了,因为内围方法事务传播属性为REQUIRED代表加入到外围方法的事务中,会同时回滚,可以将@Transactional的事务传播属性propagation值设为Propagation.NESTED,它的含义是:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,就新建一个事务。这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法有异常只需要回滚自己就行,外围方法有异常才回滚全部。
测试:
上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当insertIDcardAndName方法有异常,并不会导致外围方法register和内围方法insertAccountAndPwd回滚。
2.6业务场景3实现及测试
当前业务需求:
接下来,我们业务逻辑改为注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),也可以不提交手机号,后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。
事务传播行为选择:
上次业务修改我们只要保证内围方法insertIDcardAndName回滚不带上外围方法回滚,这次我们还要保证外围方法register回滚时不要带上内围方法insertAccountAndPwd回滚,所以这次我们可以将@Transactional的事务传播属性propagation值设为Propagation.REQUIRES_NEW,它的含义是:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法开始它自己的业务,外围方法回滚不带上内围方法。
测试:
上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当外围方法register方法有异常,并不会导致内围方法insertAccountAndPwd回滚。
3总结
当我们不想让内围回滚带上外围也回滚,内围方法事务就不要使用REQUIRED,可以使用NESTED或REQUIRES_NEW。
当我们不想让外围回滚带上内围也回滚,内围方法事务就不要使用REQUIRED和NESTED,可以使用REQUIRES_NEW。