连接池管理方面
- 预检查与优雅关闭
- 在获取连接前,通过连接池提供的方法(如
isValid
)对连接进行预检查,避免获取到无效连接后才抛出异常。这样可以在早期发现问题,减少异常处理开销。
- 当应用程序关闭时,优雅地关闭连接池。确保所有连接都被正确释放,避免因为连接未关闭导致的资源泄露和后续可能的异常。使用
try - finally
块来保证连接池关闭操作一定会执行。
DataSource dataSource =...;
Connection connection = null;
try {
connection = dataSource.getConnection();
if (connection.isValid(5)) {
// 执行数据库操作
}
} catch (SQLException e) {
// 处理获取连接异常
} finally {
if (connection!= null) {
try {
connection.close();
} catch (SQLException e) {
// 记录关闭连接异常日志
}
}
}
- 配置合理的连接池参数
- 根据应用程序的并发需求,合理设置连接池的最大连接数、最小连接数、连接超时时间等参数。避免因为连接数不足导致频繁获取连接失败的异常,或者因为连接过多造成资源浪费。
- 例如,使用HikariCP连接池,可以通过如下方式配置:
spring.datasource.hikari.maximum - pool - size = 100
spring.datasource.hikari.minimum - idle = 10
spring.datasource.hikari.connection - timeout = 30000
- 连接复用与缓存
- 尽可能复用已有的连接,减少频繁创建和销毁连接带来的性能开销。连接池本身就具备连接复用的功能,但在应用程序中也应避免不必要的连接关闭和重新获取操作。
- 对于一些只读操作,可以考虑使用缓存机制(如Redis),减少对数据库的访问频率,从而降低JDBC异常发生的概率。
异常日志记录方面
- 日志级别控制
- 使用合适的日志级别来记录异常。对于严重影响系统运行的异常(如数据库连接不可用),使用
ERROR
级别记录;对于一些可恢复的异常(如查询结果为空),可以使用WARN
级别。避免使用DEBUG
级别记录异常信息到生产环境日志中,因为DEBUG
级别日志可能会包含大量详细信息,影响日志性能。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JdbcExample {
private static final Logger logger = LoggerFactory.getLogger(JdbcExample.class);
public void executeQuery() {
try {
// JDBC操作
} catch (SQLException e) {
if (e.getSQLState().startsWith("08")) {
logger.error("数据库连接异常", e);
} else {
logger.warn("查询异常", e);
}
}
}
}
- 异步日志记录
- 使用异步日志框架(如
log4j2
的异步Appender)来记录异常日志。这样可以将日志记录操作从主线程中分离出来,避免因为日志记录的I/O操作阻塞主线程,从而减少对系统性能的影响。
<Appenders>
<Async name="AsyncFile">
<File name="File" fileName="logs/app.log"/>
<PatternLayout pattern="%d{yyyy - MM - dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Async>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="AsyncFile"/>
</Root>
</Loggers>
- 日志内容精简
- 避免在异常日志中记录过多不必要的信息。只记录关键的异常信息(如异常类型、错误码、简要的错误描述)以及相关的上下文信息(如当前执行的方法名、参数值)。过多的冗余信息不仅会增加日志文件大小,还会影响日志记录和读取的性能。
try {
// JDBC操作
} catch (SQLException e) {
logger.error("执行方法 {} 时发生数据库异常,错误码: {}, 异常信息: {}",
Thread.currentThread().getStackTrace()[1].getMethodName(), e.getSQLState(), e.getMessage());
}
通用异常处理策略
- 异常封装与转换
- 将JDBC特定的
SQLException
封装或转换为应用程序自定义的异常。这样可以在业务层屏蔽底层JDBC细节,同时可以根据自定义异常类型进行更针对性的处理。
public class DatabaseOperationException extends RuntimeException {
public DatabaseOperationException(String message) {
super(message);
}
public DatabaseOperationException(String message, Throwable cause) {
super(message, cause);
}
}
public class JdbcUtil {
public static void executeUpdate(String sql) {
try {
// JDBC执行更新操作
} catch (SQLException e) {
throw new DatabaseOperationException("执行数据库更新操作失败", e);
}
}
}
- 批量操作与事务管理
- 在可能的情况下,将多个JDBC操作合并为一个批量操作。例如,使用
PreparedStatement
的addBatch
和executeBatch
方法。这样可以减少数据库交互次数,降低异常发生的概率,同时提高性能。
- 合理使用事务管理,确保一组相关的JDBC操作要么全部成功,要么全部回滚。在事务边界处进行统一的异常处理,避免在每个单独的操作中处理异常导致的重复代码和性能损耗。
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
PreparedStatement statement = connection.prepareStatement("INSERT INTO users (name, age) VALUES (?,?)");
for (User user : userList) {
statement.setString(1, user.getName());
statement.setInt(2, user.getAge());
statement.addBatch();
}
statement.executeBatch();
connection.commit();
} catch (SQLException e) {
if (connection!= null) {
try {
connection.rollback();
} catch (SQLException ex) {
// 记录回滚异常日志
}
}
// 处理数据库操作异常
} finally {
if (connection!= null) {
try {
connection.close();
} catch (SQLException e) {
// 记录关闭连接异常日志
}
}
}
- 异常重试机制
- 对于一些因为临时网络问题或数据库短暂繁忙导致的可重试异常,可以引入重试机制。通过使用一些重试框架(如Spring Retry),在一定次数内自动重试失败的JDBC操作,避免直接抛出异常影响系统流程。
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class JdbcService {
@Retryable(value = {SQLException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void executeQuery() throws SQLException {
// JDBC操作
}
}