【Spring经典面试题】Spring事务的传播机制

1. 什么是事务的传播机制

事务传播机制: 多个事务方法存在调⽤关系时, 事务是如何在这些方法间进行传播的。
比如有两个方法A, B都被 @Transactional 修饰, A方法调用B方法,A方法运⾏时, 会开启⼀个事务。当A调⽤B时, B方法本⾝也有事务, 此时B方法运⾏时, 是加⼊A的事务, 还是创建⼀个新的事务呢?
这个就涉及到了事务的传播机制。
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题:
image.png
⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题:
image.png

2. 事务的传播机制有哪些?

@Transactional 注解⽀持事务传播机制的设置, 通过 propagation 属性来指定传播行为

@Transactional(propagation = Propagation.MANDATORY)
public Integer insertUser(String userName, String password) {
    // 代码略
    return 1;
}

Spring 事务传播机制有以下 7 种:

  1. Propagation.REQUIRED: 默认的事务传播级别。如果当前存在事务, 则加⼊该事务。如果当前没有事务, 则创建⼀个新的事务。
  2. Propagation.SUPPORTS: 如果当前存在事务,则加⼊该事务。如果当前没有事务, 则以非事务的⽅式继续运行。
  3. Propagation.MANDATORY:强制性。如果当前存在事务,则加⼊该事务。如果当前没有事务,则抛出异常。
  4. Propagation.REQUIRES_NEW: 创建⼀个新的事务。如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开启⾃⼰的事务,且开启的事务相互独立, 互不⼲扰。
  5. Propagation.NOT_SUPPORTED:以非事务⽅式运⾏,如果当前存在事务, 则把当前事务挂起(不用)。
  6. Propagation.NEVER:以非事务⽅式运⾏,如果当前存在事务, 则抛出异常。
  7. Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

比如⼀对新人要结婚了, 关于是否需要房⼦:

  1. Propagation.REQUIRED : 需要有房⼦。如果你有房, 我们就⼀起住, 如果你没房, 我们就⼀起买房。(如果A当前存在事务, B就用A的事务. 如果A没有事务, B就创建⼀个新的事务)
  2. Propagation.SUPPORTS : 可以有房⼦。如果你有房, 那就⼀起住。如果没房, 那就租房。(如果A存在事务, B就用A的事。 如果A没有事务, B就以非事务的⽅式继续运⾏)
  3. Propagation.MANDATORY : 必须有房⼦。要求必须有房, 如果没房就不结婚。(如果A存在事务, B就加⼊该事务。如果A没有事务, B就抛出异常)
  4. Propagation.REQUIRES_NEW : 必须买新房。不管你有没有房, 必须要两个⼈⼀起买房。即使有房也不住. (如果A有事务,就把事务挂起,创建⼀个新的事务。如果A没有事务, 也创建事务)
  5. Propagation.NOT_SUPPORTED : 不需要房子。不管你有没有房, 我都不住, 必须租房.(不论A是否存在事务,B都以非事务方式运行)
  6. Propagation.NEVER : 不能有房⼦。(以非事务方式运行。A不能有事务,如果有事务, 就抛出异常)
  7. Propagation.NESTED : 如果你没房, 就⼀起买房。如果你有房, 我们就以房⼦为根据地,做点小⽣意。(如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏。如果当前没有事务, 则该取值等价于PROPAGATION_REQUIRED )

3. Spring事务的传播机制的使用和场景演示

准备阶段:创建两张表user_info和log_info。

@RestController
@RequestMapping("/proga")
public class ProgaController {
    @Autowired
    private LogService logService;
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/p1")
    public String p1(String userName, String password) {
        userService.insertUser(userName, password);
        LogInfo logInfo = new LogInfo();
        logInfo.setUserName(userName);
        logInfo.setOp("用户主动注册");
        logService.insertLog(logInfo);
        return "用户注册成功";
    }
}

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional
    public Integer insertUser(String userName, String password) {
        return userInfoMapper.insertUser(userName, password);
    }
}

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional
    public Integer insertLog(LogInfo logInfo) {
        return logInfoMapper.insertLog(logInfo);
    }
}

@Mapper
public interface UserInfoMapper {
    @Insert("insert into user_info(user_name, `password`) values(#{userName},#{password})")
    Integer insertUser(String userName, String password);
}
@Mapper
public interface LogInfoMapper {
    @Insert("insert into log_info(user_name, op) values(#{userName}, #{op})")
    Integer insertLog(LogInfo logInfo);
}

数据库初始数据:
image.png
image.png
我们现在要实现一个功能,用户注册功能,做如下操作:

  1. 用户注册,用户表插⼊⼀条数据
  2. 记录操作日志, 日志表插⼊⼀条数据(出现异常)

3.1. REQUIRED(加入事务)

启动程序,观察 propagation = Propagation.REQUIRED(可以不写,默认的事务传播级别) 的执⾏结果:
访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务提交成功:
image.png
查看数据库user_info,数据插入成功:
image.png
查看数据库log_info,数据插入成功:
image.png
如果其中一个事务有异常:
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务没有提交,事务全部进行了回滚
image.png
查看数据库user_info,没有新的数据插入:
image.png
查看数据库log_info,没有新的数据插入:
image.png
流程描述:

  1. p1 ⽅法开始事务
  2. ⽤户注册, 插⼊⼀条数据 (执行成功) (和p1 使⽤同⼀个事务)
  3. 记录操作⽇志, 插⼊⼀条数据(出现异常, 执行失败) (和p1 使⽤同⼀个事务)
  4. 因为步骤3出现异常, 事务回滚. 步骤2和3使⽤同⼀个事务, 所以步骤2的数据也回滚了

总结:
两个事务全部成功,事务提交。
一个及以上失败,事务全部回滚。

3.2. REQUIRES_NEW(新建事务)

将上述UserService 和LogService 中相关⽅法事务传播机制改为Propagation.REQUIRES_NEW
image.png
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务全部提交成功
image.png
查看数据库user_info,有新的数据插入:
image.png
查看数据库log_info,有新的数据插入:
image.png
如果其中一个事务有异常:
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,发现⽤户数据插⼊成功了,事务提交,日志表数据插入失败,事务回滚
image.png
查看数据库user_info,有新的数据插入:
image.png
查看数据库log_info,没有数据插入:
image.png
总结:
两个事务全部成功,事务提交。
其中一个失败,两个事务之间不互相影响。

3.3. NEVER (不支持当前事务, 抛异常)

修改UserService 中对应⽅法的事务传播机制为 Propagation.NEVER:
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,程序执⾏报错, 没有数据插⼊:
image.png

3.4. NESTED(嵌套事务)

将上述UserService 和LogService 中相关⽅法事务传播机制改为 Propagation.NESTED:
image.png
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务全部提交
image.png
查看数据库user_info,有新的数据插入:
image.png
查看数据库log_info,有新的数据插入:
image.png
如果其中一个事务有异常:
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务全部回滚
image.png
查看数据库user_info,没有新的数据插入:
image.png
查看数据库log_info,没有新的数据插入:
image.png
流程描述:

  1. Controller 中p1 ⽅法开始事务
  2. UserService ⽤户注册, 插⼊⼀条数据 (嵌套p1事务)
  3. LogService 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (嵌套p1事务, 回滚当前事务, 数据添加失败)
  4. 由于是嵌套事务, LogService 出现异常之后, 往上找调⽤它的⽅法和事务, 所以⽤户注册也失败了
  5. 最终结果是两个数据都没有添加

p1事务可以认为是⽗事务, 嵌套事务是⼦事务. ⽗事务出现异常, ⼦事务也会回滚, ⼦事务出现异常, 如果不进⾏处理, 也会导致⽗事务回滚。

总结:
两个事务全部成功,事务提交。
其中一个失败,两个事务都回滚。

3.5. NESTED和REQUIRED 有什么区别?

  1. 对于NESTED来说,修改LogService,对异常进行捕获

image.png
启动程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务全部提交
image.png
查看数据库user_info,数据插入成功:
image.png
查看数据库log_info,数据插入成功:
image.png
我们继续修改,对LogService手动添加事务回滚:
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,只回滚当前事务
image.png
查看数据库user_info,数据插入成功:
image.png
查看数据库log_info,没有新数据添加:
image.png
总结:LogService 中的事务已经回滚, 但是嵌套事务不会回滚嵌套之前的事务, 也就是说嵌套事务可以实现部分事务回滚

  1. 同样,对于REQUIRED来说,修改LogService,对异常进行捕获

image.png
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务全部提交:
image.png
查看数据库user_info,有新的数据插入:
image.png
查看数据库log_info,有新的数据插入:
image.png
我们继续修改,对LogService手动添加事务回滚:
image.png
运行程序,访问http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,响应结果:
image.png
观察后端日志,事务全部回滚
image.png
查看数据库user_info,没有新数据插入:
image.png
查看数据库log_info,没有新数据插入:
image.png
**总结:**REQUIRED 如果回滚就是回滚所有事务, 不能实现部分事务的回滚。 (因为属于同⼀个事务)。
NESTED和REQUIRED区别:

  • 整个事务如果全部执⾏成功, 二者的结果是⼀样的。
  • 如果事务⼀部分执⾏成功, REQUIRED加⼊事务会导致整个事务全部回滚。NESTED嵌套事务可以实现局部回滚, 不会影响上⼀个⽅法中执⾏的结果。

嵌套事务之所以能够实现部分事务的回滚, 是因为事务中有⼀个保存点(savepoint)的概念, 嵌套事务进⼊之后相当于新建了⼀个保存点, ⽽回滚时只回滚到当前保存点。
REQUIRED 是加⼊到当前事务中, 并没有创建事务的保存点, 因此出现了回滚就是整个事务回滚, 这就是嵌套事务和加⼊事务的区别。

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值