共计 17999 个字符,预计需要花费 45 分钟才能阅读完成。
前言
Spring 框架已是 JAVA 我的项目的标配,其中 Spring 事务管理也是最罕用的一个性能,但如果不理解其实现原理,应用姿态不对,一不小心就可能掉坑里。
为了更透彻的阐明这些坑,本文分四局部开展论述:第一局部简略介绍下 Spring 事务集成的几种形式;第二局部联合 Spring 源代码阐明 Spring 事务的实现原理;第三局部通过理论测试代码介绍对于 Spring 事务的坑;第四局部是对本文的总结。
一、Spring 事务管理的几种形式:
Spring 事务在具体应用形式上可分为两大类:
1. 申明式
基于 TransactionProxyFactoryBean 的申明式事务管理
基于 和 命名空间的事务管理
基于 @Transactional 的申明式事务管理
2. 编程式
基于事务管理器 API 的编程式事务管理
基于 TransactionTemplate 的编程式事务管理
目前大部分我的项目应用的是申明式的后两种:
基于 和 命名空间的申明式事务管理能够充分利用切点表达式的弱小反对,使得治理事务更加灵便。基于 @Transactional 的形式须要施行事务管理的办法或者类上应用 @Transactional 指定事务规定即可实现事务管理,在 Spring Boot 中通常也倡议应用这种注解形式来标记事务。
二、Spring 事务实现机制
接下来咱们具体看下 Spring 事务的源代码,进而理解其工作原理。咱们从标签的解析类开始:
@Override
public void init() {registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser());
}
}
class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {return TransactionInterceptor.class;}
}
由此可看到 Spring 事务的外围实现类 TransactionInterceptor 及其父类 TransactionAspectSupport,其实现了事务的开启、数据库操作、事务提交、回滚等。咱们平时在开发时如果想确定是否在事务中,也能够在该办法进行断点调试。
整顿了一下 2021 年的 Java 工程师经典面试真题,共 485 页大略 850 道含答案的面试题 PDF,蕴含了 Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、Spring Boot、Spring Cloud、RabbitMQ、Kafka、Linux 等简直所有技术栈,每个技术栈都有不少于 50 道经典面试真题,不敢说刷完包你进大厂,但有针对性的刷让你面对面试官的时候多几分底气还是没问题的。
TransactionInterceptor:
public Object invoke(final MethodInvocation invocation) throws Throwable {Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
@Override
public Object proceedWithInvocation() throws Throwable {return invocation.proceed();
}
});
}
TransactionAspectSupport
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
至此咱们理解事务的整个调用流程,但还有一个重要的机制没剖析到,那就是 Spring 事务针对不同的流传级别管制以后获取的数据库连贯。接下来咱们看下 Spring 获取连贯的工具类 DataSourceUtils,JdbcTemplate、Mybatis-Spring 也都是通过该类获取 Connection。
public abstract class DataSourceUtils {
…
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {return doGetConnection(dataSource);
}
catch (SQLException ex) {throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {conHolder.requested();
if (!conHolder.hasConnection()) {logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
return conHolder.getConnection();}
…
}
TransactionSynchronizationManager 也是一个事务同步治理的外围类,它实现了事务同步治理的职能,包含记录以后连贯持有 connection holder。
TransactionSynchronizationManager
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
…
public static Object getResource(Object key) {Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Object value = doGetResource(actualKey);
if (value != null && logger.isTraceEnabled()) {logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
Thread.currentThread().getName() + "]");
}
return value;
}
/**
* Actually check the value of the resource that is bound for the given key.
*/
private static Object doGetResource(Object actualKey) {Map<Object, Object> map = resources.get();
if (map == null) {return null;}
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {resources.remove();
}
value = null;
}
return value;
}
在事务管理器类 AbstractPlatformTransactionManager 中,getTransaction 获取事务时,会解决不同的事务流传行为,例如以后存在事务,但调用办法事务流传级别为 REQUIRES_NEW、PROPAGATION_NOT_SUPPORTED 时,对以后事务进行挂起、复原等操作,以此保障了以后数据库操作获取正确的 Connection。
具体是在子事务提交的最初会将挂起的事务复原,复原时从新调用 TransactionSynchronizationManager. bindResource 设置之前的 connection holder,这样再获取的连贯就是被复原的数据库连贯,TransactionSynchronizationManager 以后激活的连贯只能是一个。
AbstractPlatformTransactionManager
private TransactionStatus handleExistingTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled)
throws TransactionException {
…
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {if (debugEnabled) {
logger.debug("Suspending current transaction, creating new transaction with name [" +
definition.getName() + "]");
}
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException beginEx) {resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
catch (Error beginErr) {resumeAfterBeginException(transaction, suspendedResources, beginErr);
throw beginErr;
}
}
/**
* Clean up after completion, clearing synchronization if necessary,
* and invoking doCleanupAfterCompletion.
* @param status object representing the transaction
* @see #doCleanupAfterCompletion
*/
private void cleanupAfterCompletion(DefaultTransactionStatus status) {status.setCompleted();
if (status.isNewSynchronization()) {TransactionSynchronizationManager.clear();
}
if (status.isNewTransaction()) {doCleanupAfterCompletion(status.getTransaction());
}
if (status.getSuspendedResources() != null) {if (status.isDebug()) {logger.debug("Resuming suspended transaction after completion of inner transaction");
}
resume(status.getTransaction(), (SuspendedResourcesHolder) status.getSuspendedResources());
}
}
Spring 的事务是通过 AOP 代理类中的一个 Advice(TransactionInterceptor)进行失效的,而流传级别定义了事务与子事务获取连贯、事务提交、回滚的具体形式。
AOP(Aspect Oriented Programming),即面向切面编程。Spring AOP 技术实现上其实就是代理类,具体可分为动态代理和动静代理两大类,其中动态代理是指应用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因而也称为编译时加强;(AspectJ);而动静代理则在运行时借助于 默写类库在内存中“长期”生成 AOP 动静代理类,因而也被称为运行时加强。其中 java 是应用的动静代理模式 (JDK+CGLIB)。
JDK 动静代理 JDK 动静代理次要波及到 java.lang.reflect 包中的两个类:Proxy 和 InvocationHandler。InvocationHandler 是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用指标类的代码,动静将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个合乎某一接口的实例,生成指标类的代理对象。
CGLIB 动静代理 CGLIB 全称为 Code Generation Library,是一个弱小的高性能,高质量的代码生成类库,能够在运行期扩大 Java 类与实现 Java 接口,CGLIB 封装了 asm,能够再运行期动静生成新的 class。和 JDK 动静代理相比拟:JDK 创立代理有一个限度,就是只能为接口创立代理实例,而对于没有通过接口定义业务办法的类,则能够通过 CGLIB 创立动静代理。
CGLIB 创立代理的速度比较慢,但创立代理后运行的速度却十分快,而 JDK 动静代理正好相同。如果在运行的时候一直地用 CGLIB 去创立代理,零碎的性能会大打折扣。因而如果有接口,Spring 默认应用 JDK 动静代理,源代码如下:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class:" +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {return new JdkDynamicAopProxy(config);
}
return new ObjenesisCGLIBAopProxy(config);
}
else {return new JdkDynamicAopProxy(config);
}
}
}
在理解 Spring 代理的两种特点后,咱们也就晓得在做事务切面配置时的一些注意事项,例如 JDK 代理时办法必须是 public,CGLIB 代理时必须是 public、protected,且类不能是 final 的;在依赖注入时,如果属性类型定义为实现类,JDK 代理时会报如下注入异样:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.wwb.test.TxTestAop': Unsatisfied dependency expressed through field 'service'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'stockService' is expected to be of type 'com.wwb.service.StockProcessServiceImpl' but was actually of type 'com.sun.proxy.$Proxy14'
但如果批改为 CGLIB 代理时则会胜利注入,所以如果有接口,倡议注入时该类属性都定义为接口。另外事务切点都配置在实现类和接口都能够失效,但倡议加在实现类上。
官网对于 Spring AOP 的具体介绍
docs.spring.io/spring/docs…
三、Spring 事务的那些坑
通过之前章节,置信您曾经把握了 spring 事务的应用形式与原理,不过还是要留神,因为一不小心就可能就掉坑。首先看第一个坑:
3.1 事务不失效
测试代码,事务 AOP 配置:
<tx:advice id="txAdvice" transaction-manager="myTxManager">
<tx:attributes>
<!-- 指定在连接点办法上利用的事务属性 -->
<tx:method name="openAccount" isolation="DEFAULT" propagation="REQUIRED"/>
<tx:method name="openStock" isolation="DEFAULT" propagation="REQUIRED"/>
<tx:method name="openStockInAnotherDb" isolation="DEFAULT" propagation="REQUIRES_NEW"/>
<tx:method name="openTx" isolation="DEFAULT" propagation="REQUIRED"/>
<tx:method name="openWithoutTx" isolation="DEFAULT" propagation="NEVER"/>
<tx:method name="openWithMultiTx" isolation="DEFAULT" propagation="REQUIRED"/>
</tx:advice>
public class StockProcessServiceImpl implements IStockProcessService{
@Autowired
private IAccountDao accountDao;
@Autowired
private IStockDao stockDao;
@Override
public void openAccount(String aname, double money) {accountDao.insertAccount(aname, money);
}
@Override
public void openStock(String sname, int amount) {stockDao.insertStock(sname, amount);
}
@Override
public void openStockInAnotherDb(String sname, int amount) {stockDao.insertStock(sname, amount);
}
}
public void insertAccount(String aname, double money) {String sql = "insert into account(aname, balance) values(?,?)";
this.getJdbcTemplate().update(sql, aname, money);
DbUtils.printDBConnectionInfo("insertAccount",getDataSource());
}
public void insertStock(String sname, int amount) {String sql = "insert into stock(sname, count) values (?,?)";
this.getJdbcTemplate().update(sql , sname, amount);
DbUtils.printDBConnectionInfo("insertStock",getDataSource());
}
public static void printDBConnectionInfo(String methodName,DataSource ds) {Connection connection = DataSourceUtils.getConnection(ds);
System.out.println(methodName+"connection hashcode="+connection.hashCode());
}
// 调用同类办法,外围配置事务
public void openTx(String aname, double money) {openAccount(aname,money);
openStock(aname,11);
}
1. 运行输入:
insertAccount connection hashcode=319558327 insertStock connection hashcode=319558327
// 调用同类办法,外围未配置事务
public void openWithoutTx(String aname, double money) {openAccount(aname,money);
openStock(aname,11);
}
2. 运行输入:
insertAccount connection hashcode=1333810223 insertStock connection hashcode=1623009085
// 通过 AopContext.currentProxy() 办法获取代理
@Override
public void openWithMultiTx(String aname, double money) {openAccount(aname,money);
openStockInAnotherDb(aname, 11);// 流传级别为 REQUIRES_NEW
}
3. 运行输入:
insertAccount connection hashcode=303240439 insertStock connection hashcode=303240439
能够看到 2、3 测试方法跟咱们事务预期并一样,论断:调用办法未配置事务、本类办法间接调用,事务都不失效!
究其原因,还是因为 Spring 的事务实质上是个代理类,而本类办法间接调用时其对象自身并不是织入事务的代理,所以事务切面并未失效。具体能够参见 #Spring 事务实现机制# 章节。
Spring 也提供了判断是否为代理的办法:
public static void printProxyInfo(Object bean) {System.out.println("isAopProxy"+AopUtils.isAopProxy(bean));
System.out.println("isCGLIBProxy="+AopUtils.isCGLIBProxy(bean));
System.out.println("isJdkProxy="+AopUtils.isJdkDynamicProxy(bean));
}
那如何批改为代理类调用呢?最间接的想法是注入本身,代码如下:
@Autowired
private IStockProcessService stockProcessService;
// 注入本身类,循环依赖,亲测能够
public void openTx(String aname, double money) {stockProcessService.openAccount(aname,money);
stockProcessService.openStockInAnotherDb (aname,11);
}
当然 Spring 提供了获取以后代理的办法:代码如下:
// 通过 AopContext.currentProxy() 办法获取代理
@Override
public void openWithMultiTx(String aname, double money) {((IStockProcessService)AopContext.currentProxy()).openAccount(aname,money);
((IStockProcessService)AopContext.currentProxy()).openStockInAnotherDb(aname, 11);
}
另外 Spring 是通过 TransactionSynchronizationManager 类中线程变量来获取事务中数据库连贯,所以如果是多线程调用或者绕过 Spring 获取数据库连贯,都会导致 Spring 事务配置生效。
最初 Spring 事务配置生效的场景:
事务切面未配置正确
本类办法调用
多线程调用
绕开 Spring 获取数据库连贯
接下来咱们看下 Spring 的事务的另外一个坑:
3.2 事务不回滚
测试代码:
<tx:advice id="txAdvice" transaction-manager="myTxManager">
<tx:attributes>
<!-- 指定在连接点办法上利用的事务属性 -->
<tx:method name="buyStock" isolation="DEFAULT" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
public void buyStock(String aname, double money, String sname, int amount) throws StockException {
boolean isBuy = true;
accountDao.updateAccount(aname, money, isBuy);
// 成心抛出异样
if (true) {throw new StockException("购买股票异样");
}
stockDao.updateStock(sname, amount, isBuy);
}
@Test
public void testBuyStock() {
try {service.openAccount("dcbs", 10000);
service.buyStock("dcbs", 2000, "dap", 5);
} catch (StockException e) {e.printStackTrace();
}
double accountBalance = service.queryAccountBalance("dcbs");
System.out.println("account balance is" + accountBalance);
}
输入后果:
insertAccount connection hashcode=656479172 updateAccount connection hashcode=517355658 account balance is 8000.0
利用抛出异样,但 accountDao.updateAccount 却进行了提交。究其原因,间接看 Spring 源代码:
TransactionAspectSupport
protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {if (txInfo != null && txInfo.hasTransaction()) {if (logger.isTraceEnabled()) {logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception:" + ex);
}
if (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;
}
…
}
public class DefaultTransactionAttribute extends DefaultTransactionDefinition implements TransactionAttribute {
@Override
public boolean rollbackOn(Throwable ex) {return (ex instanceof RuntimeException || ex instanceof Error);
}
…
}
由代码可见,Spring 事务默认只对 RuntimeException 和 Error 进行回滚,如果利用须要对指定的异样类进行回滚,可配置 rollback-for= 属性,例如:
<!-- 注册事务告诉 -->
<tx:advice id="txAdvice" transaction-manager="myTxManager">
<tx:attributes>
<!-- 指定在连接点办法上利用的事务属性 -->
<tx:method name="buyStock" isolation="DEFAULT" propagation="REQUIRED" rollback-for="StockException"/>
</tx:attributes>
</tx:advice>
事务不回滚的起因:
事务配置切面未失效
利用办法中将异样捕捉
抛出的异样不属于运行时异样(例如 IOException),
rollback-for 属性配置不正确
接下来咱们看下 Spring 事务的第三个坑:
3.3 事务超时不失效
测试代码:
<!-- 注册事务告诉 -->
<tx:advice id="txAdvice" transaction-manager="myTxManager">
<tx:attributes>
<tx:method name="openAccountForLongTime" isolation="DEFAULT" propagation="REQUIRED" timeout="3"/>
</tx:attributes>
</tx:advice>
@Override
public void openAccountForLongTime(String aname, double money) {accountDao.insertAccount(aname, money);
try {Thread.sleep(5000L);// 在数据库操作之后超时
} catch (InterruptedException e) {e.printStackTrace();
}
}
@Test
public void testTimeout() {service.openAccountForLongTime("dcbs", 10000);
}
失常运行,事务超时未失效
public void openAccountForLongTime(String aname, double money) {
try {Thread.sleep(5000L); // 在数据库操作之前超时
} catch (InterruptedException e) {e.printStackTrace();
}
accountDao.insertAccount(aname, money);
}
抛出事务超时异样,超时失效
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Nov 23 17:03:02 CST 2018 at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:141) …
通过源码看看 Spring 事务超时的判断机制:
ResourceHolderSupport
/**
* Return the time to live for this object in milliseconds.
* @return number of millseconds until expiration
* @throws TransactionTimedOutException if the deadline has already been reached
*/
public long getTimeToLiveInMillis() throws TransactionTimedOutException{if (this.deadline == null) {throw new IllegalStateException("No timeout specified for this resource holder");
}
long timeToLive = this.deadline.getTime() - System.currentTimeMillis();
checkTransactionTimeout(timeToLive <= 0);
return timeToLive;
}
/**
* Set the transaction rollback-only if the deadline has been reached,
* and throw a TransactionTimedOutException.
*/
private void checkTransactionTimeout(boolean deadlineReached) throws TransactionTimedOutException {if (deadlineReached) {setRollbackOnly();
throw new TransactionTimedOutException("Transaction timed out: deadline was" + this.deadline);
}
}
通过查看 getTimeToLiveInMillis 办法的 Call Hierarchy,能够看到被 DataSourceUtils 的 applyTimeout 所调用,持续看 applyTimeout 的 Call Hierarchy,能够看到有两处调用,一个是 JdbcTemplate,一个是 TransactionAwareInvocationHandler 类,后者是只有 TransactionAwareDataSourceProxy 类调用,该类为 DataSource 的事务代理类,咱们个别并不会用到。难道超时只能在这调用 JdbcTemplate 中失效?写代码亲测:
<!-- 注册事务告诉 -->
<tx:advice id="txAdvice" transaction-manager="myTxManager">
<tx:attributes>
<tx:method name="openAccountForLongTimeWithoutJdbcTemplate" isolation="DEFAULT" propagation="REQUIRED" timeout="3"/>
</tx:attributes>
</tx:advice>
public void openAccountForLongTimeWithoutJdbcTemplate(String aname, double money) {
try {Thread.sleep(5000L);
} catch (InterruptedException e) {e.printStackTrace();
}
accountDao.queryAccountBalanceWithoutJdbcTemplate(aname);
}
public double queryAccountBalanceWithoutJdbcTemplate(String aname) {
String sql = "select balance from account where aname = ?";
PreparedStatement prepareStatement;
try {prepareStatement = this.getConnection().prepareStatement(sql);
prepareStatement.setString(1, aname);
ResultSet executeQuery = prepareStatement.executeQuery();
while(executeQuery.next()) {return executeQuery.getDouble(1);
}
} catch (CannotGetJdbcConnectionException | SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();}
return 0;
}
运行失常,事务超时生效
由上可见:Spring 事务超时判断在通过 JdbcTemplate 的数据库操作时,所以如果超时后未有 JdbcTemplate 办法调用,则无奈精确判断超时。另外也能够得悉,如果通过 Mybatis 等操作数据库,Spring 的事务超时是有效的。鉴于此,Spring 的事务超时审慎应用。
四、总结
JDBC 标准中 Connection 的 setAutoCommit 是原生管制手动事务的办法,但流传行为、异样回滚、连贯治理等很多技术问题都须要开发者本人解决,而 Spring 事务通过 AOP 形式十分优雅的屏蔽了这些技术复杂度,使得事务管理变的异样简略。
但凡事有利弊,如果对实现机制了解不透彻,很容易掉坑里。最初总结下 Spring 事务的可能踩的坑:
1. Spring 事务未失效
调用办法自身未正确配置事务
本类办法间接调用
数据库操作未通过 Spring 的 DataSourceUtils 获取 Connection
多线程调用
2. Spring 事务回滚生效
未精确配置 rollback-for 属性
异样类不属于 RuntimeException 与 Error
利用捕捉了异样未抛出
3. Spring 事务超时不精确或生效
超时产生在最初一次 JdbcTemplate 操作之后
通过非 JdbcTemplate 操作数据库,例如 Mybatis
Spring 系列的学习笔记和面试题,蕴含 spring 面试题、spring cloud 面试题、spring boot 面试题、spring 教程笔记、spring boot 教程笔记、2020 年 Java 面试手册。一共整顿了 1184 页 PDF 文档。