前言

Spring框架已是JAVA我的项目的标配,其中Spring事务管理也是最罕用的一个性能,但如果不理解其实现原理,应用姿态不对,一不小心就可能掉坑里。

为了更透彻的阐明这些坑,本文分四局部开展论述:第一局部简略介绍下Spring事务集成的几种形式;第二局部联合Spring源代码阐明Spring事务的实现原理;第三局部通过理论测试代码介绍对于Spring事务的坑;第四局部是对本文的总结。

一、Spring事务管理的几种形式:

Spring事务在具体应用形式上可分为两大类:

1. 申明式

基于 TransactionProxyFactoryBean的申明式事务管理

基于 和 命名空间的事务管理

基于 @Transactional 的申明式事务管理

2. 编程式

基于事务管理器API 的编程式事务管理

基于TransactionTemplate 的编程式事务管理

目前大部分我的项目应用的是申明式的后两种:

基于 和 命名空间的申明式事务管理能够充分利用切点表达式的弱小反对,使得治理事务更加灵便。 基于 @Transactional 的形式须要施行事务管理的办法或者类上应用 @Transactional 指定事务规定即可实现事务管理,在Spring Boot中通常也倡议应用这种注解形式来标记事务。

二、Spring事务实现机制

接下来咱们具体看下Spring事务的源代码,进而理解其工作原理。咱们从标签的解析类开始:

@Overridepublic 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()办法获取代理@Overridepublic 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文档。