通过实际案例摸清楚Spring事务传播的行为

2020-08-16 16:03:56 浏览数 (1)

事务传播

  • 对于Spring事务传播的七大行为,我们往往还停留在一些概念上,比如下面这张表:

定义

说明

PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY

表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。

PROPAGATION_REQUIRED_NEW

表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。

PROPAGATION_NOT_SUPPORTED

表示该方法不应该运行在事务中。如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER

表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

  • 本文旨在通过实际案例代码进行分析Spring事务传播行为的各种特性。
案例准备
  • 构建一个SpringBoot项目,增加以下代码:
  1. 实体类
代码语言:javascript复制
/**
*  User.java : 用户类
*/
@Entity
public class User implements Serializable {
    // 用户id
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // 用户名
    @NotBlank(message = "用户名称不能为空")
    @Column(name="name")
    private String name;
    // 邮箱
    @Column(name="email")
    @Pattern(message ="邮箱格式不符", regexp = "^[A-Za-z0-9\u4e00-\u9fa5] @[a-zA-Z0-9_-] (\.[a-zA-Z0-9_-] ) $")
    private String email;
    
    public User(){}

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{"  
                "id="   id  
                ", name='"   name   '''  
                ", email='"   email   '''  
                ", createTime="   createTime  
                ", updateTime="   updateTime  
                '}';
    }
}
  1. DAO接口与实现类
代码语言:javascript复制
/**
 * 用户数据访问层(DAO)接口
 */
public interface UserDAO {
    // 查找所有用户
    List<User> findAll();

    // 根据id查找用户
    User findById(Long id) throws SQLException;

    // 新增用户
    Long addUser(User user) throws SQLException;

    // 更新用户
    void updateUser(User user);

    // 删除用户
    void deleteById(Long id);

    // 自定义添加通过用户名称查找用户信息
    List<User> findByName(String name);
}

/**
 * 使用JdbcTemplate模板类实现用户数据访问层
 *
 */
@Repository
public class UserDAOImpl implements UserDAO {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public List<User> findAll() {
        return jdbcTemplate.query("select id,name,email from user;",
                new Object[]{}, new BeanPropertyRowMapper<>(User.class));
    }

    @Override
    public User findById(Long id) {
        return jdbcTemplate.queryForObject("select id,name,email from user where id=?;",
                new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
    }

    @Override
    public Long addUser(User user) {
        return Integer.toUnsignedLong(
                jdbcTemplate.update("insert into  user(id,name,email) values(?,?,?);"
                        , user.getId(), user.getName(), user.getEmail()));
    }

    @Override
    public void updateUser(User user) {
        jdbcTemplate.update("update user set name=?,email=? where id =?;"
                , user.getName(), user.getEmail(), user.getId());
    }

    @Override
    public void deleteById(Long id) {
        jdbcTemplate.update("delete from user where id=?", new Object[]{id});
    }

    @Override
    public List<User> findByName(String name) {
        return jdbcTemplate.query("select id,name,email from user where name=?;",
                new Object[]{name}, new BeanPropertyRowMapper<>(User.class));
    }
}
  1. 测试类
代码语言:javascript复制
/**
 * 事务传播测试案例
 */
public class TransactionalTest {

    @Autowired
    private UserDAO userDAO;

    // 无事务
    public void noneTransaction() throws SQLException {

        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 增加一个与user1主键相同的用户
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
        
    }
	//....

}
案例解析
1、无事务
  • 插入两个id(主键)相同的用户数据。
代码语言:javascript复制
// 无事务
    public void noneTransaction() throws SQLException {

        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 增加一个与user1主键相同的用户
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
        
    }
  • 插入第一条数据成功,第二条数据失败
  • 由于没有事务控制,数据库表中会存在一条数据:
2、 Propagation.REQUIRED
  • 这个是默认的事务传播行为:如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中。
  • 仍然插入两个id(主键)相同的用户数据。
代码语言:javascript复制
    // 事务传播为PROPAGATION_REQUIRED
    @Transactional(propagation = Propagation.REQUIRED)
    public void requiredTransaction() throws SQLException {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 增加一个与user1主键相同的用户
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
    }
  • 第二条数据插入时报重复主键错误
  • 由于启用了事务,提示事务回滚,表中没有插入任何数据
3. Propagation.SUPPORTS
  • 支持当前事务,如果当前没有事务,就以非事务方式执行。这里我们做两个测试,首先以原来的代码,即调用外层没有启用事务来运行:
代码语言:javascript复制
    // 事务传播为PROPAGATION_SUPPORTS
    // 调用的外层没有事务
    @Transactional(propagation = Propagation.SUPPORTS)
    public void supportsTransaction() throws SQLException {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 增加一个与user1主键相同的用户
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
    }
  • 第一条插入成功,插入第二条事务时报主键重复错误,由于调用方外层启用事务,表中存留第一条数据。
  • 接下来修改代码,用一个已启事务的调用方来调用该测试过程:
代码语言:javascript复制
    // 事务传播为PROPAGATION_SUPPORTS
    // 调用方已启用事务
    @Transactional
    public void callSupportsTransaction() throws SQLException {
        supportsTransaction();
    }
    
	@Transactional(propagation = Propagation.SUPPORTS)
    public void supportsTransaction() throws SQLException {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 增加一个与user1主键相同的用户
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
    }
  • 第一条插入成功,插入第二条事务时报主键重复错误,但由于这次调用方已启用了事务,表中没有插入任何数据。
4. Propagation.MANDATORY
  • 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。
  • 我们首先直接运行以下代码
代码语言:javascript复制
    // 事务传播为PROPAGATION_MANDATORY
    @Transactional(propagation = Propagation.MANDATORY)
    public void mandatoryTransaction() throws SQLException {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
    }
  • 由于调用外层没有启用事务,该段测试代码判断当前事务不存在,则会抛出不存在事务的错误
  • 接下来使用调用方的外层启用事务,再调用这段测试代码:
代码语言:javascript复制
// 事务传播为PROPAGATION_MANDATORY
    // 调用方启用事务
    @Transactional
    public void callMandatoryTransaction() throws SQLException {
        User user = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user);
        mandatoryTransaction();
    }

	 @Transactional(propagation = Propagation.MANDATORY)
    public void mandatoryTransaction() throws SQLException {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
    }
  • 测试程序在插入第二条数据时报主键错误
  • 由于调用方启用事务,事务回滚,没有插入任何数据。
5. Propagation.REQUIRED_NEW
  • 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。
  • 针对这种特性,我们做一个有趣的实验:调用方启用默认事务,并调用事务传播为PROPAGATION_REQUIRES_NEW的程序,并故意造成事务回滚。
代码语言:javascript复制
// 调用方启用默认事务,并调用事务传播为PROPAGATION_REQUIRES_NEW的程序,在外层故意造成事务回滚
    @Transactional
    public void callRequiresNewTransaction() throws SQLException {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        requiresNewTransaction();
        // 增加一个主键重复的用户,故意造成事务回滚
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
    }

    // 事务传播为PROPAGATION_REQUIRES_NEW
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void requiresNewTransaction() throws SQLException {
        User user = new User(101L, "Jack", "Jack@163.com");
        userDAO.addUser(user);
    }
  • 测试情况如下:在外层事务造成回滚后,表中没有插入任何数据。
  • 接下来再改下程序,调用方启用默认事务,并调用事务传播为PROPAGATION_REQUIRES_NEW的程序,但在调用的程序内层故意造成事务回滚。
代码语言:javascript复制
  // 调用方启用默认事务,并调用事务传播为PROPAGATION_REQUIRES_NEW的程序
    @Transactional
    public void callRequiresNewTransaction() throws SQLException {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 调用事务传播为PROPAGATION_REQUIRES_NEW的过程
        requiresNewTransaction();
       
        User user2 = new User(101L, "Rose", "Rose@163.com");
        userDAO.addUser(user2);
    }

    // 事务传播为PROPAGATION_REQUIRES_NEW
    // 内层错误造成事务回滚
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requiresNewTransaction(){
        // 增加一个主键重复的用户,故意造成事务回滚
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
    }
  • 同样会造成事务回滚,表中无任何数据插入
6. Propagation.NOT_SUPPORTED
  • 该方法不应该运行在事务中。如果当前存在事务,就把当前事务挂起。
  • 为了测试该特性,我们首先定义另外一个测试服务类,该服务类中定义了事务传播为Propagation.NOT_SUPPORTED的方法
代码语言:javascript复制
/**
 * 测试 Propagation.NOT_SUPPORTED
 */
@Service
public class UserServiceTest {
    @Autowired
    private UserDAOImpl userDAO;
    // 事务传播为Propagation.NOT_SUPPORTED
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void notSupportedTransaction(){
        User user2 = new User(101L, "Rose", "Rose@163.com");
        userDAO.addUser(user2);
    }

}
  • 在主测试类启用默认事务,并调用新增服务类中的事务传播为Propagation.NOT_SUPPORTED的方法,并且故意增加重复用户数据,造成主服务的事务回滚:
代码语言:javascript复制
 // 主测试类启用默认事务,并调用Propagation.NOT_SUPPORTED的方法
    @Transactional
    public void callNotSupportedTransaction() {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 调用事务传播为Propagation.NOT_SUPPORTED的过程
        userServiceTest.notSupportedTransaction();
        // 增加重复用户数据
        User user2 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user2);
    }
  • 由于主服务类中启用了事务,在插入第二条重复用户数据时,会报主键冲突,造成事务回滚,两条数据都没有插入;但新增的服务类的方法没有运行在事务中,新增的用户数据会插入表中。
7. Propagation.NEVER
  • 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。
  • 按测试Propagation.NOT_SUPPORTED进行改造,主服务类启用默认事务特性,并调用测试服务类Propagation.NEVER的过程
代码语言:javascript复制
// 调用方启用默认事务,并调用Propagation.NEVER的过程
    // 调用方启用默认事务,并调用Propagation.NEVER的过程
    @Transactional
    public void callNeverTransaction  {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 调用事务传播为Propagation.NEVER的过程
        userServiceTest.neverTransaction();
    }
代码语言:javascript复制
// 事务传播为Propagation.NEVER的过程
    @Transactional(propagation = Propagation.NEVER)
    public void neverTransaction() {
        User user2 = new User(101L, "Rose", "Rose@163.com");
        userDAO.addUser(user2);
    }
  • 由于主服务类启用了事务,而测试服务类的Propagation.NEVER不允许运行在事务中,会抛出异常。
8. Propagation.NESTED
  • 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
  • 测试案例如下:主服务类不起任何事务,调用测试服务类Propagation.NESTED 的方法,且该方法中故意制造主键冲突的重复数据
代码语言:javascript复制
// 调用方不起事务,并调用Propagation.NESTED的过程
    public void callNestedTransaction(User user)  {
        User user1 = new User(100L, "Jack", "Jack@163.com");
        userDAO.addUser(user1);
        // 调用事务传播为Propagation.NEVER的过程
        userServiceTest.nestedTransaction();
    }
代码语言:javascript复制
// 事务传播为Propagation.NESTED
    @Transactional(propagation = Propagation.NESTED)
    public void nestedTransaction() {
        User user2 = new User(101L, "Rose", "Rose@163.com");
        userDAO.addUser(user2);
        // 插入重复数据,造成主键冲突
        User user3 = new User(101L, "Rose", "Rose@163.com");
        userDAO.addUser(user3);
    }
  • 由于主服务类没有启用事务,则第一条数据会插入表中,但测试服务类启用了Propagation.NESTED特性的事务,也即相当于默认事务行为,主键冲突抛出异常后,造成事务回滚,后面增加的两条数据都没有插入表。
注意点
  • 需要嵌套测试事务传播特性时应建立两个服务类,尽量不要在同一服务类中调用。

0 人点赞