第2篇-异常全景图
约 2757 字大约 9 分钟
2026-05-24
二、数据源层面的异常
数据源层面也是常见的出问题的地方。下面我来介绍几个经典的异常场景:
1. 连接池耗尽(最常见!)
场景描述: 你的应用程序突然报错了,错误信息是:Cannot get a connection, pool exhausted(无法获取连接,连接池已耗尽)!
你可能会问:为什么连接池会耗尽呢?
答案:有很多可能的原因!下面我来一一讲解。
原因一:连接泄漏(经典的坑!)
场景描述: 你没有正确关闭连接(或者归还连接到连接池),导致连接池中的连接越来越少,最终耗尽!
错误示例:
@Service
public class UserService {
@Autowired
private DataSource dataSource;
public void createUser(User user) throws SQLException {
// 【错误】手动获取连接,但是没有关闭!
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("INSERT INTO user(name, age) VALUES(?, ?)");
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.execute();
// 【错误】没有关闭 PreparedStatement 和 Connection!
}
}正确示例:
@Service
public class UserService {
@Autowired
private DataSource dataSource;
public void createUser(User user) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("INSERT INTO user(name, age) VALUES(?, ?)");
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.execute();
} finally {
// 【核心】在 finally 块中关闭资源!
if (ps != null) {
try { ps.close(); } catch (SQLException e) { /* 忽略 */ }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { /* 忽略 */ }
}
}
}
}【注意】 如果你使用的是 Spring 事务管理,就不需要手动关闭连接了!因为 Spring 会自动帮你关闭(或者归还到连接池)。
但是,如果你手动获取连接,并且没有使用 Spring 事务管理,就需要手动关闭连接!
原因二:连接池配置不当
场景描述: 你的应用程序的并发量并不大,但是竟然出现了连接池耗尽的问题!
你可能会问:为什么呢?
答案:可能是因为你的连接池配置不当!比如:
- maxActive(最大连接数)配置得太小了
- maxWait(获取连接的最大等待时间)配置得太长了
- removeAbandoned(是否自动回收泄漏的连接)没有开启
正确配置(Druid 连接池):
# application.yml
spring:
datasource:
druid:
# 【核心】最大连接数(根据你的业务需求配置)
max-active: 100
# 【核心】最小空闲连接数
min-idle: 10
# 【核心】获取连接的最大等待时间(毫秒)
max-wait: 60000
# 【核心】是否自动回收泄漏的连接
remove-abandoned: true
# 【核心】连接泄漏的超时时间(秒)
remove-abandoned-timeout: 300
# 【核心】是否打印连接泄漏的日志
log-abandoned: true【扩展点】 你可以通过 Druid 的监控统计功能,查看连接池的使用情况:
@Configuration
public class DruidConfig {
@Bean
public ServletRegistrationBean<StatViewServlet> druidServlet() {
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", "admin"); // 用户名
initParams.put("loginPassword", "123456"); // 密码
bean.setInitParameters(initParams);
return bean;
}
}然后,访问 http://localhost:8080/druid,就可以看到连接池的使用情况了!
2. 连接超时
场景描述: 你的应用程序突然报错了,错误信息是:Connection timed out(连接超时)!
你可能会问:为什么会出现连接超时呢?
答案:有很多可能的原因!下面我来一一讲解。
原因一:数据库服务器挂了
排查方法:
- 检查数据库服务器是否正常运行
- 检查数据库服务器的日志
- 检查网络连接是否正常(比如,使用
ping命令)
原因二:连接池配置不当
场景描述: 你的连接池配置了一些超时参数,但是配置得不恰当!
正确配置(Druid 连接池):
# application.yml
spring:
datasource:
druid:
# 【核心】连接超时时间(毫秒)
connect-timeout: 30000
# 【核心】socket 超时时间(毫秒)
socket-timeout: 60000
# 【核心】连接空闲时间(毫秒)
time-between-eviction-runs-millis: 60000
# 【核心】连接最小空闲时间(毫秒)
min-evictable-idle-time-millis: 3000003. SQL 执行超时
场景描述: 你的应用程序突然报错了,错误信息是:Statement timed out(SQL 执行超时)!
你可能会问:为什么会出现 SQL 执行超时呢?
答案:可能是因为你的 SQL 执行时间太长了!比如:
- SQL 没有索引,导致全表扫描
- SQL 返回的数据量太大
- 数据库服务器负载过高
正确配置(MyBatis):
<!-- mybatis-config.xml -->
<configuration>
<settings>
<!-- 【核心】设置全局 SQL 执行超时时间(秒) -->
<setting name="defaultStatementTimeout" value="30"/>
</settings>
</configuration>正确配置(Spring Boot):
# application.yml
mybatis:
configuration:
# 【核心】设置全局 SQL 执行超时时间(秒)
default-statement-timeout: 30【扩展点】 你可以在 Mapper XML 中为每条 SQL 单独配置超时时间:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 【核心】为这条 SQL 单独配置超时时间(秒) -->
<select id="selectById" resultType="User" timeout="10">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>三、MyBatis 层面的异常
MyBatis 层面也是常见的出问题的地方。下面我来介绍几个经典的异常场景:
1. SQL 执行失败
场景描述: 你的应用程序突然报错了,错误信息是:SQL syntax error(SQL 语法错误)!
你可能会问:为什么会出现 SQL 语法错误呢?
答案:有很多可能的原因!下面我来一一讲解。
原因一:SQL 写错了
错误示例:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 【错误】SQL 语法错误(少了 VALUES 关键字) -->
<insert id="insert" parameterType="User">
INSERT INTO user(name, age) (#{name}, #{age})
</insert>
</mapper>正确示例:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 【正确】SQL 语法正确 -->
<insert id="insert" parameterType="User">
INSERT INTO user(name, age) VALUES(#{name}, #{age})
</insert>
</mapper>原因二:参数类型不匹配
错误示例:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 【错误】参数类型不匹配(name 是 String 类型,但是用了 #{name, jdbcType=INTEGER}) -->
<insert id="insert" parameterType="User">
INSERT INTO user(name, age) VALUES(#{name, jdbcType=INTEGER}, #{age})
</insert>
</mapper>正确示例:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 【正确】参数类型匹配 -->
<insert id="insert" parameterType="User">
INSERT INTO user(name, age) VALUES(#{name, jdbcType=VARCHAR}, #{age})
</insert>
</mapper>2. 结果集映射错误
场景描述: 你的应用程序没有报错,但是查询出来的数据竟然是 null!
你可能会问:为什么呢?
答案:可能是因为你的结果集映射配置错了!比如:
- 数据库列名和 Java 属性名不一致,并且没有配置
ResultMap - 没有开启
mapUnderscoreToCamelCase(下划线转驼峰)
错误示例:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 【错误】数据库列名是 user_name,但是 Java 属性名是 userName,没有配置 ResultMap -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>正确示例一:开启 mapUnderscoreToCamelCase:
# application.yml
mybatis:
configuration:
# 【核心】开启下划线转驼峰(user_name -> userName)
map-underscore-to-camel-case: true正确示例二:配置 ResultMap:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 【核心】配置 ResultMap -->
<resultMap id="UserResultMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/> <!-- 数据库列名 -> Java 属性名 -->
<result property="userAge" column="user_age"/>
</resultMap>
<select id="selectById" resultMap="UserResultMap"> <!-- 【核心】使用 ResultMap -->
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>四、MySQL 层面的异常
MySQL 层面也是常见的出问题的地方。下面我来介绍几个经典的异常场景:
1. 死锁(最常见!)
场景描述: 你的应用程序突然报错了,错误信息是:Deadlock found when trying to get lock(尝试获取锁时发现死锁)!
你可能会问:为什么会出现死锁呢?
答案:因为两个事务互相等待对方释放锁!比如:
- 事务 A 锁定了记录 1,然后尝试锁定记录 2
- 事务 B 锁定了记录 2,然后尝试锁定记录 1
- 事务 A 和事务 B 互相等待对方释放锁,形成死锁!
排查方法:
- 查看 MySQL 的死锁日志:
SHOW ENGINE INNODB STATUS; - 分析死锁日志,找出死锁的原因
- 优化 SQL(比如,按照相同的顺序锁定记录)
解决方法:
- 按照相同的顺序锁定记录:避免互相等待
- 使用更低的隔离级别:比如,从
REPEATABLE_READ改成READ_COMMITTED - 使用索引:避免表锁
- 减少事务的范围:尽快提交或回滚事务
2. 锁等待超时
场景描述: 你的应用程序突然报错了,错误信息是:Lock wait timeout exceeded; try restarting transaction(锁等待超时,请尝试重启事务)!
你可能会问:为什么会出现锁等待超时呢?
答案:因为一个事务等待另一个事务释放锁的时间太长了!比如:
- 事务 A 锁定了记录 1,并且长时间不提交
- 事务 B 尝试锁定记录 1,但是等待时间超过了
innodb_lock_wait_timeout - MySQL 抛出锁等待超时异常
排查方法:
- 查看 MySQL 的锁等待超时配置:
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; - 查看当前正在运行的事务:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; - 查看当前的锁信息:
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
解决方法:
- 减少事务的范围:尽快提交或回滚事务
- 增大锁等待超时时间:
SET GLOBAL innodb_lock_wait_timeout = 120;(默认是 50 秒) - 优化 SQL:避免长事务
五、总结与下一篇预告
1. 总结
今天,我们详细剖析了异常全景图,包括:
- Spring 事务层面的异常:事务不回滚、事务传播机制使用不当、事务隔离级别使用不当
- 数据源层面的异常:连接池耗尽、连接超时、SQL 执行超时
- MyBatis 层面的异常:SQL 执行失败、结果集映射错误
- MySQL 层面的异常:死锁、锁等待超时
你可能会问:我知道了这些异常,但是如何快速排查问题呢?
别着急!在下一篇文章中,我们会详细讲解基于源码的调优实践,包括:
- 事务调优(比如,如何选择合适的事务传播行为、隔离级别)
- 数据源调优(比如,如何配置连接池参数)
- MyBatis 调优(比如,如何优化 SQL、如何使用缓存)
2. 下一篇预告
在下一篇(系列四第 3 篇)中,我们会讲解 《基于源码的调优实践》。
你会学到:
- 事务调优:如何选择合适的事务传播行为、隔离级别、超时时间
- 数据源调优:如何配置连接池参数(比如 maxActive、minIdle、maxWait 等)
- MyBatis 调优:如何优化 SQL、如何使用缓存(一级缓存、二级缓存)
- MySQL 调优:如何创建索引、如何分析慢查询
你可能会问:为什么需要调优呢?
问得好! 因为在实际工作中,性能问题是常见的!你需要:
- 快速定位性能瓶颈:知道哪里需要调优
- 快速解决性能问题:知道如何调优
所以,下一篇文章会是非常实用、非常有收获的!相信我!🚀
写到这里,我不禁感叹:从 Spring 事务、数据源、MyBatis 到 MySQL,每个环节都可能出错!但是,只要你知道了每个环节可能出什么错,并且知道了如何排查和解决,你就能够快速定位问题、快速解决问题!
好了,今天的文章就到这里。如果你觉得有收获,别忘了点赞、收藏、关注哦!你的支持,是我创作的最大动力!❤️
我们下一篇见!👋