头图

接上篇【熟练掌握spring框架第四篇】

spring 数据源自动配置

程序员的日常工作中操作数据库无疑是最频繁的事情了。很多刚毕业的求职者很自信,不就是CURD嘛,谁不会呢。确实我们处在一个轮子满天飞时代,很多事情框架都已经代劳了。与其说写代码是盖房子,不如说是在搭积木。我们也不需要一砖一瓦的垒房子。那样成本太大了。但既然是搭积木,那么我们就要清楚每块积木的结构。这样才能搭建简单牢靠的房子呢。下面我们就通过源码学习下spring给我们提供的针对数据库操作的模块spring-data-jdbc是如何工作的吧。为了更好的说明问题,先来个简单的例子。

image-20210514174151671

程序启动后会往coffee这个表里插入一条数据。那么问题来了。数据源,连接池呢。没错他们都被spring自动配置了。首先我们看下数据源自动配置。

image-20210515140527686

image-20210514174601316

DataSourceProperties实现了InitializingBean,它的afterPropertiesSet方法会根据当前加载的类自动判断内嵌数据库类型。

image-20210514175549690

image-20210515140648325

DataSourceProperties可以生成一个DataSource的建造器DataSourceBuilder,主要是设置驱动class连接url,用户名和密码还有数据源类型。重点来了。创建出来的DataSource用来干嘛的呢?我们发现DataSource的所在的包是tomcat-jdbc,说明这件事情已经交给了tomcat-jdbc这个东东。

tomcat-jdbc

首先它是Apache Commons DBCP的替代品,官网好处说了一堆:https://tomcat.apache.org/tom...

  1. 支持高并发。
  2. 可以自定义拦截器
  3. 高性能
  4. 支持 JMX
  5. 超简单:tomcat-jdbc只有8个核心文件,最重要的是ConnectionPool
  6. 更智能的空闲连接处理
  7. 可以异步获取ConnectionFuture<Connection>
  8. 自定义连接丢弃实时机:是连接池满了,还是连接使用超时了。

额……他说了不算,我们只相信源码。

连接池的初始化

我们将断点设置在DataSourceProxypCreatePool里,看下它的调用栈。

image-20210515140839146

随着第一次从数据源中获取ConnectionConnectionPool被创建了。创建连接池的时候主要执行了ConnectionPoolinit方法。

protected void init(PoolConfiguration properties) throws SQLException {
        poolProperties = properties;
        //检查连接池配置有无问题
        checkPoolConfiguration(properties);
        //初始化用于存放连接的繁忙队列
        busy = new LinkedBlockingQueue<>();
        //初始化用于存放连接的空闲队列
        idle = new FairBlockingQueue<>();
        //初始化定时任务:每5000毫秒一次进行连接池的健康检查。
        initializePoolCleaner(properties);

        //如果开启JMX,注册Mbean
        if (this.getPoolProperties().isJmxEnabled()) createMBean();
  
        //创建10个连接。
        PooledConnection[] initialPool = new PooledConnection[poolProperties.getInitialSize()];
        try {
            for (int i = 0; i < initialPool.length; i++) {
                //通过org.h2.Driver.connect方法逐个创建java.sql.Connection,并添加到繁忙队列中。
                initialPool[i] = this.borrowConnection(0, null, null); //don't wait, should be no contention
            } //for

        } catch (SQLException x) {
            log.error("Unable to create initial connections of pool.", x);
            if (!poolProperties.isIgnoreExceptionOnPreLoad()) {
                if (jmxPool!=null) jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_INIT, getStackTrace(x));
                close(true);
                throw x;
            }
        } finally {
            //return the members as idle to the pool
            for (int i = 0; i < initialPool.length; i++) {
                if (initialPool[i] != null) {
                    //将10个连接逐个归还到idle队列中
                    try {this.returnConnection(initialPool[i]);}catch(Exception x){/*NOOP*/}
                } //end if
            } //for
        } //catch

        closed = false;
}

通过源码发现,borrowConnection,找谁借?找idle借。借了往哪放,往busy队列放,还的时候自然也是将busy的连接还给idle,如果连接不够,内部会再去创建新的连接,当然此时借的流程还是免不了的。那么问题来了。

借了到底啥时候还

这个问题暂且放着。后面将事务的时候会解释到。

能一直借吗

当然不能,读者可以参照下ConnectionPoolprivate PooledConnection borrowConnection(int wait, String username, String password)方法。它会先使用非阻塞的方式从idle拿,如果拿到了直接返回,如果拿不到。看下当前连接数有没有超过maxActive,默认是100哦。如果没有,创建一个。如果超过了。按照指定超时时间阻塞拿吧。这个超时时间如果wait传的是-1的话,默认是maxWait30秒。如果30秒之后还是拿不到连接呢。那只能自曝了。

image-20210515161100455

连接长时间不用会关闭吗

tomcat连接池有个配置项叫做maxAge。它的含义是:

连接保持时间(以毫秒计)。当连接要返回池中时,连接池会检查是否达到 now - time-when-connected > maxAge 的条件,如果条件达成,则关闭该连接,不再将其返回池中。默认值为 0,意味着连接将保持开放状态,在将连接返回池中时,不会执行任何年龄检查。

PooledConnection有个isMaxAgeExpired的方法就是用来检查是否超过了最大保持时间。ConectionPool有个reconnectIfExpired方法检查如果某个连接超过了最大保持时间,就进行重连,即先断开,再连接。而在归还连接的时候会执行reconnectIfExpired,所以如果设置了maxAge那么就有可能触发重连操作。

另外我们看还有个配置叫maxIdle。它的含义是:

(整型值)池始终都应保留的连接的最大数目。默认为 maxActive:100。会周期性检查空闲连接(如果启用该功能),留滞时间超过 minEvictableIdleTimeMillis 的空闲连接将会被释放。

归还连接的时候,如果发现空闲队列的大小超过这个阈值,就会释放当前要归还的连接。即关闭该连接。

有了maxIdle,势必会有minIdle,它的含义是:池始终都应保留的连接的最小数目

定时清理任务PoolCleaner发现idle队列的大小如果超过这个值的话,就会检查每个连接,把当前时间减去最后一次touched的时间,如果超过minEvictableIdleTimeMillis,则释放连接。注意:这个配置项可没有maxXXX哦。

tomcat给了我们很多配置项,以便根据实际场景灵活变动。然而实际线上我们常常将initialSizeminIdlemaxIdle,这三个指标设置为一样大。是为什么呢。其实原因很简单。比如说1天的访问量,高峰期除外。50个connection就能应付了。那么这三个值就设置为50,这样连接数就基本保持不变。不会频繁的connectdisconect,因为连接也是很耗时的事情啊。如果时间都花在上面了,不就影响正确业务了吗?偶尔有个流量小高峰,没关系,连接数瞬间飙一下,又恢复到50了。所以这个值怎么定,我认为,如果一天中大部分时间都不会超过这个值,那么就是是它了。触类旁通,其实很多连接池的配置都可以参考这个套路。

使用jmx监控ConnectionPool

DataSource的继承关系,我们发现它是一个可以通过jmx管理的bean。但是使用之前必须开启它。

spring.datasource.tomcat.jmx-enabled=true

然后我们就可以使用jmx客户端jconsole查看了。不仅可以查看关心的指标,还可以执行一些操作。更厉害的是比如上面出现连接池获取不到连接时还会给jmx客户端推送一个通知。如果自己写代码实现客户端的话,就可以邮件告警啦。

image-20210515162947871

spring声明式事务

参考:https://blog.csdn.net/qq_3688...

声明式事务由来

这个问题很好回答,前面介绍AOP这种编程范式时就提到过事务处理属于其中一个很典型的应用场景。可以说AOP编程范式的由来就是声明式事务的由来。不得不说声明式事务的诞生极大的方便了程序员进行事务管理,不,应该说是AOP的诞生极大地方便了程序员进行代码复用。
我们还是以一个超简单的例子作为引子。看看spring是如何实现声明式事务的。下面定义了一个CoffeeOrderService模拟保存两杯咖啡:coffee1 coffee2。就是这么简单。

@Service
public class CoffeeService {

    @Autowired
    private CoffeeRepository coffeeRepository;

    @Transactional(rollbackFor = RuntimeException.class)
    public void save(Coffee coffee1, Coffee coffee2) {
        coffeeRepository.save(coffee1);
        System.out.println(coffeeRepository.findAll());
        coffeeRepository.save(coffee2);
        System.out.println(coffeeRepository.findAll());
        throw new RuntimeException("挂了");
    }
}

我们使用@Transactional注解,就能保证save方法在执行开始时开启事务,执行结束时提交事务或者回滚事务。读者很容易想到这一定使用了spring的AOP技术。说到AOP,很自然想到此处一定是使用了cglib动态代理,因为没有接口呀。而且真正的拦截功能一定是交给了一个MethodInterceptor的实现类。下面我画了一张图,简单的阐述下spring自动配置是如何为事务管理提供的事务拦截器和如何应用这个拦截器到标记了@Transactional的类的。

image-20210513001251307

spring的套路:

  1. 通知advice并不是直接塞到ProxyFactory的,而是通过顾问advisor动态获取的。
  2. 决定这个顾问要不要干涉coffeeOrderService的代理,核心代码是下面这段,根据目标类是否可以获取TransactionAttribute 来筛选候选的顾问。而获取txAttr的首要条件就是目标方法必须得有@Transactional注解。
@Override
public boolean matches(Method method, Class<?> targetClass) {
        TransactionAttributeSource tas = getTransactionAttributeSource();
        return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}

//来自 SpringTransactionAnnotationParser
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
        AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
                element, Transactional.class, false, false);
        if (attributes != null) {
            return parseTransactionAnnotation(attributes);
        }
        else {
            return null;
        }
}

我们理清了动态代理的机制。下面看看TransactionInterceptor是如何进行事务管理的。注意:下面贴的invokeWithinTransaction的源码是删减版的。我把默认情况下不会执行到的代码给省略掉了。

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
                                             final InvocationCallback invocation) throws Throwable {

        // If the transaction attribute is null, the method is non-transactional.
        TransactionAttributeSource tas = getTransactionAttributeSource();
        final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        final TransactionManager tm = determineTransactionManager(txAttr);
        PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
        Object retVal;
        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;
}
  1. 获取事务的基本属性TransactionAttribute包括:
    image-20210513134651466
  2. 获取事务管理器,此处是默认的事务管理器,JdbcTransactionManager,因为我使用的是spring-data-jdbc,如果使用的是spring-data-jpa那就是JpaTransactionManager。事务管理器的beanDataSourceTransactionManagerAutoConfiguration这个配置类引入的。

image-20210516104547591

  1. createTransactionIfNecessary,字面意思就是如果需要就创建事务。先来看下它的返回值。TransactionInfo

image-20210516110816337

它其实是对TransactionStatusTransactionAttributeTransactionManager 等属性的进一步封装。我们先看下TransactionStatus,实际上是:DefaultTransactionStatus,它封装了事务状态。

image-20210516112110508

可以看出,它封装了数据库连接。还有保存点信息。那么问题来了。

这个连接是什么时候从连接池获取的

上面的例子中我把使用jdbc profileSQL功能将sql打印出来是这样的。

SET autocommit=0;
insert into coffee(name,price) values('拿铁',10);
select * from coffee;
insert into coffee(name,price) values('美式',20);
select * from coffee;
rollback;
SET autocommit=1;

我们看下开启事务的调用栈。

image-20210516182435604

这个obtainDataSource获取的正是tomcat-jdbc的数据源。由于此处我是第一次尝试连接数据库,所以会进行连接池的初始化。此处有几处需要特殊说明一下。

  • AbstractPlatformTransactionManager#getTransaction 调用doGetTransaction构建事务对象时会调用TransactionSynchronizationManager.getResource(obtainDataSource())获取数据库连接持有者。获取方式是从名为resourcesThreadLocal里面获取key为当前数据源的连接持有者。因为是第一次开启事务,显然是没有的。所以此处返回为null。紧接着判断是否已经开启事务时,就是根据是否包含连接持有者(ConnectionHolder)来判断的。例子中时没有开启的。所以isExistingTransaction返回为false
  • 紧接着判断隔离级别,如果是PROPAGATION_MANDATORY,那么自爆掉。因为这个隔离级别是必须有一个已经存在的事务的。此处当然不会爆掉,因为默认隔离级别是PROPAGATION_REQUIRED。这个隔离级别就会调用startTransaction
  • startTransaction调用doBegin开启事务。它的任务实际就是从数据源获取一个连接,然后设置autoCommitfalse开启事务。
savepointAllowed为什么是true

构建事务对象时,如果TransactionManagernestedTransactionAllowedtrue,则savepointAllowed也为true,而在初始化JdbcTransactionManager的时候会调用父类DataSourceTransactionManager的构造方法,将nestedTransactionAllowed设置为true
这个标记是用来决定是否支持嵌套事务。

savepoint如何支持嵌套事务的

首先回顾下spring定义的事务的7种传播行为

传播行为含义实现方式
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是默认的事务传播行为
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。事务必须存在(是否还有连接持有者),否则直接抛出异常
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。(一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。)如果已经存在事务,将原有的连接持有者挪到suspendedResources,然后重新获取连接,重新开启事务。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。挂起当前事务实际就是将当前事务的连接持有者移除到suspendedResources,恢复的时候,再从suspendedResources挪回给连接持有者
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。如果存在事务(含有连接持有者)直接抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。(外层事务抛出异常回滚,那么内层事务必须回滚,反之内层事务并不影响外层事务)详细见下文

PROPAGATION_NESTED这种传播行为就是靠savepoint实现的。AbstractPlatformTransactionManagerhandleExistingTransaction方法里,如果是嵌套事务的话,首先判断是否允许嵌套事务,默认是允许的,如果不允许,抛异常。然后创建并持有安全点,可以回顾一下mysql安全点的知识,是能能够满足嵌套事务的要求的,就是内层事务回滚不会影响外层事务。

我们再来回答上面的数据库连接借了啥时候还的问题。很显然当事务提交之后。就可以将数据库连接还回去了。下图是调用栈。事务提交之后会进行一些清理动作,释放连接就是其中一项。

image-20210517115624721

wei_xin_tail


java论剑
17 声望0 粉丝