YON CISE

Tomcat JDBC 连接池配置

isPoolSweeperEnabled

PoolSweeper 会定时检查连接池中的连接, 然后根据你的配置来处理连接. 比如关闭连接时间过长的连接, 废弃长时间没有归还的连接等等.

PoolSweeper 的开启并不是一个单独的属性决定的, 而是多个属性共同决定的. 我们可以看下 PoolProperties 类的 isPoolSweeperEnabled 方法:

public boolean isPoolSweeperEnabled() {
    boolean timer = getTimeBetweenEvictionRunsMillis()>0;
    boolean result = timer && (isRemoveAbandoned() && getRemoveAbandonedTimeout()>0);
    result = result || (timer && getSuspectTimeout()>0);
    result = result || (timer && isTestWhileIdle() && getValidationQuery()!=null);
    result = result || (timer && getMinEvictableIdleTimeMillis()>0);
    return result;
}

首先 timer 必须为 true, PoolSweeper 才会开启. (为什么不把 timer 在返回的时候和 result and 一下?)

所以你必须配置 timeBetweenEvictionRunsMillis 这个属性, 即 PoolSweeper 多久运行一次.

属性 默认值 单位 含义
timeBetweenEvictionRunsMillis 5000 毫秒 PoolSweeper 多久运行一次

timertrue, 还是不够的, PoolSweeper 每次运行的时候还需要有事情做. PoolSweeper 运行时主要检查两个方面, 1). 是否有连接泄露 2). 是否需要关闭闲置的连接

Leak

所谓的连接泄露就是说, 程序从连接池中获取连接之后, 没有将连接归还. 可能是因为忘记了, 也可能是因为异常而未正常归还. 通过配置相应的属性, 可以让 PoolSweeper 检查是否有连接泄露.

属性 默认值 单位 含义
logAbandoned false - 是否打印相关日志
suspectTimeout 0 当连接超过该时间没有归还, 则认为可能泄露. 如果 logAbandoned 开启, 会输出相应日志
removeAbandonedTimeout 60 当连接超过该时间没有归还, 则认为泄露.
removeAbandoned false - 是否关闭泄露的连接

Idle

之所以使用连接池比较高效, 就是因为不需要频繁的建立连接. 连接在连接池中有两种状态 1). busy 2). idle.

busy 说明该连接正在使用, idle 说明该连接闲置. 维持连接是需要消耗资源的, 所以对于不需要的连接应该被关闭掉以节省资源.

属性 默认值 单位 含义
testWhileIdle false - 是否检查 idle 的连接的有效性
validationInterval 3000 毫秒 在该时间内被验证过的连接不会被重复验证
validationQuery null - 执行该语句检查连接的有效性, 通常为比较简单的语句, 比如 SELECT 1
minEvictableIdleTimeMillis 60000 毫秒 连接闲置超过该时间则被认为可以关闭
minIdle 10 - 当闲置连接数量超过该值, PoolSweeper 会关闭可关闭的连接, 但不会让闲置连接数量低于该值
maxIdle 100 - 当 PoolSweeper 未开启时, 闲置连接数不会超过该值. PoolSweeper 开启时, 闲置连接数可以超过该值
maxActive 100 - 连接池最多维持的连接数量

容易理错的是 minIdlemaxIdle. minIdle 不是说连接池中必须有这么多的闲置连接, 连接池的闲置连接是可能低于这个值的, 比如连接都处于 busy 状态或者连接失效被关闭. minIdle 是指, 当闲置连接数量大于这个值时, PoolSweeper 在运行时会关闭所有可被关闭的连接 (闲置时间超过 minEvictableIdleTimeMillis), 直到闲置连接数等于 minIdle 或者没有可关闭的闲置连接.

maxIdle 要分为 PoolSweeper 开启和未开启两个情况讨论. 未开启时, 当用户归还连接, 如果此时闲置连接数量已经等于 maxIdle 了, 那么该连接会被关闭而不是放到连接池里. 开启时, 闲置的连接数量是可能大于 maxIdle 的 (但所有连接数是不会超过 maxActive 的). 这么做主要是出于性能考虑, 因为程序的使用是有高峰和低峰的, 高峰时这些多出来的闲置连接是很有可能被再次使用, 从而提高了性能. 之所以 PoolSweeper 开启时才会有这种行为, 是因为, 如果 PoolSweeper 没有开启的话, 这些多出来的连接是没有人去关闭的!

其他

属性 默认值 单位 含义
maxAge 0 毫秒 连接归还时, 如果该连接已经连接超过该时间则会被关闭
initialSize 10 - 连接池初始创建的连接数量
initSQL null - 连接创建时执行的初始化语句 (例如 SET NAMES 'utf8mb4', time_zone = '+0:00';)
testOnBorrow false - 从连接池获取连接时是否验证连接的有效性. (可设置 validationInterval, 防止频繁验证)

Configuring jdbc-pool for high-concurrency

MySQL datetime 时区问题

我觉得直接把 datetime 当成字符串会更好理些, 因为它就是一个日期时间的字符串, 没有时区信息.

比如 2016-11-16 23:00 这个日期时间表示的含义完全取决于你所在地的时区. 如果你在中国, 它就是中国时间, 你在美国, 它就是美国时间.

为什么不用 timestamp 呢? 因为 timestamp 只能表示到 2038-01-19 03:14:07 UTC (万一软件能活到那时候呢).

MySQL 端

数据中一般我们会增加字段来表示数据的元信息, 比如创建时间和修改时间. 对于这种字段, 我们一般会设置 DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 属性.

前面提到, datetime 是没有时区的, 所以 MySQL 的 CURRENT_TIMESTAMP 转换成 datetime 时是基于 MySQL 的时区的.

有时候我们不方便修改 MySQL 服务器, 可以在连接 MySQL 后执行: SET time_zone = "+0:00" 来修改当前数据库连接的时区信息. 你也可以使用 SET time_zone = "UTC" 来设置, 不过不能确保所有 MySQL 能识别这个时区信息 (腾讯云的数据库就不行).

Java 端

我在项目中使用 java.sql.Timestamp 来映射 MySQL 中的 datetime. 不过需要注意的是, JDBC 在转换 Timestampdatetime 时, 是根据 Timestamp 的时区来的 (Timestamp 继承 Date).

因为我希望项目和数据库中的日期时间相关的都使用 UTC 时区, 所以我直接把 Java 默认的时区改成 UTC 了:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

总结

datetime 就是个日期时间的字符串, 没有时区信息.

The DATE, DATETIME, and TIMESTAMP Types

Java 日志系统

日志体系

Java 日志框架有很多, 什么 JCL (Apache Commons Loggins), SLF4J (Simple Logging Facade for Java), Logback, JUL (Java Util Logging), Log4J, 十分让人摸不着头脑.

首先, JCL, SLF4J 这两个和其它的各种日志框架是要区别开来的. 他们都不能算作真正的日志框架, 就像 SLF4J 的名字里说的, 它们实现的是个 Facade 设计模式, 即门面设计模式. 当用户使用他们打印日志时, 他们内部会调用其它具体的日志框架实现 (比如 Logback, JUL) 打印日志.

为什么要这样呢? 假如你现在写的是一个 Java 的框架 (包括库), 你需要打印日志, 但是作为一个框架, 你肯定不能帮用户选择使用哪个日志框架, 所以这时候你就需要 JCL 或者 SLF4J 这种框架了, 他们让你和具体的日志框架解耦.

以后看到 SLF4J + Logback 这样的组合你就知道是什么意思了, 他们说的是使用 SLF4J 做日志门面, Logback 做具体的日志实现.

实现原理

那么 JCL 和 SLF4J 是怎么找到具体的日志框架实现的呢? 两者的实现方式有点区别, JCL 通过动态加载, SLF4J 通过 “静态绑定”.

JCL

动态加载就是使用 ClassLoader 动态加载类. 当我们使用 JCL 作为日志门面时, 我们在代码中是这样调用的:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

class Foo {
    private static final Log LOG = LogFactory.getLog(Foo.class);

    public void bar() {
        if (LOG.isInfoEnabled()) {
            LOG.info("some message");
        }
    }
}

当我们调用 LogFactory.getLogger(...) 时, JCL 会先拿 LogFactory 接口的实例:

public static Log getLog(Class clazz) throws LogConfigurationException {
    return getFactory().getInstance(clazz);
}

getFactory() 方法会按照如下顺序取得实例:

  1. 看 Java System Property 是否设置了 org.apache.commons.logging.LogFactory (值应该为实现了 LogFactory 接口的类的 binary name) 如果设置了就实例化对应的类
  2. 使用 Java SPI 找对应的类 (JCL 是通过 ClassLoader.getResourceAsStream() 来读取 META-INF/services/org.apache.commons.logging.LogFactory 文件的内容再实例化)
  3. 看 JCL 的配置文件 (commons-logging.properties) 里是否设置了 org.apache.commons.logging.LogFactory 属性, 有就实例化对应的类
  4. 如果通过上面的方式都找不到的话, 就会实例化默认的实现类 org.apache.commons.logging.impl.LogFactoryImpl

通常使用的都是默认的 LogFactoryImpl. 找到 LogFactory 后, 就会去拿 Log 接口的实例, 默认的 LogFactoryImpl 会按照如下顺序去找实例 (关键的代码在 LogFactoryImpl.discoverLogImplementation() 里):

  1. 看 JCL 配置文件里有没有设置 org.apache.commons.logging.Log
  2. 看 Java System Property 里有没有 org.apache.commons.logging.Log
  3. 尝试加载 Log4J 的包装类 org.apache.commons.logging.impl.Log4JLogger (如果没有依赖 Log4J 那么加载会失败)
  4. 尝试加载 JUL 的包装类 org.apache.commons.logging.impl.Jdk14Logger 或者 org.apache.commons.logging.impl.Jdk13LumberjackLogger (根据 JDK 版本决定)
  5. 使用默认的 org.apache.commons.logging.impl.SimpleLog

可以看到 JCL 主要使用 ClassLoader 动态加载类, 所以容易遇到 ClassLoader 相关的问题, 尤其是 JCL 早期的版本 (Taxonomy of class loader problems encountered when using Jakarta Commons Logging).

SLF4J

SLF4J 就是因为早期的 JCL 有 ClassLoader 相关的问题才出现的. SLF4J 是通过 “静态绑定” 来找到相关的类的. 所谓的 “静态绑定” 就是说, SLF4J 会在编译的时候确定使用哪个日志框架, 不能理解? 看看 SLF4J 是怎么实现的就明白了, 下面简单分析下.

如果使用 SLF4J 做日志门面, 我们会这么使用:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class Foo {
    private static final Logger LOGGER = LoggerFactory.getLogger(Foo.class);

    public void bar() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("some {}", "message");
        }
    }
}

和 JCL 类似 LoggerFactory.getLogger(...) 里会拿 ILoggerFactory 接口的实例再拿 logger. 获取 ILoggerFactory 最关键的就是 org.slf4j.impl.StaticLoggerBinder 这个类. 不过神奇的是, SLF4J 的核心包 (slf4j-api) 里没有这个类! 那这个类在哪里呢? 如果使用 SLF4J, 根据你使用的具体日志框架实现不同, 你还需要添加相应的依赖, 比如: slf4j-log4j12-1.7.21.jar, slf4j-jdk14-1.7.21.jar 等等. 在这些包里会有 org.slf4j.impl.StaticLoggerBinder 类. 这就是所谓的 “静态绑定” 了.

Spring 日志

Spring 使用 JCL + Logback 作为默认日志系统. 如果没什么特殊要求的话, 正常使用 JCL 就行了.

如果希望使用 SLF4J 的话, 那么需要添加 slf4j-apijcl-over-slf4j 依赖. jcl-over-slf4j 通过 SPI (看上面 JCL LogFactory 查找的顺序) 让 JCL 使用 SLF4JLogFactor 作为 LogFactory 的实现. 最后, 我们在项目中, 按照正常方式使用 SLF4J 就行.

总结

知道了 Java 日志系统里有个 门面层, 那么其它就很好理解了. 首先我们确定日志门面, 然后决定使用哪个具体的日志框架, 使用哪个就去它的官网查具体的怎么配置 (用 Logback 就去查 Logback, 用 Log4J 就去查 Log4J).

目前项目中使用大多数人推荐的 SLF4J + Logback 组合.

SLF4J user manual

Apache Commons Logging - User Guide

MySQL 锁相关

MySQL 有两个常见的引擎: MyISAM 和 InnoDB. 两者主要的区别有:

  MyISAM InnoDB
事务 不支持 支持
表级锁 不支持 支持
全文搜索 支持 5.6.4 以上支持
外键 不支持 支持

所以通常情况下我们都会选择 InnoDB 引擎, 除非你的数据库是大量的读而很少写才会选择 MyISAM 引擎.

InnoDB 锁

共享锁 (Share) 排它锁 (Exclusive)

InnoDB 支持两种常见的 行级锁: 共享锁和排它锁

  • 共享锁 (S): 拿到共享锁的事务可以去读取一行数据
  • 排它锁 (X): 拿到排它锁的事务可以更新和删除一行数据

假如事务 T1 在 row r 上有一个共享锁 (S), 现在事务 T2 想要在 r 上获取锁, 那么:

  • T2 可以马上获得 S 锁, 最终 T1T2r 上各有一个 S 锁
  • T2 无法立即获得一个 X 锁

如果 T1r 上有一个排它锁 (X), 那么 T2r 上无论是 S 锁还是 X 锁都无法立即获得.

SELECT ... FOR UPDATE 可以在相应的行上获得 X 锁, SELECT ... LOCK IN SHARE MODE 可以在相应的行上获得 S 锁.

需要注意的是, InnoDB 是锁在索引上的, 所以如果你的 SELECT 语句中的 WHERE 没有用到加索引的列, 那么 InnoDB 就会在所有行上加上锁, 相当于在表上加锁了.

意向锁

InnoDB 的意向锁是 表级锁 , 意向锁有两种: Intention shared (IS), Intention exclusive (IX).

当一个事务想要获得某行的 S 锁时, 它必须先获得表上 IS 锁, 当一个事务想要获得某行的 X 锁时, 它必须先获得表上 IX 锁. 所以 SELECT ... FOR UPDAGTESELECT ... LOCK IN SHARE MODE 除了在相应行上获得相应的锁, 在表上也会分别获得 IX 锁和 IS 锁.

在表级别粒度下, 锁之间的互斥关系如下:

  X IX S IS
X 互斥 互斥 互斥 互斥
IX 互斥 兼容 互斥 兼容
S 互斥 互斥 兼容 兼容
IS 互斥 兼容 兼容 兼容

上表中的 X 锁和 S 锁都是指表级别的锁 (InnoDB 的 X 锁和 S 锁都是行级别的, 所以不会冲突). 可以通过 LOCK TABLES ... READ | WRITE 获得表级别的 S 和 X 锁.

记录锁 (Record Locks)

InnoDB 对于行级锁的实现都是锁在索引上的. 如果一个表没有设置任何索引, InnoDB 会给表添加一个隐藏的索引. 加在这个索引上的锁就叫记录锁.

虽然共享锁, 排它锁, 意向锁, 记录锁以及下面要介绍的锁都属于锁的模式 (Lock Mode), 但我的理解是 共享锁, 排它锁和意向锁更偏是锁的策略, 其他的锁更偏是锁的对象.

间隙锁 (Gap Locks)

间隙锁是锁在间隙上的锁. 间隙可以位于记录与记录之间, 最小的索引之前或者最大的索引之后.

需要注意的是, 间隙锁与间隙锁之间是不会冲突的. 间隙锁只和之后会介绍的 插入意向锁 冲突. 这里的冲突是指如果间隙上已经有了间隙锁, 那么间隙上无法加上 插入意向锁. 但是反过来, 如果间隙上有 插入意向锁, 间隙锁是可以加上的.

所以间隙锁只在需要防止其它事务插入时使用, 比如在 RR 隔离级别执行 SELECT ... FOR UPDATE 时会锁住符合条件的所有间隙.

Next-Key Locks

如果同时锁住了记录锁和该记录锁之前的间隙, 那么就把这两个锁合起来称作 Next-Key Lock.

插入意向锁 (Insert Intention Locks)

该锁也是锁在间隙上的. 当执行插入语句的时候, 会在插入记录之前检查待插入记录的索引前的间隙上是否有间隙锁, 如果有就在间隙上等待插入 插入意向锁. 需要注意的事, InnoDB 在具体实现上为了效率, 并不会每次插入都加 插入意向锁, 而只会在检测到有锁冲突的时候加 插入意向锁. 如果没有冲突, 就直接插入记录. 那么 InnoDB 是怎么保证隔离性的呢? 答案就是, 隐式锁. 其他事务想要去给未提交的记录加锁, 会看记录上的事务 id 对应的事务是否活跃, 如果活跃就帮该事务加锁.

具体的 Insert 执行流程可以参考这篇文章, 写的很好: 读 MySQL 源码再看 INSERT 加锁流程

AUTO-INC Locks

这是个表级锁, 当表有自增 id 的时候会使用. 该锁的具体加锁策略和 innodb_autoinc_lock_mode 配置项有关. 该锁和一般的锁不一样, 它不是在事务结束的时候才释放的, 具体参考 AUTO_INCREMENT Handling in InnoDB

悲观锁 (Pessimistii Locking) 乐观锁 (Optimistic Locking)

因为翻译的原因, 很容易让人误以为悲观锁和乐观锁与之前提到的共享锁和排它锁是一类东西. 但英文里共享锁和排它锁的锁是名词 (Lock), 悲观锁和乐观锁的锁是动词 (Locking).

悲观锁和乐观锁是用锁的策略 (strategy).

假如我现在要先读取一个数据, 然后再修改它. 悲观锁的用锁方式是, 读取数据时就让数据库给数据加上锁 SELECT ... FOR UPDATE, 最后调用 UPDATE 修改数据. 而乐观锁的方式是先正常的读取数据 SELECT ..., 最后修改的时候判断下数据的时间戳和之前读取的时间戳一致不一致 UPDATE ... WHERE (这里的时间戳充当了锁的角色), 不一致则说明数据读取之后被修改过 (用时间戳并不是唯一的方式, 也可以用版本号).

锁的观察

虽然上面描述了很多的锁, 但是理论和实现肯定是有区别的, 具体实现的时候肯定会有很多现实面临的问题需要考虑. 所以有时候发现 InnoDB 加锁的行为出乎意料的时候, 最好有办法直接看 InnoDB 加了什么锁. MySQL 8.0 我们可以通过下面的表查看当前加的锁:

select * from performance_schema.data_locks

大部分字段都比较直白, 主要讲下 LOCK_MODE. 如果值为 X 或者 S 表示这是一个 Next-Key Lock, 如果锁住的是最后一个索引之后的间隙, 那么 LOCK_DATA 的值是 supremum pseudo-record, 你可以理解为这是一个虚拟的最大的索引. 如果是记录锁那么 LOCK_MODE 的值会是 X,REC_NOT_GAP, 间隙锁的话是 X,GAP.

事务隔离级别

Isolation Level Dirty Read Nonrepeatable Read Phantom Read
Read uncommitted Possible Possible Possible
Read committed Not possible Possible Possible
Repeatable read Not possible Not possible Possible
Serializable Not possible Not possible Not possible
  • Dirty Read (脏读): 读到其他事务未提交的数据
  • Nonrepeatable Read (不可重复读): 可不可以重复读是指, 多次读取, 同一行数据中的列数据会不会发生变化. (读到新的行不算)
  • Phantom Read (幻读): 多次读取, 会不会出现新增的行.

需要注意的是, SQL 标准中只规定了相应的隔离级别中哪些现象不可以发生, 并没有说相应的级别中这些现象一定会发生. 比如在 PostgreSQL 中, 事务在 Read Uncommitted 隔离级别下是不会出现脏读的, 同时 PostgreSQL 中事务是不会出现幻读的.

传统的隔离级别是基于锁实现的, 这种方式叫做 基于锁的并发控制 (Lock-Based Concurrent Control, 简写 LBCC). 虽然数据库的四种隔离级别通过 LBCC 技术都可以实现, 但是它最大的问题是它只实现了并发的读读, 对于并发的读写还是冲突的. 针对这种场景, MVCC (Multi-Version Concurrent Control) 技术应运而生. 具体就不多说了, 这文章写的很详细了 解决死锁之路 - 学习事务与隔离级别

InnoDB Locking

MySQL · 引擎特性 · InnoDB 事务锁系统简介

Transaction Isolation

Optimistic locking in MySQL

Lock (database)

X row locks do not prevent an IX table lock, contrary to documentation

Why doesn’t MySQL’s MyISAM engine support Foreign keys?

MyISAM versus InnoDB

解决死锁之路 - 了解常见的锁类型

解决死锁之路 - 常见 SQL 语句的加锁分析

MyBatis 记录三: 其它

基于 v3.4.1

@Results

网上关于通过注解配置都提到说, 注解配置的 ResultMap 是没法复用的, 但是我发现 @Results 中有个 id 的属性, 于是测试了下能否在注解中引用之前配置过的 @Results, 发现是可以. 看来 3.4.1 版本已经支持了.

文档

MyBatis 各版本之前还是有些差异的, 但是官网上只有最新版本的文档, 没法查旧版本的. 看了下 MyBatis 在 github 上的源码, 发现它是用 maven-site-plugin 这个 maven 插件生成文档的, 所以我们可以从 github 上 checkout 下来相应的版本, 然后自己生成下文档:

mvn site

Configuration

Mapper XML Files

Java API