HikariCP:一个叫光的JDBC连接池

news2024/11/16 9:22:21

文章目录

    • 简介
    • 数据库连接池
      • C3P0
      • DBCP
      • BoneCP
    • 精简的设计
      • 字节码优化
      • ArrayList-->FastList
      • ConcurrentBag
      • 代理实现
      • Statement Cache
      • Scheduler quanta
      • CPU缓存行失效
    • 优雅的实现
      • 获取连接
      • 初始化池对象
      • 连接池管理
        • 连接池扩充
        • 连接池缩容
        • 连接池关闭
      • ConcurrentBag
    • 连接池参数
    • 总结
    • 参考

简介

天不生我李淳罡,剑道万古如长夜。

Hikari [hi·ka·'lē] 是日语“光”的意思。HikariCP的卖点是快、简洁、可靠,整体非常轻量,只有130Kb左右。

HikariCP性能对比
HikariVsBone

HikariCP的出现可以说是颠覆了连接池领域,直接将性能做到极致,从此之后再无人可做出大的突破。就连曾经风靡一时的BoneCP也主动停止维护,让贤于他。而且,在Spring Boot 2.0中,HikariCP凭借其优越的性能取代Tomcat成为默认的数据库连接池。

BoneCP

数据库连接池

以史为鉴,可以知兴替。

连接池(Connection Pool)技术的核心思想就是:连接复用,通过建立一个数据库连接池以及一套连接使用、分配、管理策略,使得该连接池中的连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。

连接池

在不使用连接池的情况下,每次请求都需要经过如下过程:

  1. 与MySQL服务器三次握手建立TCP连接;
  2. 登录认证建立MySQL连接;
  3. 执行SQL语句获取结果;
  4. 断开与MySQL的连接;
  5. 关闭TCP连接。

使用连接池后只需要在初始化时执行一次建立TCP连接和登录认证流程,之后连接池会维护指定数量的连接资源,当有请求时直接从连接池中获取连接执行SQL即可,SQL执行完毕后归还连接给连接池,而无需关闭连接,待应用关闭时由连接池负责将申请的连接资源释放即可。

数据库连接池相关技术发展至今已非常成熟了,使用比较广泛的有BoneCP、DBCP、C3P0、Druid等。

C3P0

C3P0(Why C3P0)在很长一段时间内一直是Java领域内数据库连接池的代名词,当年盛极一时的Hibernate都将其作为内置的数据库连接池,可见业内对它还是认可的。C3P0的功能简单易用、稳定性好,但是性能上的缺点却让它最终被打入冷宫。C3P0的性能差到即便是和同时代的产品相比它也是垫底的,更不用和Druid、HikariCP相比了。正常来讲,有问题很正常,改就是了,但C3P0最致命的问题就是架构设计过于复杂,让重构变成了一项不可能完成的任务。最终,性能有硬伤的C3P0也彻底的退出了历史舞台。

DBCP

DBCP(DataBase Connection Pool)属于Apache顶级项目Commons中的核心子项目,在Apache的生态圈中的影响十分广泛,Tomcat就在其内部集成了DBCP,实现JPA规范的OpenJPA,也默认集成了DBCP。但DBCP并不是独立实现连接池功能的,它内部依赖于Commons中的另一个子项目pool。连接池最核心的“池”,就是由pool组件提供的,因此,DBCP的性能实际上就是pool的性能。但有很长一段时间pool都停留在1.x版本,导致DBCP也更新乏力,许多依赖DBCP的应用在遇到性能瓶颈后别无选择,只能将其替换掉。Tomcat就在其7.0版本中重新设计开发了一套连接池–Tomcat JDBC Pool

2013年9月Commons-Pool 2.0 版本发布,事情迎来转机,DBCP 2.0版本在2014年2月发布,基于新的线程模型全新设计的“池”让DBCP重焕青春,虽然和新一代的连接池相比仍有一定差距,但DBCP 2.x版本已经稳稳达到了和新一代产品同级别的性能指标。

然而长时间的等待已经消磨了用户的耐心,DBCP2与其他产品相比没有任何突出优势。试问,谁会在有选择的前提下,去选择那个并不优秀的呢?如果有,那也只是情怀吧。

BoneCP

在本文开头已提到过BoneCP,它是一个以高性能著称的JDBC连接池。BoneCP的高性能一是源自其极简的设计,整个产品只有几百k大小,二是其重构了内部pool的设计,减少了锁的使用。这两点优化原则,几乎适用于所有的连接池产品。

值得一提的是,BoneCP本身并不“健全”,它的很多特征都依赖于Guava,因此也和DBCP一样面临更新乏力的问题。但现在这些都不重要了,BoneCP引以为傲的性能已被HikariCP全面超越,已主动让贤于与自己设计思路类似的HikariCP。

精简的设计

有道无术,术尚可求,有术无道,止于术。

HikariCP崇尚的设计美学是极简主义,遵循KISS (Keep It Simple Stupid) 的设计哲学。

对连接池来说,要想在性能上做出改进,最直接的做法可能是优化获取连接的逻辑。然而HikariCP获取连接的逻辑和其他连接池的差别并不大,其大部分性能提升来自于对Connection,Statement 等的委托(delegates)的优化。具体包括以下几个方面。

HikariCP

字节码优化

HikariCP的作者研究了编译器的字节码输出,JIT的汇编输出,将关键链路限制在JIT内联阈值以下。并且扁平化了继承层次结构,隐藏了成员变量,消除了类型转换。

ArrayList–>FastList

减少ConnectionProxy中用来追踪Statement实例的ArrayList实例,当一个Statement被关闭的时候,必须将它从集合中移出,当集合被关闭的时候,必须迭代集合将其中打开的所有statement实例,然后才能清空集合。Java中的ArrayList在get(int index)调用时会进行边界检测,HikariCP中能保证边界,因此这个检查就是无意义的。

另外,在remove(Object)的实现中会从头到尾进行扫描,然而,在JDBC中通常会在使用完后立即关闭Statements或按打开的相反顺序进行关闭,因此从尾部开始扫描性能会更好。因此,HikariCP中用自定义的FastList来替换ArrayList,FastList不进行边界检测且删除元素时从尾部开始扫描。

ConcurrentBag

HikariCP中包含了一个自定义的无锁集合ConcurrentBag。无锁设计、ThreadLocal缓存、

Queue-stealing、hand-off优化使得ConcurrentBag具有高并发、低延迟、最小化伪共享(false-sharing)的特性。

ConcurrentBag是HikariCP连接池中存放连接的容器,属于核心内容,其具体实现我们留到后面代码分析时再进行详细说明。

代理实现

为了为 Connection、Statement 和 ResultSet 实例生成代理,HikariCP 最初使用了一个以ConnectionProxy形式维护在静态字段 (PROXY_FACTORY) 中的单例工厂。有十几个类似的方法:

public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
    return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}

用单例工厂生成的字节码如下:

    public final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
    flags: ACC_PRIVATE, ACC_FINAL
    Code:
      stack=5, locals=3, args_size=3
         0: getstatic     #59                 // Field PROXY_FACTORY:Lcom/zaxxer/hikari/proxy/ProxyFactory;
         3: aload_0
         4: aload_0
         5: getfield      #3                  // Field delegate:Ljava/sql/Connection;
         8: aload_1
         9: aload_2
        10: invokeinterface #74,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
        15: invokevirtual #69                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
        18: return

可以看到先是getstatic调用拿到静态字段PROXY_FACTORY的值,最后invokevirtual调用ProxyFactory实例中的getProxyPreparedStatement()方法。

去掉单例工厂将其替换为有着静态方法(由Javassist生成具体实现)的final类后,Java代码变为:

    public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
    {
        return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
    }

生成的字节码变为:

    private final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
    flags: ACC_PRIVATE, ACC_FINAL
    Code:
      stack=4, locals=3, args_size=3
         0: aload_0
         1: aload_0
         2: getfield      #3                  // Field delegate:Ljava/sql/Connection;
         5: aload_1
         6: aload_2
         7: invokeinterface #72,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
        12: invokestatic  #67                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
        15: areturn

两相对比可看出:

  1. getstatic调用没有了;

  2. invokevirtual调用替换成了更容易被JVM优化的invokestatic;

  3. 堆栈大小从5个元素减少到4个元素。这是因为invokevirtual会先将ProxyFactory实例压入堆栈,当调用getProxyPreparedStatement()时再将其弹出。

Statement Cache

在许多连接池中都会提供PreparedStatement缓存,而HikariCP中则去掉了这个缓存。在连接池层使用statement cache是一个反面模式 (Anti-pattern),与driver层提供的缓存相比,会对应用程序的性能产生负面影响。

在连接池层 PreparedStatements 只能缓存每个连接。如果有 250 个经常执行的查询和一个包含 20 个连接的池,则数据库需要保留 5000 个查询执行计划。同理,连接池必须缓存这么多 PreparedStatements 及其相关的对象图。

大多数主流数据库 JDBC 驱动程序(PostgreSQL、Oracle、MySQL等)已经有一个可以配置的statement缓存。 JDBC 驱动程序处于利用数据库特定功能的独特位置,几乎所有缓存实现都能够跨连接共享执行计划。这意味着这 250 个经常执行的查询会在数据库中产生恰好 250 个执行计划,而不是内存中的 5000 个语句和相关的执行计划。聪明的实现甚至不会在驱动程序级别的内存中保留 PreparedStatement 对象,而只是将新实例附加到现有计划 ID。

Scheduler quanta

当‘一次’运行的线程数超过CPU核数时,操作系统会给每个线程分配一个小的运行时间片,并在线程间切换调度(这样一次切换叫做一个quantum)。当一个时间片用完后,在调度器再给该线程分配下一个时间片之前可能要等待一段‘长’的时间,因此,一个线程要尽可能在他自己的时间片里完成,并避免锁强制它放弃自己的时间片就显得至关重要,否则就会有大的性能损耗。

CPU缓存行失效

CPU缓存行失效是除了锁之外,另一个会导致线程无法在一个quanta中完成的因素。

当线程被调度器再次唤起时,它确实获得了再次在其经常访问的数据上运行的机会,然而这些数据可能已经不再位于CPU的L1 或 L2 缓存中了,因为我们没法控制下次调度会被分配给哪个CPU核。

优雅的实现

“Simplicity is prerequisite for reliability.”
- Edsger Dijkstra

HikariCP 的源码少且精,可读性非常高。如果你想提升自己的Java多线程编程能力,可以来看看HikariCP的源码。

@startuml
participant HikariDataSource [
    =HikariDataSource
    ----
    ""Hikari池化数据源类""
]
participant HikariPool [
    =HikariPool
    ----
    ""连接池管理类""
]
participant ConcurrentBag [
    =ConcurrentBag
    ----
    ""连接对象存放类""
]
participant ProxyFactory [
    =ProxyFactory
    ----
    ""JDBC接口代理类""
]


HikariDataSource -> HikariPool: getConnection
HikariPool -> ConcurrentBag: borrow
ConcurrentBag -> HikariPool: PoolEntry对象
HikariPool -> ProxyFactory: createProxyConnection
ProxyFactory -> HikariPool: ProxyConnection
HikariPool -> HikariDataSource: Connection


@enduml

获取连接

  • HikariDataSource#getConnection
@startuml

start

if (数据源是否被关闭?) then (关闭)
  :抛异常;
  end
else
  if (fastPathPool是否为空?) then (非空) 
  :getConnection返回;
  end
  else (为空)
      if (pool是否为空?) then (为空) 
        :validate校验配置合法性;     
        :HikariPool初始化;
        :sealed=true标记当前数据源的pool已初始化;
     else (非空)
     endif
  endif

:pool.getConnection;

stop

@enduml

说明:

  1. 该方法的主体是使用 双重检查锁定模式 获取HikariPool对象,如果对象为空则进行初始化,实际getConnection逻辑在HikariPool中;

  2. 从上述流程图可知HikariCP是在第一次getConnection时对HikariPool进行的初始化,一旦启动相应配置就固化了(sealed),不允许再次修改(除非使用HikariConfigMXBean方法);

  3. HikariPool类型的对象有两个:fastPathPool和pool,如果使用的是HikariDataSource的默认构造函数则fastPathPool为null,如果使用指定配置的构造函数则pool和fastPathPool等价。二者的差别在于getConnection时pool会因为延迟初始化检查导致性能有轻微下降。

  • HikariPool#getConnection
@startuml

start

:获取连接池挂起恢复锁(suspendResumeLock);

repeat
  :connectionBag.borrow获取连接(poolEntry);
  if (poolEntry为null) then 
     :中断循环,抛超时异常;
     end
  else
      if (poolEntry被标记为evicted 或 (距上次访问时间大于alive时间 且 连接dead)) then
      :关闭连接;
      :超时时间更新:减去已消耗时间;
      else
        :记录borrow状态;
        :将连接包装为ProxyConnection返回;
        end
      endif
  endif
repeat while (timeout > 0)

:记录borrow超时状态、抛异常、释放锁;
stop

@enduml

说明:

  1. suspendResumeLock:通过对信号量进行简单封装实现了一个锁,用于控制获取连接的频率。如果isAllowPoolSuspension配置为true则创建一个最大并发量10000的Semaphore,否则给一个假的锁实现。

  2. 获取poolEntry后判断其是否可用时,如果连接没有被标记为evicted,则会进行连接判活,判断连接距上次访问的时间间隔是否大于500ms(默认值,可配置),如果大于则继续判断连接是否dead。从中可看出HikariCP的连接判活还是挺频繁的,这说明连接判活对其性能影响并不大,这也是得益于其无锁实现。

  3. isConnectionDead方法用于判断连接是否已失活,需要注意的是这里的连接不是PoolEntry对象,而是其持有的实际驱动的Connection对象。如果支持jdbc4协议,则执行驱动程序的isValid判断,否则执行开销较大的createStatement+execute的逻辑。比较取巧的是HikariCP是通过connectionTestQuery是否为null来判断是否支持jdbc4协议的。因此,如果是jdbc4驱动的话不要配置connectionTestQuery,这个配置只是给旧的不支持Connection.isValid的版本使用的

  4. HikariPool中维护了一个ConcurrentBag,里面存放着连接对象PoolEntry,getConnection逻辑就是从ConcurrentBag中borrow一个可用的连接PoolEntry。

初始化池对象

public HikariPool(final HikariConfig config)
   {
      super(config); // 继承自PoolBase,基本属性设置
      // initializeDataSource 底层DataSource设置,jdbcUrl、username、password等

      this.connectionBag = new ConcurrentBag<>(this); // 真正存放连接的对象,核心!
      this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;

      this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
      // 初始化一个ScheduledExecutorService,用于Housekeeping:
      // 连接泄露检测、定时清理闲置连接、延迟关闭连接 

      checkFailFast(); // initializationFailTimeout>0 则进行DB连通性检测

      if (config.getMetricsTrackerFactory() != null) {
         setMetricsTrackerFactory(config.getMetricsTrackerFactory());
      }
      else {
         setMetricRegistry(config.getMetricRegistry());
      }

      setHealthCheckRegistry(config.getHealthCheckRegistry());

      handleMBeans(this, true);

      ThreadFactory threadFactory = config.getThreadFactory();

      final int maxPoolSize = config.getMaximumPoolSize();
      LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize);

      // 用于添加连接的线程池,核心和最大线程数为1,队列为addConnectionQueue
      this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new CustomDiscardPolicy());
      // 用于异步关闭物理连接的线程池,核心和最大线程数为1,maxPoolSize大小的LinkedBlockingQueue
      this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
      // 初始化连接泄露告警器
      this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
      // 定时任务,30s调度一次HouseKeeper,用于淘汰连接、维护最小连接数
      this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

      if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) {
         addConnectionExecutor.setMaximumPoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));
         addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));

         final long startTime = currentTime();
         while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) {
            quietlySleep(MILLISECONDS.toMillis(100));
         }

         addConnectionExecutor.setCorePoolSize(1);
         addConnectionExecutor.setMaximumPoolSize(1);
      }
   }

说明:

  1. 上述主体逻辑为初始化连接池属性、DataSource属性、初始化存放连接的ConcurrentBag、初始化用于管理连接的线程池和调度任务、初始化监控器。

  2. 如果设置了初始化超时时间,则会进行联通检测/连接池预热,具体流程为创建一个连接对象PoolEntry,放到连接池的connectionBag中,代码见checkFailFast。

连接池管理

HikariPool负责对连接池的管理,包括连接池扩充、连接池缩容、连接池关闭等。

hikaricp_overview

连接池扩充

连接池在淘汰连接、关闭连接后,以及连接池从挂起变为启用之前,会进行连接池扩充,以保证minimumIdle。

   private synchronized void fillPool(final boolean isAfterAdd)
   {
      // 获取空闲连接数:统计connectionBag中状态为STATE_NOT_IN_USE的连接数
      final var idle = getIdleConnections();
      final var shouldAdd = getTotalConnections() < config.getMaximumPoolSize() && idle < config.getMinimumIdle();

      if (shouldAdd) {
         final var countToAdd = config.getMinimumIdle() - idle;
         for (int i = 0; i < countToAdd; i++)
            // 异步添加连接,postFillPoolEntryCreator与poolEntryCreator的区别只是日志前缀
            // addConnectionExecutor 核心和最大线程数为1
            addConnectionExecutor.submit(isAfterAdd ? postFillPoolEntryCreator : poolEntryCreator);
      }
      else if (isAfterAdd) {
         logger.debug("{} - Fill pool skipped, pool has sufficient level or currently being filled.", poolName);
      }
   }

   // PoolEntryCreator的核心逻辑是创建连接createPoolEntry,并将其加到connectionBag中
   // 创建链接对象的逻辑如下
   private PoolEntry createPoolEntry()
   {
      try {
         // 从DataSource获取物理连接,设置到poolEntry
         final var poolEntry = newPoolEntry();

         // maxLifetime>0 houseKeepingExecutorService延迟执行MaxLifetimeTask任务
         final var maxLifetime = config.getMaxLifetime();
         if (maxLifetime > 0) {
            // variance up to 2.5% of the maxlifetime
            final var variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0;
            final var lifetime = maxLifetime - variance;
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(new MaxLifetimeTask(poolEntry), lifetime, MILLISECONDS));
         }

         // houseKeepingExecutorService定期执行KeepaliveTask
         final long keepaliveTime = config.getKeepaliveTime();
         if (keepaliveTime > 0) {
            // variance up to 10% of the heartbeat time
            final var variance = ThreadLocalRandom.current().nextLong(keepaliveTime / 10);
            final var heartbeatTime = keepaliveTime - variance;
            poolEntry.setKeepalive(houseKeepingExecutorService.scheduleWithFixedDelay(new KeepaliveTask(poolEntry), heartbeatTime, heartbeatTime, MILLISECONDS));
         }

         return poolEntry;
      }
      catch (ConnectionSetupException e) {
         if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            logger.error("{} - Error thrown while acquiring connection from data source", poolName, e.getCause());
            lastConnectionFailure.set(e);
         }
      }
      catch (Exception e) {
         if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            logger.debug("{} - Cannot acquire connection from data source", poolName, e);
         }
      }

      return null;
   }
  • MaxLifetimeTask 与 KeepaliveTask
   // 当连接poolEntry达到设置的maxLifetime时,执行任务将其从连接池中剔除
   private final class MaxLifetimeTask implements Runnable
   {
      private final PoolEntry poolEntry;

      MaxLifetimeTask(final PoolEntry poolEntry)
      {
         this.poolEntry = poolEntry;
      }

      public void run()
      {
         if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
            // 旧连接被剔除后,判断是否需要添加新连接
            addBagItem(connectionBag.getWaitingThreadCount());
         }
      }
   }

   @Override
   public void addBagItem(final int waiting)
   {
      // 当等待线程数大于添加连接的queue size时,增加一个添加连接的任务
      // 本来需要waiting个连接,现在可用连接又被淘汰了一个,需要再添加一个
      if (waiting > addConnectionExecutor.getQueue().size())
         addConnectionExecutor.submit(poolEntryCreator);
   }

   // 定时执行连接保活任务
   private final class KeepaliveTask implements Runnable
   {
      private final PoolEntry poolEntry;

      KeepaliveTask(final PoolEntry poolEntry)
      {
         this.poolEntry = poolEntry;
      }

      public void run()
      {
         // 将连接置为reserved,如果物理连接可用再将连接置为可用
         // 如果物理连接不可用,则将该连接淘汰,并按需要添加新连接
         if (connectionBag.reserve(poolEntry)) {
            if (isConnectionDead(poolEntry.connection)) {
               softEvictConnection(poolEntry, DEAD_CONNECTION_MESSAGE, true);
               addBagItem(connectionBag.getWaitingThreadCount());
            }
            else {
               connectionBag.unreserve(poolEntry);
               logger.debug("{} - keepalive: connection {} is alive", poolName, poolEntry.connection);
            }
         }
      }
   }

连接池缩容

在连接池初始化时,会初始化一个默认30s执行一次的houseKeeperTask,用于定期将连接池中失效的连接清理掉,同时保持要求的最小空闲链接数。

// HouseKeeper.run()
            /**
             * 时间回拨:由于网络时间校准或人工设置,导致系统时间跳回到过去的某个时间
             * 时间回拨检测,如果时间回退到过去则淘汰所有连接
             * 如果时间往前拨,则不做处理,因为这除了会加速连接被淘汰外无其他影响
             */   
            // Detect retrograde time, allowing +128ms as per NTP spec.
            if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs)) {
               logger.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
                           poolName, elapsedDisplayString(previous, now));
               previous = now;
               softEvictConnections();
               return; // 发生时间回拨时任务中断执行,不再执行后续清理和扩充逻辑
            }
            else if (now > plusMillis(previous, (3 * housekeepingPeriodMs) / 2)) {
               // No point evicting for forward clock motion, this merely accelerates connection retirement anyway
               logger.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
            }

            previous = now;

            if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
               logPoolState("Before cleanup ");

               // 取出所有空闲连接,淘汰其中已超时连接
               final var notInUse = connectionBag.values(STATE_NOT_IN_USE);
               var maxToRemove = notInUse.size() - config.getMinimumIdle();
               for (PoolEntry entry : notInUse) {
                  if (maxToRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
                     closeConnection(entry, "(connection has passed idleTimeout)");
                     maxToRemove--;
                  }
               }
               logPoolState("After cleanup  ");
            }
            else
               logPoolState("Pool ");

            // 可能淘汰了一些连接,因此需要触发连接池扩充流程
            fillPool(true); // Try to maintain minimum connections
  • 淘汰连接
   // 淘汰连接:撤销连接泄露报警任务,软淘汰连接
   public void evictConnection(Connection connection)
   {
      var proxyConnection = (ProxyConnection) connection;
      proxyConnection.cancelLeakTask();

      try {
         softEvictConnection(proxyConnection.getPoolEntry(), "(connection evicted by user)", !connection.isClosed() /* owner */);
      }
      catch (SQLException e) {
         // unreachable in HikariCP, but we're still forced to catch it
      }
   }

   // 软淘汰一个连接,将连接标记为evict
   // 如果是连接的owner 或者 连接是空闲的(可以被置为reserved),则进一步关闭连接
   private boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner)
   {
      poolEntry.markEvicted();
      if (owner || connectionBag.reserve(poolEntry)) {
         closeConnection(poolEntry, reason);
         return true;
      }

      return false;
   }

   // 关闭连接
   void closeConnection(final PoolEntry poolEntry, final String closureReason)
   {
      // 将待关闭连接poolEntry从connectionBag移除
      if (connectionBag.remove(poolEntry)) {
         // 关闭poolEntry连接:endOfLife、keepalive、connection置为null
         final var connection = poolEntry.close();
         // 线程池异步关闭物理连接
         closeConnectionExecutor.execute(() -> {
            quietlyCloseConnection(connection, closureReason);
            if (poolState == POOL_NORMAL) {
               fillPool(false); // 关闭连接后需要扩充连接池保证最小连接数
            }
         });
      }
   }

连接池关闭

   // 关闭pool,关闭所有空闲连接,关闭活动连接
   public synchronized void shutdown() throws InterruptedException
   {
      try {
         poolState = POOL_SHUTDOWN; // 关闭连接池

         if (addConnectionExecutor == null) { // pool never started
            return;
         }

         logPoolState("Before shutdown ");

         // 关闭houseKeeperTask,都要关了,没必要维护最小空闲连接了,直接在后面关闭所有连接即可
         if (houseKeeperTask != null) {
            houseKeeperTask.cancel(false);
            houseKeeperTask = null;
         }

         // 关闭所有空闲连接,对connectionBag中的每个连接执行softEvictConnection逻辑
         // owner参数为false,所以只有实际是空闲的才会执行closeConnection逻辑,否则只是标记为evicted
         softEvictConnections();

         // 关闭添加连接线程池
         addConnectionExecutor.shutdown();
         if (!addConnectionExecutor.awaitTermination(getLoginTimeout(), SECONDS)) {
            logger.warn("Timed-out waiting for add connection executor to shutdown");
         }

         // 关闭HouseKeeping线程池
         destroyHouseKeepingExecutorService();

         // 关闭存放连接的connectionBag,closed=true
         connectionBag.close();

         // 派一组刺客杀掉所有连接,形象的名字:)
         final var assassinExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection assassinator",
                                                                           config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
         try {
            final var start = currentTime();
            do {
               // 干掉所有活着的连接:关闭poolEntry、中止物理连接、从connectionBag中移除poolEntry
               abortActiveConnections(assassinExecutor);
               // 再次关闭所有空闲连接
               softEvictConnections();
               // 给10s时间赶尽杀绝
            } while (getTotalConnections() > 0 && elapsedMillis(start) < SECONDS.toMillis(10));
         }
         finally {
            assassinExecutor.shutdown(); // 关闭刺客组织
            if (!assassinExecutor.awaitTermination(10L, SECONDS)) {
               logger.warn("Timed-out waiting for connection assassin to shutdown");
            }
         }

         shutdownNetworkTimeoutExecutor();
         closeConnectionExecutor.shutdown();
         if (!closeConnectionExecutor.awaitTermination(10L, SECONDS)) {
            logger.warn("Timed-out waiting for close connection executor to shutdown");
         }
      }
      finally {
         logPoolState("After shutdown ");
         handleMBeans(this, false);
         metricsTracker.close();
      }
   }

ConcurrentBag

ConcurrentBag是一个专为连接池设计的并发包,它比LinkedBlockingQueue 和 LinkedTransferQueue有着更好的性能。它使用ThreadLocal来尽可能避免锁定,但当ThreadLocal列表中没有可用元素时也会去扫描公共集合。也就是说ThreadLocal列表中未使用的元素可以被其他borrowing线程窃取。

需要注意的是从ConcurrentBag中borrow出去的元素实际上并没有从容器中移除,也就是说即使没有引用也不会发生GC,因此必须注意对借出的对象进行requite,否则会导致内存泄露。只有remove方法才能将对象从ConcurrentBag中完全移除。

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable
{
   private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class);

   // 存放连接PoolEntry的地方
   // CopyOnWriteArrayList线程安全,读操作无锁,写操作通过创建新副本实现
   // 读写分离,适用于读多写少场景
   private final CopyOnWriteArrayList<T> sharedList;
   // 是否使用WeakReference?默认false
   private final boolean weakThreadLocals;

   // 线程维度的缓存,从sharedList中拿到的连接会缓存到线程里,borrow时先从缓存里
   private final ThreadLocal<List<Object>> threadList;
   // 内部接口,HikariPool实现了该接口,用于添加连接,即sharedList添加元素
   private final IBagStateListener listener;
   // 当前获取不到连接而发生阻塞的线程数
   private final AtomicInteger waiters;
   private volatile boolean closed;
   // 0容量的同步队列,即产即销,用于做信息传递。
   // 不能insert元素除非有另一个线程正在尝试remove它。
   // 公平模式:先进先出queue,非公平模式:后进先出stack
   private final SynchronousQueue<T> handoffQueue;
   // 内部接口,PoolEntry实现了该接口,用于控制连接的状态,通过状态标记实现了无锁设计
   public interface IConcurrentBagEntry
   {
      int STATE_NOT_IN_USE = 0;
      int STATE_IN_USE = 1;
      int STATE_REMOVED = -1;
      int STATE_RESERVED = -2;

      boolean compareAndSet(int expectState, int newState);
      void setState(int newState);
      int getState();
   }

   // 省略...
}
  • borrow

    borrow用于在指定时间内从ConcurrentBag中获取一个连接,如果超时未拿到则返回null。会先从缓存ThreadLocal中拿,无可用连接时再从公共sharedList中拿,sharedList也无可用连接,则需要新增连接。

        public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
       {
          // Try the thread-local list first
          final var list = threadList.get();
          // 从后往前判断,因为回收连接时add操作会将刚回收的放在末尾,尾部拿到空闲连接的概率大
          for (int i = list.size() - 1; i >= 0; i--) {
             final var entry = list.remove(i); // 先remove,回收时再add进来
             @SuppressWarnings("unchecked")
             final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
             if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                return bagEntry;
             }
          }
    
          // Otherwise, scan the shared list ... then poll the handoff queue
          final int waiting = waiters.incrementAndGet(); // threadList没拿到连接waiters就会加1
          try {
             for (T bagEntry : sharedList) {
                if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                   // If we may have stolen another waiter's connection, request another bag add.
                   if (waiting > 1) { 
                      // 有阻塞线程在等着获取连接,更新添加连接的队列大小为waiting-1
                      listener.addBagItem(waiting - 1); // 当前线程已获取连接,所以要-1
                   }
                   return bagEntry;
                }
             }
    
             // 当前线程未获取连接,等待添加的连接数仍然是waiting
             listener.addBagItem(waiting);
             // 到这里才开始算timeout,也就是timeout控制的是新增连接线程池添加连接的超时时间
             // 不包括从ThreadLocal缓存和sharedList中获取连接的耗时,但注意while循环条件判断时做了10ms的偏移
             timeout = timeUnit.toNanos(timeout);
             do {
                final var start = currentTime();
                final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
                if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                   return bagEntry;
                }
    
                timeout -= elapsedNanos(start);
             } while (timeout > 10_000);
    
             return null; // 仍然没拿到连接,返回null,HikariPool抛超时异常
          }
          finally {
             waiters.decrementAndGet(); // 当前请求处理结束,waiters-1
          }
       }
    
  • add

    添加一个新的连接对象到ConcurrentBag中,给其他等待创建连接的线程borrow。addConnectionExecutor异步添加连接时会调用该方法。

    handoffQueue起到线程间通信的作用,offer只有在有另一个线程等着接收时才能将元素给出去,即产即销保证borrow线程能及时拿到需要的连接。

       public void add(final T bagEntry)
       {
          if (closed) {
             LOGGER.info("ConcurrentBag has been closed, ignoring add()");
             throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
          }
    
          sharedList.add(bagEntry); // 添加到公共sharedList里
    
          // spin until a thread takes it or none are waiting
          // 自旋等待直到有线程来获取连接或者已没有等待线程
          // while+Thread.yield 是一种常见的实现自旋的写法,在开源框架中常见其身影
          while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {
             // 自旋等待让出CPU,让同优先级或更高优先级的线程先执行,提高cpu利用率
             Thread.yield();
          }
       }
    
  • requite

    getConnection时会先将物理连接包装为PoolEntry放进ConcurrentBag,获取连接时从ConcurrentBag中取出后会再包装为ProxyConnection对象给使用方,使用方使用完后会触发ProxyConnection中的close方法回收连接。

    ProxyConnection.close 方法会触发PoolEntry.recycle 方法将poolEntry归还给pool,会调用HikariPool.recycle方法,该方法实际执行的是ConcurrentBag.requite方法,requite方法负责将用完的连接重置为可用状态,达到连接复用的目的。

    如果被borrow的连接没有被requite则会导致内存泄露。

       public void requite(final T bagEntry)
       {
          bagEntry.setState(STATE_NOT_IN_USE); // 将连接状态置为空闲
    
          // 如果存在等待线程,将归还的连接直接通过handoffQueue传递给borrow使用
          // borrow时threadList中没有waiters会+1
          for (var i = 0; waiters.get() > 0; i++) {
             if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
                return;
             }
             // 每255次进行一次10ms的小憩,防止并发过高waiters过多导致cpu占用过高
             else if ((i & 0xff) == 0xff) { 
                // LockSupport.parkNanos挂起当前线程不再被调度,
                // 直到超时或有其他线程调用unpark释放该线程的许可(HikariCP中没有调用unpark)
                parkNanos(MICROSECONDS.toNanos(10)); 
             }
             else { // 当前循环没有线程poll连接,让出CPU,等下一个循环
                Thread.yield();
             }
          }
          // 没有waiters且没有线程使用该连接,则将该空闲连接放到threadLocalList尾部
          final var threadLocalList = threadList.get();
          if (threadLocalList.size() < 50) {
             threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
          }
       }
    

    注意:add中while+yield与requite中for+yield+parkNanos的区别 #1305

    • requite中的连接(bagEntry)在自旋时可能改变状态,这是因为其引用可能被其他线程的ThreadLocal所持有,因此在自旋时必须检查连接状态。而这种状态改变是不可能发生在add中的,因为add时不会有其他线程拥有这个新连接的引用。

    • 对于yield和yield+parkNanos,循环中只有yield可能导致一个CPU核被打到100%,在requite中可能有很多线程,需要偶尔调用parkNanos以防跨多核使用CPU。在add时因为添加连接是一个单线程,所以影响并不大。

  • remove

    从connectionBag中移除连接,只有在要关闭对应物理连接时才会执行该方法。

       public boolean remove(final T bagEntry)
       {
          // 只有borrow出去的或reserve的连接能被removed
          // 通过CAS将状态置为REMOVED
          if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
             LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
             return false;
          }
    
          // 从sharedList中移出元素,CopyOnWriteArrayList.remove
          final boolean removed = sharedList.remove(bagEntry);
          if (!removed && !closed) {
             LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
          }
    
          // 从ThreadLocal中移出元素
          threadList.get().remove(bagEntry);
    
          return removed;
       }
    
  • reserve & unreserve

       // reserve用于使连接不可被borrow,主要用于清理空闲连接时使用
       public boolean reserve(final T bagEntry)
       {
          // 只能将空闲连接置为reserved
          return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
       }
    
       // unreserve用于KeepaliveTask,poolEntry会先reserve
       // 如果物理连接仍然可用,则通过unreserve再将其变为空闲连接
       public void unreserve(final T bagEntry)
       {
          if (bagEntry.compareAndSet(STATE_RESERVED, STATE_NOT_IN_USE)) {
             // spin until a thread takes it or none are waiting
             while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
                Thread.yield();
             }
          }
          else {
             LOGGER.warn("Attempt to relinquish an object to the bag that was not reserved: {}", bagEntry);
          }
       }
    
  • 状态流转
    hikaricp_state

连接池参数

  • 常用参数
参数名参数说明默认值参考值
minimumIdle连接池空闲连接最小数量。105
maximumPoolSize最大连接数。1010
connectionTimeout如果不能获取可用连接,抛出异常前的等待时间。300006000
idleTimeout空闲连接可被清理的超时时长。真正清理是在守护线程(默认每30s一次)执行时进行。不小于10000600000600000
maxLifetime连接的生命时长(毫秒),超时而且没被使用则被释放,不小于3000018000001800000
leakDetectionThreshold连接出池超时记录时长。默认为0关闭泄露检测,如果要开启需>20000300000
validationTimeout检验连接存活超时时长,如果断开不能重连,可以尝试调这个值50005000
dataSource.cachePrepStmts缓存PreparedStatementfalsefalse
dataSource.prepStmtCacheSizePreparedStatement缓存大小2525
dataSource.prepStmtCacheSqlLimitdriver缓存的statement 最大长度256256
dataSource.useServerPrepStmts新版MySQL支持服务端prepared statements,提升性能falsefalse
connectionTestQuerydriver支持JDBC4,强烈建议不设置

注:所有时间参数单位都是毫秒;minimumIdle、maximumPoolSize按场景调整。

  • 连接池不建议设置太大,主要有如下几点原因:
  1. 连接池内的连接是复用的;
  2. 对于分库分表数据源,对每个库都会维持一份连接池;
  3. 对于读写分离的数据源,对每个库会分别维持一份读连接池、一份写连接池,相当于每个库的连接数都翻倍了;
  4. 需要考虑服务所在机器的承受能力,比如一个服务有32个库,每个库连接池最大连接数是100,则单个服务实例最大会有3200个数据库连接,也即仅数据库连接就占用了3200个线程;
  5. 需要考虑数据库服务端能够承受的最大连接数,比如一个服务设置连接池最大连接数是100,如果服务部署了200台,则数据库服务端单个库的连接数最大将达到20000个,数据库服务端可能会被打挂。

总结

本文介绍了一个牛气哄哄的命名为光的数据库连接池:HikariCP。先简单回顾了数据库连接池的发展,然后重点针对HikariCP的设计哲学和代码实现进行深入学习,在如诗一般的代码中领略大师的智慧和思想,受益匪浅。

参考

  1. 为什么需要数据库连接池_MySQL_赖猫_InfoQ写作社区

  2. 源码详解系列(八)–全面讲解HikariCP的使用和源码

  3. 终于理解Spring Boot 为什么青睐HikariCP了,图解的太透彻了!

  4. 伪共享(false sharing),并发编程无声的性能杀手 - cyfonly - 博客园

  5. 【并发编程】不存储元素的同步阻塞队列SynchronousQueue

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/623596.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络协议分析:网络性能的防御工具

作为网络管理员知道管理不断发展的 IT 环境需要付出巨大的努力。无论是对于小型还是大型企业&#xff0c;管理网络以使其可访问并使其性能有效都需要一套监控策略和工具。大多数 IT 管理员需要协议分析器来识别潜在的网络风险并帮助排除故障。与传统分析不同&#xff0c;协议分…

PPT中彩虹线-变色线是怎么画出来的?

​ 效果 上面用箭头指出的线框处,各位可以看到这种有多种颜色组成的渐变的就叫彩虹线 彩虹线是怎么设置的? 请看下面的操作步骤 此处,请单击选中你要变色的线,然后我们点击鼠标右键,在弹出的菜单中选择“设置形状格式" ​ 然后你会在PPT右边得到这样的一个界面…

脑机接口 | 面向步态神经电生理研究的非人灵长类模型与系统

近期&#xff0c;海南大学生物医学工程学院脑机芯片神经工程团队在Frontiers in Neuroscience期刊上发表了题为《面向步态&神经电生理研究的非人灵长类模型与系统》的学术论文。海南大学生物医学工程学院梁丰研副教授为第一作者&#xff0c;殷明教授为通讯作者。海南大学为…

2023年上半年软件设计师试题及答案解析

请点击↑关注、收藏&#xff0c;本博客免费为你获取精彩知识分享&#xff01;有惊喜哟&#xff01;&#xff01; 计算机中&#xff0c;系统总线用于 &#xff08;1&#xff09; 连接。 &#xff08;1&#xff09; A. 接口和外设 B. 运算器、控制器和寄存器 C. CPU、主存及…

经典面试题---【第一档】

1.如果你想new一个Quene&#xff0c;你有几种方式&#xff1f;他们之间的区别是什么&#xff1f; 2.Redis 是如何判断数据是否过期的呢&#xff1f; Redis 通过一个叫做过期字典&#xff08;可以看作是 hash 表&#xff09;来保存数据过期的时间。过期字典的键指向 Redis 数据…

R语言脚本:关于 TissueEnrich包 得到的组织特异性基因富集结果的进一步处理

1. 说明 (来自官方文档)&#xff1a; The TissueEnrich package is used to calculate enrichment of tissue-specific genes in a set of input genes. Tissue-specific genes were defined by processing RNA-Seq data from the Human Protein Atlas (HPA) (Uhln et al. 2015…

HttpServlet概述

HTTP协议包括: 请求协议:浏览器向WEB服务器发送数据的时候&#xff0c;这个发送的数据需要遵循一套标准&#xff0c;这套标准中规定了发送的数据具体格式。 相应协议:WEB服务器向浏览器发送数据的时候&#xff0c;这个发送的数据需要遵循一套标准&#xff0c;这套标准中规定了发…

【2023年首次更新】MyEclipse v2023.1支持Java 20

MyEclipse让您在开发过程中不受技术约束&#xff0c;不断创新帮您找到关键技术的解决方案您能在这里得到Java EE开发所需要的一切支持&#xff01; MyEclipse v2023.1官方正式版下载 更新日志如下&#xff1a; MyEclipse官方近期更新了2023年第一个版本——v2023.1&#xff…

Nat.Commun. : 新的硬件将扩大量子计算机的工业应用规模

光子盒研究院 由明尼苏达大学双城分校领导的一个团队开发了一种新的超导二极管——这是电子设备中的一个关键部件&#xff0c;可以帮助扩大量子计算机的工业使用规模&#xff0c;并提高人工智能系统的性能。与其他超导二极管相比&#xff0c;研究人员的装置更加节能、可以同时处…

看过才知道,这套SpringCloudAlibaba笔记,把微服务玩的出神入化!

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件&#xff0c;依托Spring Cloud Alibaba&#xff0c;只需要添加一些注解和少量配置&#xff0c;就可以将Spring Cloud 应用接入阿里微服务解决方案&#xff0c;通过阿里中…

pwn(2)-栈溢出下

32位shellcode编写 不同内核态操作通过给寄存器设置不同的值&#xff0c;在调用指令int 80h&#xff0c;就可以通知内核完成不同的功能。 只要我们通过特定的汇编代码把特定的寄存器设定为特定的值后&#xff0c;在调用int 80h执行sys_execve(“/bin/sh”,NULL,NULL)就可以获…

Python获取链家二手房源数据信息

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 环境使用: Python 3.8 Pycharm 模块使用: requests >>> pip install requests 数据请求模块 parsel >>> pip install parsel 数据解析模块 csv 内置模块 &#x1f447; &#x1f447; &#x1…

OJ#203.身高排序

题目描述 ​ 海贼小学为了强健学生的身体&#xff0c;每天课间都要组织学生在户外学做广播体操。​ 这一天&#xff0c;五年级三班的所有同学在老师的指引下将队形排成了 M行 N 列。 现已知所有同学 的身高&#xff0c;数值为整数&#xff0c;单位&#xff1a;厘米。要求在所有…

Ansible从入门到精通【五】

大家好&#xff0c;我是早九晚十二&#xff0c;目前是做运维相关的工作。写博客是为了积累&#xff0c;希望大家一起进步&#xff01; 我的主页&#xff1a;早九晚十二 专栏名称&#xff1a;Ansible从入门到精通 立志成为ansible大佬 ansible-playbook企业级实战--handler hand…

爬虫基本的编码基础知识

爬虫的编码基础知识包括以下几个方面&#xff1a; 网络请求&#xff1a;使用Python中的requests库或urllib库发送HTTP请求&#xff0c;获取网页内容。 解析网页&#xff1a;使用Python中的BeautifulSoup库或lxml库解析HTML或XML格式的网页内容&#xff0c;提取所需的数据。 数…

如何开发视频上传和播放功能时,既省钱又体验好?

前言 现如今&#xff0c;大部分带内容的网站或应用都有视频区了&#xff0c;不说是大厂平台&#xff0c;就连个人开发者也相继在自己网站或小程序上迭代出视频板块。那既然有了视频模块&#xff0c;除个性化推荐&#xff0c;智能审核等这种费钱又耗时的功能外(个人开发者暂缓)。…

软件测试金融测试岗面试热点问题

1、网上银行转账是怎么测的&#xff0c;设计一下测试用例。 回答思路&#xff1a; 宏观上可以从质量模型&#xff08;万能公式&#xff09;来考虑&#xff0c;重点需要测试转账的功能、性能与安全性。设计测试用例可以使用场景法为主&#xff0c;先列出转账的基本流和备选流。…

Hive Code2报错排查

前言 大多数可能的code2报错一般是内存不够&#xff0c;所以加下面这个配置可以有效解决这个问题 set hive.auto.convert.join false; #取消小表加载至内存中 但这个不一定是因为内存不够&#xff0c;其实很多错误都是报这种官方错误的&#xff0c;所以一定要去yarn上看日志。…

如何解决vcruntime140.dll找不到的问题?两种方法教你解决

当你在运行某些应用程序或游戏时&#xff0c;可能会遇到一个错误提示&#xff0c;即“找不到vcruntime140.dll”文件。这是因为你的电脑中缺少了这个动态链接库文件&#xff0c;这个问题可能会导致你无法正常使用某些应用程序。在本文中&#xff0c;我们将介绍两种方法来解决 …

Vue3.0快速入门(速查)

Vue也是基于状态改变渲染页面&#xff0c;Vue相对于React要好上手一点。有两种使用Vue的方式&#xff0c;可以直接导入CDN&#xff0c;也可以直接使用CLI创建项目&#xff0c;我们先使用CDN导入&#xff0c;学一些Vue的基本概念。 <!-- 开发环境版本&#xff0c;包含了有帮…