spring基础知识-事务

1 事务

  1. 事务是数据库操作最基本单位,要么都成功,要么都失败。

  2. 事务四个特性ACID:原子性,一致性,隔离性,持久性。

  3. Spring事务管理有两种方式:编程式事务管理 和 声明式事务管理,一般使用声明式事务管理,底层使用AOP原理。

  4. 声明式事务管理有两种方式:基于xml配置方式 和 基于注解方式,一般使用注解方式。

  5. Spring事务管理提供了一个接口,叫做事务管理器,这个接口针对不同的框架提供不同的实现类。

  6. 在service类上面或者service类的方法上面添加事务注解@Transactional

  7. 如果把@Transactional添加在类上面,这个类里面所有方法都添加事务。

  8. 如果只是添加在方法上面,则只为这个方法添加事务。

    1
    2
    3
    4
    @Service
    @Transactional
    public class UserService {
    }

声明式事务

  1. propagation:事务传播行为,总共有7种。

  2. isolation:事务隔离级别

    有三个读问题:脏读,不可重复读,虚读(幻读)。

    设置隔离级别,解决读问题:

    image-20240414125344168

  3. timeout:超时时间

    • 事务需要在一定时间内进行提交,超过时间后回滚。
    • 默认值是-1,设置时间以秒为单位。
  4. readOnly:是否只读

    • 默认值为false,表示可以查询,也可以增删改。
    • 设置为true,只能查询。
  5. rollbackFor:回滚,设置出现哪些异常进行事务回滚。

  6. noRollbackFor:不回滚,设置出现哪些异常不进行事务回滚。

1
2
3
@Service
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
public class AccountService {

完全注解实现声明式事务管理

配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration  //配置类
@ComponentScan(basePackages = "com.oymn.spring5") //开启组件扫描
@EnableTransactionManagement //开启事务
public class Config {

//创建数据库连接池
@Bean
public DruidDataSource getDruidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/book");
druidDataSource.setUsername("root");
druidDataSource.setPassword("000000");
return druidDataSource;
}
//创建JdbcTemplate对象
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//创建事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}

service类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class AccountService {

@Autowired
private AccountDao accountDao;

@Transactional
public void accountMoney(){
accountDao.add();
//int i=1/0; //用来模拟转账失败
accountDao.reduce();
}
}

事务传播行为

在spring中一共有7中传播行为,REQUIRED、NESTED、REQUIRES_NEW常用

1
2
3
4
5
6
7
8
9
10
11
12
13
REQUIRED:如果当前没有事务,就新建一个事务。如果当前存在事务,则加入这个事务。 

NESTED:如果当前没有事务,就新建一个事务。如果当前事务存在,则执行一个嵌套事务。

REQUIRES_NEW:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。

SUPPORTS:如果当前没有事务,就以非事务方式执行。 如果当前有事务,则使用事务。

NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

MANDATORY:以事务方式执行,如果当前没有事务,就抛出异常。

NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

场景示例

有个注册业务,注册时需要记录登录账号、密码、身份证号、姓名、手机号这5个信息,涉及三张表,分别是t_user_account、t_user_idcard、t_user_phone,刚开始的业务要求这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚(这些异常是我用作数据校验时抛出的,正常生产上会先进行数据校验,校验成功了再更改数据库,我这里为了演示@Transactional的回滚效果,先更改数据,如果校验不通过,便会通过spring的事务进行回滚)。

1
2
3
4
5
6
7
8
9
@Data
public class UserVO {
private Long id;
private String userAccount;
private String userPwd;
private String phoneNum;
private String userName;
private String IDCard;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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();
}
}
}
1
2
3
4
5
6
public interface UserService {
//插入新用户的id
void insertId(UserVO userVO);
//插入用户账号、密码、手机号、姓名、身份证号
void register(UserVO userVO);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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);
}
}

注:这里选择抛出的是RuntimeException,该异常是运行时异常,如果不做异常回滚的配置,默认非运行时异常(比如我刚开始抛出的是Exception),则不会发生回滚。

1
2
3
4
5
6
7
8
public interface UserDao {
//插入账户和密码
void insertAccountAndPwd(UserVO userVO);

//插入身份证号和姓名
void insertIDcardAndName(UserVO userVO);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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("姓名不能为空");
}
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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);
}
}

在该示例中涉及事务传播行为要点的是UserServiceImpl的register方法。

register方法:是外围方法,除了调用了insertAccountAndPwd方法和insertIDcardAndName方法外,直接调用jdbcTemplate进行手机号的插入。

insertAccountAndPwd方法:是内围方法,进行账号密码的插入。

insertIDcardAndName方法:是内围方法,进行身份证号、姓名的插入。

场景1

我们要实现登录账号、密码、手机号、身份证号、姓名,这些都不能为空,如果其中一个发生异常,那么其余已经插入的全部回滚。

根据这个业务及我们当前代码结构,我们需要在这一个外围方法和两个内围方法中都添加@Transactional注解,因为要求有异常,只要有数据库改动的代码全部回滚。

传播行为选择

@Transactional的事务传播属性propagation值设为Propagation.REQUIRED

这个值是默认的,它的含义是:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。

这正好符合我们业务需求:外围方法已经是一个事务后,被调用的两个内围方法也加入到这个事务中,就实现了只要有任意一个异常,只会插入一个id,其余需要插入的登录账号、密码、手机号、身份证号、姓名都回滚。

测试:

img

img

img

img

可以看见:只要一个方法内有异常,这三个方法都会回滚,数据库里没有一个表中有新记录。

场景2

业务逻辑改为注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。

那么这里我们仍保持内围方法insertIDcardAndName方法为事务,外围方法register和内围方法insertAccountAndPwd也为事务,但是要保证insertIDcardAndName方法出现异常时不能导致外围方法回滚。

导致外围回滚有两种途径,一个是内围方法出现异常后会抛出给外围方法,然后外围方法的@Transactional感知到异常回滚,另一个是外围方法与内围方法本事是一个事务,那么内围方法回滚的时候自然会使外围方法也会滚。

传播行为选择

首先要在外围方法中将内围方法insertIDcardAndName的异常捕获,不能再使用REQUIRED为事务传播属性了,因为内围方法事务传播属性为REQUIRED代表加入到外围方法的事务中,会同时回滚。将@Transactional的事务传播属性propagation值设为Propagation.NESTED,它的含义是:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,就新建一个事务。这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法有异常只需要回滚自己就行,外围方法有异常才回滚全部。

img

img

测试:

img

img

img

img

上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当insertIDcardAndName方法有异常,并不会导致外围方法register和内围方法insertAccountAndPwd回滚。

场景3

注册时可以不提交身份证号与姓名(但是身份证号和姓名必须同时添加或不添加),也可以不提交手机号,后续确定后再提交即可,账号密码和手机号仍然不可缺少,少一个则全部回滚。

传播行为选择

上次业务修改我们只要保证内围方法insertIDcardAndName回滚不带上外围方法回滚,这次我们还要保证外围方法register回滚时不要带上内围方法insertAccountAndPwd回滚。

所以这次我们可以将@Transactional的事务传播属性propagation值设为Propagation.REQUIRES_NEW,它的含义是:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。

这正好符合我们业务需求:外围方法是一个事务的情况下,我们的内围方法开始它自己的业务,外围方法回滚不带上内围方法。

测试

img

img

img

img

上边我们通过引起这三个方法(一个外围、两个内围)内的异常,发现当外围方法register方法有异常,并不会导致内围方法insertAccountAndPwd回滚。

总结

当我们不想让内围回滚带上外围也回滚,内围方法事务就不要使用REQUIRED,可以使用NESTED或REQUIRES_NEW。

当我们不想让外围回滚带上内围也回滚,内围方法事务就不要使用REQUIRED和NESTED,可以使用REQUIRES_NEW。


spring基础知识-事务
https://baijianglai.cn/spring基础知识-事务/2ff047cb81fb/
作者
Lai Baijiang
发布于
2024年4月13日
许可协议