Java 应用中的数据库相关组件
网络协议:客户端通过标准 MySQL 协议 和 TiDB 进行网络交互。 JDBC API 及实现:Java 应用通常使用 JDBC (Java Database Connectivity) 来访问数据库。JDBC 定义了访问数据库 API,而 JDBC 实现完成标准 API 到 MySQL 协议的转换,常见的 JDBC 实现是 MySQL Connector/J,此外有些用户可能使用 MariaDB Connector/J。 数据库连接池:为了避免每次创建连接,通常应用会选择使用数据库连接池来复用连接,JDBCDataSource 定义了连接池 API,开发者可根据实际需求选择使用某种开源连接池实现。 数据访问框架:应用通常选择通过数据访问框架(MyBatis、Hibernate)的封装来进一步简化和管理数据库访问操作。 业务实现:业务逻辑控制着何时发送和发送什么指令到数据库,其中有些业务会使用 Spring Transaction 切面来控制管理事务的开始和提交逻辑。
JDBC 1. JDBC API?
在客户端替换后以文本形式发送到客户端,所以除了要使用 Prepare API,还需要在 JDBC 连接参数中配置 useServerPrepStmts = true,才能在 TiDB 服务器端进行语句预处理(下面参数配置章节有详细介绍)。
注意: 对于 MySQL Connector/J 实现,默认 Batch 只是将多次 addBatch
的 SQL 发送时机延迟到调用executeBatch
的时候,但实际网络发送还是会一条条的发送,通常不会降低与数据库服务器的网络交互次数。如果希望 Batch 网络发送批量插入,需要在 JDBC 连接参数中配置 rewriteBatchedStatements=true
(下面参数配置章节有详细介绍)。
设置 FetchSize
为Integer.MIN_VALUE
让客户端不缓存,客户端通过StreamingResult
的方式从网络连接上流式读取执行结果。使用 Cursor Fetch 首先需 设置 FetchSize
为正整数且在 JDBC URL 中配置useCursorFetch=true
。
2. MySQL JDBC 参数FetchSize
设置为 Integer.MIN_VALUE
的方式,比第二种功能实现更简单且执行效率更高。
useServerPrepStmts
useServerPrepStmts
为 false,即尽管使用了 Prepare API,也只会在客户端做 “prepare”。因此为了避免服务器重复解析的开销,如果同一条 SQL 语句需要多次使用 Prepare API,则建议设置该选项为 true。Query Summary > QPS By Instance
查看请求命令类型,如果请求中 COM_QUERY
被 COM_STMT_EXECUTE
或 COM_STMT_PREPARE
代替即生效。
cachePrepStmts
useServerPrepStmts=true
能让服务端执行 prepare 语句,但默认情况下客户端每次执行完后会 close prepared 的语句,并不会复用,这样 prepare 效率甚至不如文本执行。所以建议开启 useServerPrepStmts=true
后同时配置 cachePrepStmts=true
,这会让客户端缓存 prepare 语句。Query Summary > QPS By Instance
查看请求命令类型,如果类似下图,请求中 COM_STMT_EXECUTE
数目远远多于 COM_STMT_PREPARE
即生效。
useConfigs=maxPerformance
配置会同时配置多个参数,其中也包括 cachePrepStmts=true
。prepStmtCacheSqlLimit
cachePrepStmts
后还需要注意 prepStmtCacheSqlLimit
配置(默认为 256),该配置控制客户端缓存 prepare 语句的最大长度,超过该长度将不会被缓存。Query Summary > QPS by Instance
查看请求命令类型,如果已经配置了 cachePrepStmts=true
,但 COM_STMT_PREPARE
还是和 COM_STMT_EXECUTE
基本相等且有 COM_STMT_CLOSE
,需要检查这个配置项是否设置得太小。
prepStmtCacheSize
prepStmtCacheSize
控制缓存的 prepare 语句数目(默认为 25),如果应用需要 prepare 的 SQL 种类很多且希望复用 prepare 语句,可以调大该值。Query Summary > QPS by Instance
查看请求中 COM_STMT_EXECUTE
数目是否远远多于 COM_STMT_PREPARE
来确认是否正常。rewriteBatchedStatements=true
,在已经使用 addBatch
或 executeBatch
后默认 JDBC 还是会一条条 SQL 发送,例如:
pstmt = prepare(“insert into t (a) values(?)”);
pstmt.setInt(1, 10);
pstmt.addBatch();
pstmt.setInt(1, 11);
pstmt.addBatch();
pstmt.setInt(1, 12);
pstmt.executeBatch();
insert into t(a) values(10);
insert into t(a) values(11);
insert into t(a) values(12);
rewriteBatchedStatements=true
,发送到 TiDB 的 SQL 将是:
insert into t(a) values(10),(11),(12);
insert into t (a) values (10) on duplicate key update a = 10;
insert into t (a) values (11) on duplicate key update a = 11;
insert into t (a) values (12) on duplicate key update a = 12;
insert into t (a) values (10) on duplicate key update a = values(a);
insert into t (a) values (11) on duplicate key update a = values(a);
insert into t (a) values (12) on duplicate key update a = values(a);
insert into t (a) values (10), (11), (12) on duplicate key update a = values(a);
update t set a = 10 where id = 1; update t set a = 11 where id = 2; update t set a = 12 where id = 3;
rewriteBatchedStatements=true
。elect @@session.transaction_read_only
)。这些 SQL 对 TiDB 无用,推荐配置 useConfigs=maxPerformance
来避免额外开销。useConfigs=maxPerformance
会包含一组配置:
cacheServerConfiguration=true
useLocalSessionState=true
elideSetAutoCommits=true
alwaysSendSetIsolation=false
enableQueryTimeouts=false
连接池 1. 连接数配置
maximumPoolSize
:连接池最大连接数,配置过大会导致 TiDB 消耗资源维护无用连接,配置过小则会导致应用获取连接变慢,所以需根据应用自身特点配置合适的值,可参考 这篇文章。minimumIdle
:连接池最大空闲连接数,主要用于在应用空闲时存留一些连接以应对突发请求,同样是需要根据业务情况进行配置。
2. 探活配置metricRegistry
),通过监控能及时定位连接池问题。
The last packet sent successfully to the server was 3600000 milliseconds ago. The driver has not received any packets from the server. com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
n milliseconds ago
中的 n
是 0
或很小的值,则通常是执行的 SQL 导致 TiDB 异常退出引起的报错,推荐查看 TiDB stderr 日志;如果 n
是一个非常大的值(比如这里的 3600000),很可能是因为这个连接空闲太久然后被中间 proxy 关闭了,通常解决方式除了调大 proxy 的 idle 配置,还可以让连接池:
每次使用连接前检查连接是否可用。 使用单独线程定期检查连接是否可用。 定期发送 test query 保活连接。
数据访问框架 1. MyBatis
select 1 from t where id = #{param1}
会作为 prepare 语句转换为select 1 from t where id = ?
进行 prepare, 并使用实际参数来复用执行,通过配合前面的 Prepare 连接参数能获得最佳性能。select 1 from t where id = ${param2}
会做文本替换为select 1 from t where id = 1
执行,如果这条语句被 prepare 成了不同参数,可能会导致 TiDB 缓存大量的 prepare 语句,并且这种方式执行 SQL 有注入安全风险。
insert ... values(...), (...), ...
的形式,除了前面所说的在 JDBC 配置 rewriteBatchedStatements=true
外,MyBatis 还可以使用动态 SQL 的foreach 语法 来半自动生成 batch insert。比如下面的 mapper:
<insert id="insertTestBatch" parameterType="java.util.List" fetchSize="1">
insert into test
(id, v1, v2)
values
<foreach item="item" index="index" collection="list" separator=",">
(
#{item.id}, #{item.v1}, #{item.v2}
)
</foreach>
on duplicate key update v2 = v1 + values(v1)
</insert>
insert on duplicate key update
语句,values 后面的 (?, ?, ?)
数目是根据传入的 list 个数决定,最终效果和使用 rewriteBatchStatements=true
类似,可以有效减少客户端和 TiDB 的网络交互次数,同样需要注意 prepare 后超过 prepStmtCacheSqlLimit
限制导致不缓存 prepare 语句的问题。
1.3 Streaming 结果
可以通过在 mapper 配置中对单独一条 SQL 设置 fetchSize
(见上一段代码段),效果等同于调用 JDBC setFetchSize。可以使用带 ResultHandler 的查询接口来避免一次获取整个结果集。 可以使用 Cursor 类来进行流式读取。
<select>
部分配置 fetchSize="-2147483648"
(Integer.MIN_VALUE
) 来流式读取结果。
<select id="getAll" resultMap="postResultMap" fetchSize="-2147483648">
select * from post;
</select>
@Options(fetchSize = Integer.MIN_VALUE)
并返回 Cursor 从而让 SQL 结果能被流式读取。
@Select("select * from post")
@Options(fetchSize = Integer.MIN_VALUE)
Cursor<Post> queryAllPost();
2. ExecutorTypeopenSession
的时候可以选择 ExecutorType
,MyBatis 支持三种 executor:
Simple
:每次执行都会向 JDBC 进行 prepare 语句的调用(如果 JDBC 配置有开启cachePrepStmts
,重复的 prepare 语句会复用)。
Reuse
:在 executor 中缓存 prepare 语句,这样不用 JDBC 的cachePrepStmts
也能减少重复 prepare 语句的调用。Batch
:每次更新只有在addBatch
到 query 或 commit 时才会调用executeBatch
执行,如果 JDBC 层开启了rewriteBatchStatements
,则会尝试改写,没有开启则会一条条发送。
Spring Transaction 排查工具 1. jstack 2. jmap & mat 3. trace 4. 火焰图 总结Simple
,需要在调用 openSession
时改变 ExecutorType
。如果是 Batch 执行,会遇到事务中前面的 update 或 insert 都非常快,而在读数据或 commit 事务时比较慢的情况,这实际上是正常的,在排查慢 SQL 时需要注意。@Transactional
注解标记方法,AOP 将会在方法前开启事务,方法返回结果前 commit 事务。如果遇到类似业务,可以通过查找代码 @Transactional
来确定事务的开启和关闭时机。需要特别注意有内嵌的情况,如果发生内嵌,Spring 会根据 Propagation 配置使用不同的行为,因为 TiDB 未支持 savepoint,所以不支持嵌套事务。pprof/goroutine
,可以比较方便地排查进程卡死的问题。-m
选项。Mybatis BatchExecutor flush
调用 update)或死锁问题(比如:测试程序都在抢占应用中某把锁导致没发送 SQL)top -p $PID -H
或者 Java swiss knife 都是常用的查看线程 ID 的方法。通过 printf "%x\n" pid
把线程 ID 转换成 16 进制,然后去 jstack 输出结果中找对应线程的栈信息,可以定位“某个线程占用 CPU 比较高,不知道它在执行什么”的问题。pprof/heap
不同,jmap 会将整个进程的内存快照 dump 下来(go 是分配器的采样),然后可以通过另一个工具 mat 做分析。
java培训班:http://www.baizhiedu.com/java2019