756 lines
31 KiB
Markdown
756 lines
31 KiB
Markdown
|
||
|
||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||
|
||
|
||
20 Spring 事务常见错误(下)
|
||
你好,我是傅健。
|
||
|
||
通过上一节课的学习,我们了解了 Spring 事务的原理,并解决了几个常见的问题。这节课我们将继续讨论事务中的另外两个问题,一个是关于事务的传播机制,另一个是关于多数据源的切换问题,通过这两个问题,你可以更加深入地了解 Spring 事务的核心机制。
|
||
|
||
案例 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;
|
||
|
||
|
||
|
||
学生选课表 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。
|
||
|
||
接下来我们完成用户的相关操作,主要包括两部分。
|
||
|
||
|
||
新增学生选课记录
|
||
|
||
|
||
@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
|
||
|
||
|
||
@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;
|
||
}
|
||
//......省略非关键代码.....
|
||
}
|
||
|
||
|
||
整个方法完成了事务的一整套处理逻辑,如下:
|
||
|
||
|
||
检查是否需要创建事务;
|
||
调用具体的业务方法进行处理;
|
||
提交事务;
|
||
处理异常。
|
||
|
||
|
||
这里要格外注意的是,当前案例是两个事务嵌套的场景,外层事务 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);
|
||
}
|
||
}
|
||
|
||
|
||
这个方法里区分了三种不同类型的情况:
|
||
|
||
|
||
是否有保存点;
|
||
是否为一个新的事务;
|
||
是否处于一个更大的事务中。
|
||
|
||
|
||
在这里,因为我们用的是默认的传播类型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 的默认声明行不行,为什么?
|
||
|
||
期待你的思考,我们留言区见!
|
||
|
||
|
||
|
||
|