阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

使用声明式事务

27次阅读
没有评论

共计 7280 个字符,预计需要花费 19 分钟才能阅读完成。

使用 Spring 操作 JDBC 虽然方便,但是我们在前面讨论 JDBC 的时候,讲到过 JDBC 事务,如果要在 Spring 中操作事务,没必要手写 JDBC 事务,可以使用 Spring 提供的高级接口来操作事务。

Spring 提供了一个 PlatformTransactionManager 来表示事务管理器,所有的事务都由它负责管理。而事务由 TransactionStatus 表示。如果手写事务代码,使用 try...catch 如下:

TransactionStatus tx = null;
try {// 开启事务:
    tx = txManager.getTransaction(new DefaultTransactionDefinition());
    // 相关 JDBC 操作:
    jdbcTemplate.update("...");
    jdbcTemplate.update("...");
    // 提交事务:
    txManager.commit(tx);
} catch (RuntimeException e) {// 回滚事务:
    txManager.rollback(tx);
    throw e;
}

Spring 为啥要抽象出 PlatformTransactionManagerTransactionStatus?原因是 JavaEE 除了提供 JDBC 事务外,它还支持分布式事务 JTA(Java Transaction API)。分布式事务是指多个数据源(比如多个数据库,多个消息系统)要在分布式环境下实现事务的时候,应该怎么实现。分布式事务实现起来非常复杂,简单地说就是通过一个分布式事务管理器实现两阶段提交,但本身数据库事务就不快,基于数据库事务实现的分布式事务就慢得难以忍受,所以使用率不高。

Spring 为了同时支持 JDBC 和 JTA 两种事务模型,就抽象出 PlatformTransactionManager。因为我们的代码只需要 JDBC 事务,因此,在AppConfig 中,需要再定义一个 PlatformTransactionManager 对应的 Bean,它的实际类型是DataSourceTransactionManager

@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {
    ...
    @Bean
    PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {return new DataSourceTransactionManager(dataSource);
    }
}

使用编程的方式使用 Spring 事务仍然比较繁琐,更好的方式是通过声明式事务来实现。使用声明式事务非常简单,除了在 AppConfig 中追加一个上述定义的 PlatformTransactionManager 外,再加一个 @EnableTransactionManagement 就可以启用声明式事务:

@Configuration
@ComponentScan
@EnableTransactionManagement // 启用声明式
@PropertySource("jdbc.properties")
public class AppConfig {...}

然后,对需要事务支持的方法,加一个 @Transactional 注解:

@Component
public class UserService {// 此 public 方法自动具有事务支持:
    @Transactional
    public User register(String email, String password, String name) {...}
}

或者更简单一点,直接在 Bean 的 class 处加上,表示所有 public 方法都具有事务支持:

@Component
@Transactional
public class UserService {...}

Spring 对一个声明式事务的方法,如何开启事务支持?原理仍然是 AOP 代理,即通过自动创建 Bean 的 Proxy 实现:

public class UserService$$EnhancerBySpringCGLIB extends UserService {UserService target = ...
    PlatformTransactionManager txManager = ...

    public User register(String email, String password, String name) {TransactionStatus tx = null;
        try {tx = txManager.getTransaction(new DefaultTransactionDefinition());
            target.register(email, password, name);
            txManager.commit(tx);
        } catch (RuntimeException e) {txManager.rollback(tx);
            throw e;
        }
    }
    ...
}

注意:声明了 @EnableTransactionManagement 后,不必额外添加@EnableAspectJAutoProxy

回滚事务

默认情况下,如果发生了RuntimeException,Spring 的声明式事务将自动回滚。在一个事务方法中,如果程序判断需要回滚事务,只需抛出RuntimeException,例如:

@Transactional
public buyProducts(long productId, int num) {
    ...
    if (store < num) {// 库存不够,购买失败:
        throw new IllegalArgumentException("No enough products");
    }
    ...
}

如果要针对 Checked Exception 回滚事务,需要在 @Transactional 注解中写出来:

@Transactional(rollbackFor = {RuntimeException.class, IOException.class})
public buyProducts(long productId, int num) throws IOException {...}

上述代码表示在抛出 RuntimeExceptionIOException时,事务将回滚。

为了简化代码,我们强烈建议业务异常体系从 RuntimeException 派生,这样就不必声明任何特殊异常即可让 Spring 的声明式事务正常工作:

public class BusinessException extends RuntimeException {...}

public class LoginException extends BusinessException {...}

public class PaymentException extends BusinessException {...}

事务边界

在使用事务的时候,明确事务边界非常重要。对于声明式事务,例如,下面的 register() 方法:

@Component
public class UserService {@Transactional
    public User register(String email, String password, String name) {// 事务开始
       ...
    } // 事务结束
}

它的事务边界就是 register() 方法开始和结束。

类似的,一个负责给用户增加积分的 addBonus() 方法:

@Component
public class BonusService {@Transactional
    public void addBonus(long userId, int bonus) {// 事务开始
       ...
    } // 事务结束
}

它的事务边界就是 addBonus() 方法开始和结束。

在现实世界中,问题总是要复杂一点点。用户注册后,能自动获得 100 积分,因此,实际代码如下:

@Component
public class UserService {@Autowired
    BonusService bonusService;

    @Transactional
    public User register(String email, String password, String name) {// 插入用户记录:
        User user = jdbcTemplate.insert("...");
        // 增加 100 积分:
        bonusService.addBonus(user.id, 100);
    }
}

现在问题来了:调用方(比如 RegisterController)调用UserService.register() 这个事务方法,它在内部又调用了 BonusService.addBonus() 这个事务方法,一共有几个事务?如果 addBonus() 抛出了异常需要回滚事务,register()方法的事务是否也要回滚?

问题的复杂度是不是一下子提高了 10 倍?

事务传播

要解决上面的问题,我们首先要定义事务的传播模型。

假设用户注册的入口是 RegisterController,它本身没有事务,仅仅是调用UserService.register() 这个事务方法:

@Controller
public class RegisterController {@Autowired
    UserService userService;

    @PostMapping("/register")
    public ModelAndView doRegister(HttpServletRequest req) {String email = req.getParameter("email");
        String password = req.getParameter("password");
        String name = req.getParameter("name");
        User user = userService.register(email, password, name);
        return ...
    }
}

因此,UserService.register()这个事务方法的起始和结束,就是事务的范围。

我们需要关心的问题是,在 UserService.register() 这个事务方法内,调用BonusService.addBonus(),我们期待的事务行为是什么:

@Transactional
public User register(String email, String password, String name) {// 事务已开启:
    User user = jdbcTemplate.insert("...");
    // ???:
    bonusService.addBonus(user.id, 100);
} // 事务结束

对于大多数业务来说,我们期待 BonusService.addBonus() 的调用,和 UserService.register() 应当融合在一起,它的行为应该如下:

UserService.register()已经开启了一个事务,那么在内部调用 BonusService.addBonus() 时,BonusService.addBonus()方法就没必要再开启一个新事务,直接加入到 BonusService.register() 的事务里就好了。

其实就相当于:

  1. UserService.register()先执行了一条 INSERT 语句:INSERT INTO users ...
  2. BonusService.addBonus()再执行一条 INSERT 语句:INSERT INTO bonus ...

因此,Spring 的声明式事务为事务传播定义了几个级别,默认传播级别就是 REQUIRED,它的意思是,如果当前没有事务,就创建一个新事务,如果当前有事务,就加入到当前事务中执行。

我们观察 UserService.register() 方法,它在 RegisterController 中执行,因为 RegisterController 没有事务,因此,UserService.register()方法会自动创建一个新事务。

UserService.register() 方法内部,调用 BonusService.addBonus() 方法时,因为 BonusService.addBonus() 检测到当前已经有事务了,因此,它会加入到当前事务中执行。

因此,整个业务流程的事务边界就清晰了:它只有一个事务,并且范围就是 UserService.register() 方法。

有的童鞋会问:把 BonusService.addBonus() 方法的 @Transactional 去掉,变成一个普通方法,那不就规避了复杂的传播模型吗?

去掉 BonusService.addBonus() 方法的 @Transactional,会引来另一个问题,即其他地方如果调用BonusService.addBonus() 方法,那就没法保证事务了。例如,规定用户登录时积分 +5:

@Controller
public class LoginController {@Autowired
    BonusService bonusService;

    @PostMapping("/login")
    public ModelAndView doLogin(HttpServletRequest req) {User user = ...
        bonusService.addBonus(user.id, 5);
    }
}

可见,BonusService.addBonus()方法必须要有@Transactional,否则,登录后积分就无法添加了。

默认的事务传播级别是REQUIRED,它满足绝大部分的需求。还有一些其他的传播级别:

SUPPORTS:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为 SELECT 语句既可以在事务内执行,也可以不需要事务;

MANDATORY:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;

REQUIRES_NEW:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;

NOT_SUPPORTED:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;

NEVER:和 NOT_SUPPORTED 相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;

NESTED:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。

上面这么多种事务的传播级别,其实默认的 REQUIRED 已经满足绝大部分需求,SUPPORTSREQUIRES_NEW 在少数情况下会用到,其他基本不会用到,因为把事务搞得越复杂,不仅逻辑跟着复杂,而且速度也会越慢。

定义事务的传播级别也是写在 @Transactional 注解里的:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public Product createProduct() {...}

现在只剩最后一个问题了:Spring 是如何传播事务的?

我们在 JDBC 中使用事务的时候,是这么个写法:

Connection conn = openConnection();
try {// 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条 SQL 语句:
    insert(); update(); delete();
    // 提交事务:
    conn.commit();} catch (SQLException e) {// 回滚事务:
    conn.rollback();} finally {conn.setAutoCommit(true);
    conn.close();}

Spring 使用声明式事务,最终也是通过执行 JDBC 事务来实现功能的,那么,一个事务方法,如何获知当前是否存在事务?

答案是使用 ThreadLocal。Spring 总是把 JDBC 相关的 ConnectionTransactionStatus实例绑定到 ThreadLocal。如果一个事务方法从ThreadLocal 未取到事务,那么它会打开一个新的 JDBC 连接,同时开启一个新的事务,否则,它就直接使用从 ThreadLocal 获取的 JDBC 连接以及TransactionStatus

因此,事务能正确传播的前提是,方法调用是在一个线程内才行。如果像下面这样写:

@Transactional
public User register(String email, String password, String name) {// BEGIN TX-A
    User user = jdbcTemplate.insert("...");
    new Thread(() -> {// BEGIN TX-B:
        bonusService.addBonus(user.id, 100);
        // END TX-B
    }).start();} // END TX-A

在另一个线程中调用 BonusService.addBonus(),它根本获取不到当前事务,因此,UserService.register()BonusService.addBonus()两个方法,将分别开启两个完全独立的事务。

换句话说,事务只能在当前线程传播,无法跨线程传播。

那如果我们想实现跨线程传播事务呢?原理很简单,就是要想办法把当前线程绑定到 ThreadLocalConnectionTransactionStatus 实例传递给新线程,但实现起来非常复杂,根据异常回滚更加复杂,不推荐自己去实现。

练习

使用声明式事务。

下载练习

小结

Spring 提供的声明式事务极大地方便了在数据库中使用事务,正确使用声明式事务的关键在于确定好事务边界,理解事务传播级别。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计7280字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中