乐趣区

关于spring:熟练掌握spring框架第五篇

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

spring 数据源主动配置

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

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

DataSourceProperties实现了 InitializingBean,它的afterPropertiesSet 办法会依据以后加载的类主动判断内嵌数据库类型。

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里,看下它的调用栈。

随着第一次从数据源中获取 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 秒之后还是拿不到连贯呢。那只能自曝了。

连贯长时间不必会敞开吗

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 客户端推送一个告诉。如果本人写代码实现客户端的话,就能够邮件告警啦。

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 的类的。

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 包含:
  2. 获取 事务管理器 ,此处是默认的事务管理器,JdbcTransactionManager,因为我应用的是spring-data-jdbc,如果应用的是spring-data-jpa 那就是 JpaTransactionManager。事务管理器的beanDataSourceTransactionManagerAutoConfiguration这个配置类引入的。

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

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

能够看出,它封装了数据库连贯。还有 保留点 信息。那么问题来了。

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

下面的例子中我把应用 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;

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

这个 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 平安点的常识,是能可能满足嵌套事务的要求的,就是内层事务回滚不会影响外层事务。

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

退出移动版