part3-performance-tuning
约 2603 字大约 9 分钟
2026-05-24
系列文章:《深入Spring事务与数据源:从注解到数据库连接的全链路解析》
系列四第3篇:全链路性能调优实战与最佳实践
基于版本:Spring 5.3.x + Druid 1.2.x + MyBatis 3.5.x
前置阅读:系列四第2篇:常见异常分析与根因定位
前言
前面两篇文章咱们从源码角度搞清楚了三大框架的协作机制,以及各种异常的根因。今天这篇文章,咱们来聊聊性能调优。
你可能知道一些零散的优化技巧,比如"加索引"、"调大连接池",但是你有没有想过:
- 这些优化为什么有效?
- 还有哪些隐藏的优化点?
- 怎么权衡不同优化方案的利弊?
- 怎么建立系统化的调优思路?
别急,今天这篇文章就来回答这些问题。而且我敢说,看完这篇文章,你不仅能掌握具体的调优技巧,还能建立系统化的调优思维!
好了,坐稳了,咱们开始!
一、Spring事务优化
1.1 缩小事务范围
优化前:
@Transactional
public void createUser(User user) {
// 耗时操作:调用第三方API(5秒)
callExternalAPI(user);
// 数据库操作(0.1秒)
userMapper.insert(user);
// 耗时操作:发送邮件(3秒)
sendEmail(user);
}问题:整个方法都在事务中,连接被占用8.1秒。
优化后:
public void createUser(User user) {
// 非事务操作
callExternalAPI(user);
// 只有必要的操作在事务中
insertUserWithTransaction(user);
// 非事务操作
sendEmail(user);
}
@Transactional
private void insertUserWithTransaction(User user) {
userMapper.insert(user);
}效果:连接只占用0.1秒,提升了81倍!
源码角度分析:
从DataSourceTransactionManager.doBegin()源码可以看到,事务开启时会获取连接并绑定到ThreadLocal:
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
// 获取连接(从连接池)
Connection newCon = obtainDataSource().getConnection();
// 关闭自动提交
con.setAutoCommit(false);
// 绑定到ThreadLocal
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}事务范围越小,连接占用时间越短,连接池的并发能力就越强。
1.2 选择合适的事务传播行为
场景1:日志记录不需要和主业务一起回滚
优化前:
@Transactional
public void createUser(User user) {
userMapper.insert(user);
// 如果这个方法抛异常,整个事务都会回滚
logService.saveLog("创建用户");
}优化后:
@Transactional
public void createUser(User user) {
userMapper.insert(user);
try {
// 使用REQUIRES_NEW,开启独立事务
logService.saveLogWithNewTransaction("创建用户");
} catch (Exception e) {
// 日志失败不影响主业务
log.error("日志记录失败", e);
}
}
// LogService
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLogWithNewTransaction(String message) {
logMapper.insert(new Log(message));
}效果:日志记录失败不会导致主业务回滚。
源码角度分析:
从AbstractPlatformTransactionManager源码可以看到,REQUIRES_NEW会挂起现有事务,创建新事务:
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
// 挂起现有事务
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
// 创建新事务
return startTransaction(definition, transaction, debugEnabled, suspendedResources);
}
catch (RuntimeException | Error beginEx) {
// 如果创建失败,恢复现有事务
resumeAfterBeginException(transaction, null, suspendedResources, beginEx);
throw beginEx;
}
}1.3 合理使用只读事务
场景:查询方法
优化前:
@Transactional
public User getUserById(Long id) {
return userMapper.selectById(id);
}优化后:
@Transactional(readOnly = true)
public User getUserById(Long id) {
return userMapper.selectById(id);
}效果:
- 数据库可以对只读事务做优化
- 某些数据库(如MySQL)可以走只读连接池
- Spring可以跳过一些不必要的操作
源码角度分析:
从DataSourceTransactionManager.doBegin()可以看到,只读事务会设置连接的只读属性:
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
// 设置只读标记
if (definition.isReadOnly()) {
try {
con.setReadOnly(true);
}
catch (SQLException ex) {
logger.debug("Could not set read-only for transaction", ex);
}
}
}二、Druid连接池优化
2.1 合理配置连接池参数
优化前:
spring:
datasource:
druid:
initial-size: 1
max-active: 5
min-idle: 1问题:
- 初始连接太少,启动后需要频繁创建连接
- 最大连接太少,并发能力差
- 最小空闲太少,无法保持连接活跃
优化后:
spring:
datasource:
druid:
initial-size: 10 # 根据系统负载调整
max-active: 20 # 根据数据库连接数限制调整
min-idle: 10 # 和initial-size保持一致
max-wait: 5000 # 5秒,快速失败效果:
- 启动时就有10个连接可用,避免冷启动
- 最大20个连接,提供足够的并发能力
- 保持10个空闲连接,避免频繁创建/销毁
源码角度分析:
从DruidDataSource.init()可以看到,初始化时会创建initialSize个连接:
@Override
public void init() throws SQLException {
// 创建初始连接
for (int i = 0; i < initialSize; ++i) {
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
connections[poolingCount++] = holder;
}
}2.2 开启连接有效性检测
优化前:
spring:
datasource:
druid:
test-on-borrow: false
test-while-idle: false问题:如果数据库重启,连接池中的连接都失效了,但是不会检测,导致获取到无效连接。
优化后:
spring:
datasource:
druid:
test-on-borrow: false # 关闭获取时检测(性能好)
test-while-idle: true # 开启空闲时检测
time-between-eviction-runs-millis: 60000 # 每60秒检测一次
validation-query: SELECT 1
validation-query-timeout: 1效果:定期检测空闲连接的有效性,移除无效连接。
源码角度分析:
从DestroyConnectionThread可以看到,后台线程会定期检测:
private class DestroyConnectionThread extends Thread {
public void run() {
while (true) {
try {
// 每60秒执行一次
Thread.sleep(timeBetweenEvictionRunsMillis);
if (testWhileIdle) {
// 检测空闲连接
checkIdleConnections();
}
} catch (InterruptedException e) {
break;
}
}
}
}
private void checkIdleConnections() {
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder holder = connections[i];
// 检测连接有效性
boolean validate = testConnectionInternal(holder.getConnection());
if (!validate) {
// 移除无效连接
removeConnection(holder);
JdbcUtils.close(holder.getConnection());
}
}
}2.3 开启连接保活
优化前:
spring:
datasource:
druid:
keep-alive: false问题:连接长时间不活动,可能被防火墙或数据库断开。
优化后:
spring:
datasource:
druid:
keep-alive: true
keep-alive-between-time-millis: 60000 # 每60秒保活一次效果:定期发送心跳,保持连接活跃。
源码角度分析:
从CreateConnectionThread可以看到,后台线程会定期保活:
private class CreateConnectionThread extends Thread {
public void run() {
while (true) {
try {
// 每60秒执行一次
Thread.sleep(keepAliveBetweenTimeMillis);
if (keepAlive) {
// 保活连接
keepAliveConnections();
}
} catch (InterruptedException e) {
break;
}
}
}
}
private void keepAliveConnections() {
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder holder = connections[i];
long idleMillis = System.currentTimeMillis() - holder.getLastActiveTime();
// 如果空闲时间超过阈值
if (idleMillis >= keepAliveBetweenTimeMillis) {
// 执行validationQuery,发送心跳
boolean validate = testConnectionInternal(holder.getConnection());
if (validate) {
// 更新最后活跃时间
holder.setLastActiveTime(System.currentTimeMillis());
}
}
}
}三、MyBatis优化
3.1 使用合适的Executor类型
场景1:批量插入
优化前:
// 默认使用SimpleExecutor
for (User user : users) {
userMapper.insert(user); // 每次都创建新的Statement
}优化后:
// 使用BatchExecutor
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user); // 批量执行
}
session.commit();
} finally {
session.close();
}效果:批量插入性能提升10倍以上。
源码角度分析:
从BatchExecutor.doUpdate()可以看到,批量执行器会累积SQL,最后一次性执行:
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject,
RowBounds.DEFAULT, null, null);
// 添加到批处理列表
BatchResult batchResult = new BatchResult(ms, parameterObject);
batchResultList.add(batchResult);
return BATCH_UPDATE_COUNT;
}
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
List<BatchResult> results = new ArrayList<>();
// 一次性执行所有SQL
for (BatchResult batchResult : batchResultList) {
int[] updateCounts = statement.executeBatch();
batchResult.setUpdateCounts(updateCounts);
results.add(batchResult);
}
batchResultList.clear();
return results;
}3.2 合理使用缓存
场景1:频繁查询的字典数据
优化前:
<!-- 没有开启二级缓存 -->
<mapper namespace="com.example.DictMapper">
<select id="selectDict" resultType="Dict">
SELECT * FROM dict WHERE type = #{type}
</select>
</mapper>优化后:
<!-- 开启二级缓存 -->
<mapper namespace="com.example.DictMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<select id="selectDict" resultType="Dict" useCache="true">
SELECT * FROM dict WHERE type = #{type}
</select>
</mapper>效果:查询直接从缓存返回,不查询数据库。
源码角度分析:
从CachingExecutor.query()可以看到,会先检查二级缓存:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
// 获取二级缓存
Cache cache = ms.getCache();
if (cache != null) {
// 从缓存获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list != null) {
// 缓存命中,直接返回
return list;
}
}
// 缓存未命中,查询数据库
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}3.3 避免N+1查询
优化前:
// 查询订单列表
List<Order> orders = orderMapper.selectList();
// 循环查询每个订单的用户(N次查询)
for (Order order : orders) {
User user = userMapper.selectById(order.getUserId());
order.setUser(user);
}优化后:
// 使用JOIN查询,一次查询所有数据
List<OrderVO> orders = orderMapper.selectListWithUser();<select id="selectListWithUser" resultMap="OrderUserResultMap">
SELECT o.*, u.name, u.email
FROM order o
LEFT JOIN user u ON o.user_id = u.id
</select>效果:从N+1次查询减少到1次查询。
四、全链路优化
4.1 完整推荐配置
application.yml:
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 连接池配置
initial-size: 10
max-active: 20
min-idle: 10
max-wait: 5000
# 连接检测
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000
validation-query: SELECT 1
validation-query-timeout: 1
# 保活
keep-alive: true
keep-alive-between-time-millis: 60000
# 连接泄漏检测
remove-abandoned: true
remove-abandoned-timeout: 180
log-abandoned: true
# Filter
filters: stat,wall
# 监控
stat:
slow-sql-millis: 2000
log-slow-sql: true
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
lazy-loading-enabled: true
multiple-result-sets-enabled: true
use-column-label: true
use-generated-keys: false
default-executor-type: SIMPLE
default-statement-timeout: 30
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl4.2 监控与排查
开启Druid监控页面:
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: admin123访问:http://localhost:8080/druid/index.html
关键监控指标:
- 活跃连接数(ActiveCount):应该远低于maxActive
- 空闲连接数(PoolingCount):应该在minIdle附近
- 等待线程数(WaitThreadCount):应该为0
- 慢SQL列表:应该很少或没有
- SQL执行时间:应该在合理范围内
4.3 性能测试
使用JMeter或wrk进行压力测试:
# wrk压测
wrk -t12 -c400 -d30s http://localhost:8080/api/users
# 观察指标
- QPS(每秒请求数)
- RT(响应时间)
- 错误率
- 连接池使用率五、总结
通过这篇文章的分析,咱们总结了全链路的性能调优最佳实践:
Spring事务优化:
- 缩小事务范围
- 选择合适的事务传播行为
- 合理使用只读事务
Druid连接池优化:
- 合理配置连接池参数
- 开启连接有效性检测
- 开启连接保活
MyBatis优化:
- 使用合适的Executor类型
- 合理使用缓存
- 避免N+1查询
全链路优化:
- 完整推荐配置
- 监控与排查
- 性能测试
记住,调优不是一蹴而就的,需要持续监控、持续优化。建立系统化的调优思路,比掌握零散的技巧更重要!
参考资料
- Spring Framework 5.3.x 源码
- Druid 1.2.x 源码
- MyBatis 3.5.x 源码
- 《Spring源码深度解析》
- 《MyBatis技术内幕》
- 《Java性能调优实战》
- 实际项目调优经验总结