大师兄

20 | Spring 事务常见错误(下)

你好,我是傅健。

通过上一节课的学习,我们了解了 Spring 事务的原理,并解决了几个常见的问题。这节课我们将继续讨论事务中的另外两个问题,一个是关于事务的传播机制,另一个是关于多数据源的切换问题,通过这两个问题,你可以更加深入地了解 Spring 事务的核心机制。

案例 1:嵌套事务回滚错误

上一节课我们完成了学生注册功能,假设我们需要对这个功能继续进行扩展,当学生注册完成后,需要给这个学生登记一门英语必修课,并更新这门课的登记学生数。为此,我添加了两个表。

  1. 课程表 course,记录课程名称和注册的学生数。
CREATE TABLE `course` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`course_name` varchar(64) DEFAULT NULL,
`number` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 学生选课表 student\_course,记录学生表 student 和课程表 course 之间的多对多关联。
CREATE TABLE `student_course` (
`student_id` int(11) NOT NULL,
`course_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

同时我为课程表初始化了一条课程信息,id = 1,course_name = "英语",number = 0。

接下来我们完成用户的相关操作,主要包括两部分。

  1. 新增学生选课记录
@Mapper
public interface StudentCourseMapper {
@Insert("INSERT INTO `student_course`(`student_id`, `course_id`) VALUES (#{studentId}, #{courseId})")
void saveStudentCourse(@Param("studentId") Integer studentId, @Param("courseId") Integer courseId);
}
  1. 课程登记学生数 + 1
@Mapper
public interface CourseMapper {
@Update("update `course` set number = number + 1 where id = #{id}")
void addCourseNumber(int courseId);
}

我们增加了一个新的业务类 CourseService,用于实现相关业务逻辑。分别调用了上述两个方法来保存学生与课程的关联关系,并给课程注册人数+1。最后,别忘了给这个方法加上事务注解。

@Service
public class CourseService {
@Autowired
private CourseMapper courseMapper;
@Autowired
private StudentCourseMapper studentCourseMapper;
//注意这个方法标记了“Transactional”
@Transactional(rollbackFor = Exception.class)
public void regCourse(int studentId) throws Exception {
studentCourseMapper.saveStudentCourse(studentId, 1);
courseMapper.addCourseNumber(1);
}
}

我们在之前的 StudentService.saveStudent() 中调用了 regCourse(),实现了完整的业务逻辑。为了避免注册课程的业务异常导致学生信息无法保存,在这里 catch 了注册课程方法中抛出的异常。我们希望的结果是,当注册课程发生错误时,只回滚注册课程部分,保证学生信息依然正常。

@Service
public class StudentService {
//省略非关键代码
@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception {
Student student = new Student();
student.setRealname(realname);
studentService.doSaveStudent(student);
try {
courseService.regCourse(student.getId());
} catch (Exception e) {
e.printStackTrace();
}
}
//省略非关键代码
}

为了验证异常是否符合预期,我们在 regCourse() 里抛出了一个注册失败的异常:

@Transactional(rollbackFor = Exception.class)
public void regCourse(int studentId) throws Exception {
studentCourseMapper.saveStudentCourse(studentId, 1);
courseMapper.addCourseNumber(1);
throw new Exception("注册失败");
}

运行一下这段代码,在控制台里我们看到了以下提示信息:

java.lang.Exception: 注册失败
at com.spring.puzzle.others.transaction.example3.CourseService.regCourse(CourseService.java:22)
//......省略非关键代码.....
Exception in thread "main" org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:873)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:710)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:533)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:304)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at com.spring.puzzle.others.transaction.example3.StudentService$$EnhancerBySpringCGLIB$$50cda404.saveStudent(<generated>)
at com.spring.puzzle.others.transaction.example3.AppConfig.main(AppConfig.java:22)

其中,注册失败部分的异常符合预期,但是后面又多了一个这样的错误提示:Transaction rolled back because it has been marked as rollback-only。

最后的结果是,学生和选课的信息都被回滚了,显然这并不符合我们的预期。我们期待的结果是即便内部事务regCourse()发生异常,外部事务saveStudent()俘获该异常后,内部事务应自行回滚,不影响外部事务。那么这是什么原因造成的呢?我们需要研究一下 Spring 的源码,来找找答案。

案例解析

在做进一步的解析之前,我们可以先通过伪代码把整个事务的结构梳理一下:

// 外层事务
@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception {
//......省略逻辑代码.....
studentService.doSaveStudent(student);
try {
// 嵌套的内层事务
@Transactional(rollbackFor = Exception.class)
public void regCourse(int studentId) throws Exception {
//......省略逻辑代码.....
}
} catch (Exception e) {
e.printStackTrace();
}
}

可以看出来,整个业务是包含了 2 层事务,外层的 saveStudent() 的事务和内层的 regCourse() 事务。

在 Spring 声明式的事务处理中,有一个属性 propagation,表示打算对这些方法怎么使用事务,即一个带事务的方法调用了另一个带事务的方法,被调用的方法它怎么处理自己事务和调用方法事务之间的关系。

其中 propagation 有7种配置:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。默认是 REQUIRED,它的含义是:如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。

结合我们的伪代码示例,因为在 saveStudent() 上声明了一个外部的事务,就已经存在一个事务了,在propagation值为默认的REQUIRED的情况下, regCourse() 就会加入到已有的事务中,两个方法共用一个事务。

我们再来看下 Spring 事务处理的核心,其关键实现参考TransactionAspectSupport.invokeWithinTransaction():

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 是否需要创建一个事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 调用具体的业务方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 当发生异常时进行处理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
// 正常返回时提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
//......省略非关键代码.....
}

整个方法完成了事务的一整套处理逻辑,如下:

  1. 检查是否需要创建事务;
  2. 调用具体的业务方法进行处理;
  3. 提交事务;
  4. 处理异常。

这里要格外注意的是,当前案例是两个事务嵌套的场景,外层事务 doSaveStudent()和内层事务 regCourse(),每个事务都会调用到这个方法。所以,这个方法会被调用两次。下面我们来具体来看下内层事务对异常的处理。

当捕获了异常,会调用TransactionAspectSupport.completeTransactionAfterThrowing() 进行异常处理:

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
//......省略非关键代码.....
}
}

在这个方法里,我们对异常类型做了一些检查,当符合声明中的定义后,执行了具体的 rollback 操作,这个操作是通过 TransactionManager.rollback() 完成的:

public final void rollback(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
processRollback(defStatus, false);
}

而 rollback() 是在 AbstractPlatformTransactionManager 中实现的,继续调用了 processRollback():

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;
if (status.hasSavepoint()) {
// 有保存点
status.rollbackToHeldSavepoint();
}
else if (status.isNewTransaction()) {
// 是否为一个新的事务
doRollback(status);
}
else {
// 处于一个更大的事务中
if (status.hasTransaction()) {
// 分支1
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
doSetRollbackOnly(status);
}
}
if (!isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = false;
}
}
// 省略非关键代码
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}

这个方法里区分了三种不同类型的情况:

  1. 是否有保存点;
  2. 是否为一个新的事务;
  3. 是否处于一个更大的事务中。

在这里,因为我们用的是默认的传播类型REQUIRED,嵌套的事务并没有开启一个新的事务,所以在这种情况下,当前事务是处于一个更大的事务中,所以会走到情况3分支1的代码块下。

这里有两个判断条件来确定是否设置为仅回滚:
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure())

满足任何一个,都会执行 doSetRollbackOnly() 操作。isLocalRollbackOnly 在当前的情况下是 false,所以是否分设置为仅回滚就由 isGlobalRollbackOnParticipationFailure() 这个方法来决定了,其默认值为 true, 即是否回滚交由外层事务统一决定 。

显然这里的条件得到了满足,从而执行 doSetRollbackOnly:

protected void doSetRollbackOnly(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
txObject.setRollbackOnly();
}

以及最终调用到的DataSourceTransactionObject中的setRollbackOnly():

public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}

到这一步,内层事务的操作基本执行完毕,它处理了异常,并最终调用到了DataSourceTransactionObject中的setRollbackOnly()

接下来,我们来看外层事务。因为在外层事务中,我们自己的代码捕获了内层抛出来的异常,所以这个异常不会继续往上抛,最后的事务会在 TransactionAspectSupport.invokeWithinTransaction() 中的 commitTransactionAfterReturning() 中进行处理:

protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
if (txInfo != null && txInfo.getTransactionStatus() != null) { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}

在这个方法里我们执行了 commit 操作,代码如下:

public final void commit(TransactionStatus status) throws TransactionException {
//......省略非关键代码.....
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
processRollback(defStatus, true);
return;
}
processCommit(defStatus);
}

在 AbstractPlatformTransactionManager.commit()中,当满足了 shouldCommitOnGlobalRollbackOnly() 和 defStatus.isGlobalRollbackOnly(),就会回滚,否则会继续提交事务。其中shouldCommitOnGlobalRollbackOnly()的作用为,如果发现了事务被标记了全局回滚,并且在发生了全局回滚的情况下,判断是否应该提交事务,这个方法的默认实现是返回了 false,这里我们不需要关注它,继续查看isGlobalRollbackOnly()的实现:

public boolean isGlobalRollbackOnly() {
return ((this.transaction instanceof SmartTransactionObject) &&
((SmartTransactionObject) this.transaction).isRollbackOnly());
}

这个方法最终进入了DataSourceTransactionObject类中的isRollbackOnly():

public boolean isRollbackOnly() {
return getConnectionHolder().isRollbackOnly();
}

现在让我们再次回顾一下之前的内部事务处理结果,其最终调用到的是DataSourceTransactionObject中的setRollbackOnly():

public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}

isRollbackOnly()和setRollbackOnly()这两个方法的执行本质都是对ConnectionHolder中rollbackOnly属性标志位的存取,而ConnectionHolder则存在于DefaultTransactionStatus类实例的transaction属性之中。

至此,答案基本浮出水面了,我们把整个逻辑串在一起就是:外层事务是否回滚的关键,最终取决于DataSourceTransactionObject类中的isRollbackOnly(),而该方法的返回值,正是我们在内层异常的时候设置的

所以最终外层事务也被回滚了,从而在控制台中打印出异常信息:"Transaction rolled back because it has been marked as rollback-only"。

所以到这里,问题也就清楚了,Spring默认的事务传播属性为REQUIRED,如我们之前介绍的,它的含义是:如果本来有事务,则加入该事务,如果没有事务,则创建新的事务,因而内外两层事务都处于同一个事务中。所以,当我们在 regCourse()中抛出异常,并触发了回滚操作时,这个回滚会进一步传播,从而把 saveStudent() 也回滚了。最终导致整个事务都被回滚了。

问题修正

从上述案例解析中,我们了解到,Spring 在处理事务过程中,有个默认的传播属性 REQUIRED,在整个事务的调用链上,任何一个环节抛出的异常都会导致全局回滚。

知道了这个结论,修改方法也就很简单了,我们只需要对传播属性进行修改,把类型改成 REQUIRES_NEW 就可以了。于是这部分代码就修改成这样:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void regCourse(int studentId) throws Exception {
studentCourseMapper.saveStudentCourse(studentId, 1);
courseMapper.addCourseNumber(1);
throw new Exception("注册失败");
}

运行一下看看:

java.lang.Exception: 注册失败
at com.spring.puzzle.others.transaction.example3.CourseService.regCourse(CourseService.java:22)

异常正常抛出,注册课程部分的数据没有保存,但是学生还是正常注册成功。这意味着此时Spring 只对注册课程这部分的数据进行了回滚,并没有传播到上一级。

这里我简单解释下这个过程:

  • 当子事务声明为 Propagation.REQUIRES_NEW 时,在 TransactionAspectSupport.invokeWithinTransaction() 中调用 createTransactionIfNecessary() 就会创建一个新的事务,独立于外层事务。
  • 而在 AbstractPlatformTransactionManager.processRollback() 进行 rollback 处理时,因为 status.isNewTransaction() 会因为它处于一个新的事务中而返回 true,所以它走入到了另一个分支,执行了 doRollback() 操作,让这个子事务单独回滚,不会影响到主事务。

至此,这个问题得到了很好的解决。

案例 2:多数据源间切换之谜

在前面的案例中,我们完成了学生注册功能和课程注册功能。假设新需求又来了,每个学生注册的时候,需要给他们发一张校园卡,并给校园卡里充入 50 元钱。但是这个校园卡管理系统是一个第三方系统,使用的是另一套数据库,这样我们就需要在一个事务中同时操作两个数据库。

第三方的 Card 表如下:

CREATE TABLE `card` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`student_id` int(11) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

对应的 Card 对象如下:

public class Card {
private Integer id;
private Integer studentId;
private Integer balance;
//省略 Get/Set 方法
}

对应的 Mapper 接口如下,里面包含了一个 saveCard 的 insert 语句,用于创建一条校园卡记录:

@Mapper
public interface CardMapper {
@Insert("INSERT INTO `card`(`student_id`, `balance`) VALUES (#{studentId}, #{balance})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int saveCard(Card card);
}

Card 的业务类如下,里面实现了卡与学生 ID 关联,以及充入 50 元的操作:

@Service
public class CardService {
@Autowired
private CardMapper cardMapper;
@Transactional
public void createCard(int studentId) throws Exception {
Card card = new Card();
card.setStudentId(studentId);
card.setBalance(50);
cardMapper.saveCard(card);
}
}

案例解析

这是一个相对常见的需求,学生注册和发卡都要在一个事务里完成,但是我们都默认只会连一个数据源,之前我们一直连的都是学生信息这个数据源,在这里,我们还需要对校园卡的数据源进行操作。于是,我们需要在一个事务里完成对两个数据源的操作,该如何实现这样的功能呢?

我们继续从 Spring 的源码中寻找答案。在 Spring 里有这样一个抽象类 AbstractRoutingDataSource,这个类相当于 DataSource 的路由中介,在运行时根据某种 key 值来动态切换到所需的 DataSource 上。通过实现这个类就可以实现我们期望的动态数据源切换。

这里强调一下,这个类里有这么几个关键属性:

  • targetDataSources 保存了 key 和数据库连接的映射关系;
  • defaultTargetDataSource 标识默认的连接;
  • resolvedDataSources 存储数据库标识和数据源的映射关系。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
//省略非关键代码
}

AbstractRoutingDataSource 实现了 InitializingBean 接口,并覆写了 afterPropertiesSet()。该方法会在初始化 Bean 的时候执行,将多个 DataSource 初始化到 resolvedDataSources。这里的 targetDataSources 属性存储了将要切换的多数据源 Bean 信息。

@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}

获取数据库连接的是 getConnection(),它调用了 determineTargetDataSource()来创建连接:

@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}

determineTargetDataSource()是整个部分的核心,它的作用就是动态切换数据源。有多少个数据源,就存多少个数据源在 targetDataSources 中。

targetDataSources 是一个 Map 类型的属性,key 表示每个数据源的名字,value 对应的是每个数据源 DataSource。

protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}

而选择哪个数据源又是由 determineCurrentLookupKey()来决定的,此方法是抽象方法,需要我们继承 AbstractRoutingDataSource 抽象类来重写此方法。该方法返回一个 key,该 key 是 Bean 中的 beanName,并赋值给 lookupKey,由此 key 可以通过 resolvedDataSources 属性的键来获取对应的 DataSource 值,从而达到数据源切换的效果。

protected abstract Object determineCurrentLookupKey();

这样看来,这个方法的实现就得由我们完成了。接下来我们将会完成一系列相关的代码,解决这个问题。

问题修正

首先,我们创建一个 MyDataSource 类,继承了 AbstractRoutingDataSource,并覆写了 determineCurrentLookupKey():

public class MyDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> key = new ThreadLocal<String>();
@Override
protected Object determineCurrentLookupKey() {
return key.get();
}
public static void setDataSource(String dataSource) {
key.set(dataSource);
}
public static String getDatasource() {
return key.get();
}
public static void clearDataSource() {
key.remove();
}
}

其次,我们需要修改 JdbcConfig。这里我新写了一个 dataSource,将原来的 dataSource 改成 dataSourceCore,再将新定义的 dataSourceCore 和 dataSourceCard 放进一个 Map,对应的 key 分别是 core 和 card,并把 Map 赋值给 setTargetDataSources

public class JdbcConfig {
//省略非关键代码
@Value("${card.driver}")
private String cardDriver;
@Value("${card.url}")
private String cardUrl;
@Value("${card.username}")
private String cardUsername;
@Value("${card.password}")
private String cardPassword;
@Autowired
@Qualifier("dataSourceCard")
private DataSource dataSourceCard;
@Autowired
@Qualifier("dataSourceCore")
private DataSource dataSourceCore;
//省略非关键代码
@Bean(name = "dataSourceCore")
public DataSource createCoreDataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
@Bean(name = "dataSourceCard")
public DataSource createCardDataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(cardDriver);
ds.setUrl(cardUrl);
ds.setUsername(cardUsername);
ds.setPassword(cardPassword);
return ds;
}
@Bean(name = "dataSource")
public MyDataSource createDataSource() {
MyDataSource myDataSource = new MyDataSource();
Map<Object, Object> map = new HashMap<>();
map.put("core", dataSourceCore);
map.put("card", dataSourceCard);
myDataSource.setTargetDataSources(map);
myDataSource.setDefaultTargetDataSource(dataSourceCore);
return myDataSource;
}
//省略非关键代码
}

最后还剩下一个问题,setDataSource 这个方法什么时候执行呢?

我们可以用 Spring AOP 来设置,把配置的数据源类型都设置成注解标签, Service层中在切换数据源的方法上加上注解标签,就会调用相应的方法切换数据源。

我们定义了一个新的注解 @DataSource,可以直接加在 Service()上,实现数据库切换:

@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value();
String core = "core";
String card = "card";
}

声明方法如下:

@DataSource(DataSource.card)

另外,我们还需要写一个 Spring AOP 来对相应的服务方法进行拦截,完成数据源的切换操作。特别要注意的是,这里要加上一个 @Order(1) 标记它的初始化顺序。这个 Order 值一定要比事务的 AOP 切面的值小,这样可以获得更高的优先级,否则自动切换数据源将会失效。

@Aspect
@Service
@Order(1)
public class DataSourceSwitch {
@Around("execution(* com.spring.puzzle.others.transaction.example3.CardService.*(..))")
public void around(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(DataSource.class)) {
DataSource dataSource = method.getAnnotation(DataSource.class);
MyDataSource.setDataSource(dataSource.value());
System.out.println("数据源切换至:" + MyDataSource.getDatasource());
}
point.proceed();
MyDataSource.clearDataSource();
System.out.println("数据源已移除!");
}
}

最后,我们实现了 Card 的发卡逻辑,在方法前声明了切换数据库:

@Service
public class CardService {
@Autowired
private CardMapper cardMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@DataSource(DataSource.card)
public void createCard(int studentId) throws Exception {
Card card = new Card();
card.setStudentId(studentId);
card.setBalance(50);
cardMapper.saveCard(card);
}
}

并在 saveStudent() 里调用了发卡逻辑:

@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception {
Student student = new Student();
student.setRealname(realname);
studentService.doSaveStudent(student);
try {
courseService.regCourse(student.getId());
cardService.createCard(student.getId());
} catch (Exception e) {
e.printStackTrace();
}
}

执行一下,一切正常,两个库的数据都可以正常保存了。

最后我们来看一下整个过程的调用栈,重新过一遍流程(这里我略去了不重要的部分)。

在创建了事务以后,会通过 DataSourceTransactionManager.doBegin()获取相应的数据库连接:

protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = obtainDataSource().getConnection();
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
//省略非关键代码
}

这里的 obtainDataSource().getConnection() 调用到了 AbstractRoutingDataSource.getConnection(),这就与我们实现的功能顺利会师了。

public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}

重点回顾

通过以上两个案例,相信你对 Spring 的事务机制已经有了深刻的认识,最后总结下重点:

  • Spring 在事务处理中有一个很重要的属性 Propagation,主要用来配置当前需要执行的方法如何使用事务,以及与其它事务之间的关系。
  • Spring 默认的传播属性是 REQUIRED,在有事务状态下执行,如果当前没有事务,则创建新的事务;
  • Spring 事务是可以对多个数据源生效,它提供了一个抽象类 AbstractRoutingDataSource,通过实现这个抽象类,我们可以实现自定义的数据库切换。

思考题

结合案例2,请你思考这样一个问题:在这个案例中,我们在 CardService类方法上声明了这样的事务传播属性,@Transactional(propagation = Propagation.REQUIRES_NEW),如果使用 Spring 的默认声明行不行,为什么?

期待你的思考,我们留言区见!