1. JDBC基础概念与核心组件
JDBC(Java Database Connectivity)是Java语言中用来规范客户端程序如何访问数据库的应用程序接口(API)。它提供了一套标准的方法与各种关系型数据库进行交互,使得开发者无需针对不同数据库编写特定代码。想象JDBC就像是一个万能翻译器,无论数据库说哪种"方言"(MySQL、Oracle、PostgreSQL等),它都能让Java程序与数据库顺畅沟通。
JDBC的核心由以下几个关键组件构成:
驱动程序(Driver):这是JDBC架构中最底层的部分,每个数据库厂商都需要提供自己的JDBC驱动实现。就像不同类型的手机需要不同的充电线一样,MySQL有mysql-connector-java,Oracle有ojdbc驱动。驱动负责与实际的数据库建立物理连接,并将通用的JDBC调用转换为特定数据库能理解的指令。
DriverManager:这是JDBC提供的一个基础服务类,用于管理各种数据库驱动。当你的Java程序需要连接数据库时,DriverManager会根据连接URL自动选择合适的驱动。它就像是一个交通调度中心,根据目的地(连接字符串)安排合适的车辆(驱动)。
Connection:代表与数据库的物理连接。获取Connection对象是JDBC操作的起点,就像打电话需要先拨通一样。每个Connection对象都维持着一个与数据库的会话,需要注意及时关闭以避免资源泄漏。
Statement/PreparedStatement:这两个接口用于执行SQL语句。Statement用于执行静态SQL,而PreparedStatement则用于参数化查询,后者不仅能防止SQL注入,还能提高重复执行的效率。就像写信时,Statement是每次手写全文,而PreparedStatement是使用填空模板。
ResultSet:这是SQL查询返回的结果集,可以看作是一个指向数据行的游标。通过它我们可以逐行读取查询结果,就像用放大镜查看Excel表格一样。ResultSet还支持结果集的滚动和更新,为复杂数据处理提供了便利。
事务管理:JDBC通过Connection接口提供事务支持,包括commit()、rollback()和setAutoCommit()等方法。这就像银行转账操作,要么全部成功,要么全部回滚到初始状态,保证数据的一致性。
2. JDBC环境配置与基础操作
2.1 驱动加载与连接建立
在开始JDBC编程前,需要先配置好开发环境。以MySQL为例,首先需要在项目中引入驱动jar包。如果你使用Maven,可以在pom.xml中添加:
xml复制<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
手动添加的话,需要下载对应版本的JDBC驱动jar包,并将其添加到项目的classpath中。这就像给Java程序安装了一个能与MySQL对话的插件。
建立数据库连接的标准代码如下:
java复制// 1. 加载驱动(新版本可省略这步)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取连接
String url = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection conn = DriverManager.getConnection(url, username, password);
这里有几个关键点需要注意:
- 连接URL的格式通常是:
jdbc:mysql://主机:端口/数据库名?参数=值 - 新版本MySQL驱动可以自动注册,所以Class.forName()这步不是必须的
- 生产环境中密码不应该硬编码在代码中,应该使用配置管理工具
- 建议总是设置serverTimezone参数,避免时区问题
2.2 执行SQL语句
获取Connection后,就可以执行SQL语句了。JDBC提供了三种执行SQL的方式:
Statement - 最基本的执行方式,适合执行静态SQL:
java复制Statement stmt = conn.createStatement();
String sql = "SELECT * FROM users WHERE age > 20";
ResultSet rs = stmt.executeQuery(sql);
PreparedStatement - 预编译SQL,适合带参数的查询:
java复制String sql = "INSERT INTO users(name, age) VALUES(?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "张三");
pstmt.setInt(2, 25);
int affectedRows = pstmt.executeUpdate();
CallableStatement - 用于调用存储过程:
java复制CallableStatement cstmt = conn.prepareCall("{call get_user_by_id(?)}");
cstmt.setInt(1, 1001);
ResultSet rs = cstmt.executeQuery();
在实际开发中,PreparedStatement应该是首选,因为它:
- 防止SQL注入攻击
- 提高性能(SQL被预编译)
- 自动处理特殊字符转义
- 代码更清晰易读
2.3 处理结果集
执行查询后会返回ResultSet对象,处理结果集的典型模式是:
java复制while(rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
Date birthDate = rs.getDate("birth_date");
// 处理数据...
}
ResultSet的常用方法包括:
- next():移动到下一行
- getXXX():获取当前行的各列值
- absolute(int row):直接定位到指定行
- updateXXX():更新当前行数据
- deleteRow():删除当前行
重要提示:ResultSet在使用后必须关闭,否则会导致数据库连接无法释放。最好使用try-with-resources语法确保资源释放。
3. JDBC高级特性与性能优化
3.1 事务管理
JDBC的事务管理是通过Connection对象控制的。默认情况下,每个SQL语句执行后都会自动提交(auto-commit模式)。要使用事务,需要先关闭自动提交:
java复制try {
conn.setAutoCommit(false); // 开始事务
// 执行多个SQL操作
updateAccount(conn, "A", -100); // A账户扣款
updateAccount(conn, "B", 100); // B账户收款
conn.commit(); // 提交事务
} catch (SQLException e) {
conn.rollback(); // 回滚事务
e.printStackTrace();
} finally {
conn.setAutoCommit(true); // 恢复自动提交
}
JDBC支持的事务隔离级别可以通过Connection的setTransactionIsolation()方法设置:
- TRANSACTION_READ_UNCOMMITTED
- TRANSACTION_READ_COMMITTED
- TRANSACTION_REPEATABLE_READ
- TRANSACTION_SERIALIZABLE
选择合适的事务隔离级别需要在性能和数据一致性之间取得平衡。大多数数据库的默认级别是READ_COMMITTED。
3.2 批量处理
当需要执行大量相似SQL时,使用批量处理可以显著提高性能:
java复制String sql = "INSERT INTO logs(time, message) VALUES(?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (Log log : logList) {
pstmt.setTimestamp(1, new Timestamp(log.getTime()));
pstmt.setString(2, log.getMessage());
pstmt.addBatch(); // 添加到批处理
if (i % 1000 == 0) { // 每1000条执行一次
pstmt.executeBatch();
}
}
pstmt.executeBatch(); // 执行剩余的
批量处理的性能提升主要来自:
- 减少网络往返次数
- 数据库优化器可以优化批量操作
- 减少JDBC驱动与数据库的交互开销
3.3 连接池技术
直接创建数据库连接是昂贵的操作(通常需要100ms以上),因此生产环境总是使用连接池。常见的连接池实现有:
-
HikariCP:目前性能最好的连接池
java复制HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(20); HikariDataSource ds = new HikariDataSource(config); Connection conn = ds.getConnection(); -
Druid:阿里开源的连接池,功能丰富
java复制DruidDataSource ds = new DruidDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/mydb"); ds.setUsername("root"); ds.setPassword("123456"); ds.setMaxActive(20); Connection conn = ds.getConnection();
连接池的关键配置参数包括:
- 最大连接数:根据应用负载和数据库能力设置
- 最小空闲连接:保持一定数量的预热连接
- 连接超时时间:避免长时间等待
- 连接存活时间:定期刷新连接
3.4 元数据获取
JDBC提供了获取数据库元数据的API,这在编写通用数据库工具时非常有用:
java复制// 获取数据库元数据
DatabaseMetaData dbMeta = conn.getMetaData();
ResultSet tables = dbMeta.getTables(null, null, "%", new String[]{"TABLE"});
// 获取结果集元数据
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData rsMeta = rs.getMetaData();
int columnCount = rsMeta.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String name = rsMeta.getColumnName(i);
String type = rsMeta.getColumnTypeName(i);
System.out.println(name + ": " + type);
}
元数据常用于:
- 动态SQL生成
- 数据库迁移工具
- 通用查询界面
- ORM框架实现
4. JDBC实战技巧与常见问题
4.1 资源管理最佳实践
JDBC编程中最常见的错误就是资源泄漏。正确的资源管理方式应该是:
java复制// 使用try-with-resources确保资源释放
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
// 处理结果
}
} catch (SQLException e) {
// 异常处理
}
如果没有使用Java 7+的try-with-resources,则需要手动关闭:
java复制Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
// ...
} finally {
// 关闭顺序与创建顺序相反
if (rs != null) try { rs.close(); } catch (SQLException e) { /* ignore */ }
if (pstmt != null) try { pstmt.close(); } catch (SQLException e) { /* ignore */ }
if (conn != null) try { conn.close(); } catch (SQLException e) { /* ignore */ }
}
4.2 SQL注入防护
SQL注入是最常见的安全漏洞之一。使用PreparedStatement是防止SQL注入的最有效方法:
java复制// 不安全的写法 - 容易受SQL注入攻击
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 安全的写法 - 使用参数化查询
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
即使使用PreparedStatement,也需要注意:
- 不要动态拼接SQL语句的关键字部分(如表名、列名)
- 对于表名、列名等无法参数化的部分,应该使用白名单校验
- 避免将敏感数据直接拼接到SQL中
4.3 常见异常处理
JDBC操作中常见的异常包括:
-
SQLException:所有JDBC异常的基类
- 包含错误码(errorCode)和SQL状态(SQLState)
- 不同数据库的错误码体系不同
-
BatchUpdateException:批量操作时部分失败
- 可以通过getUpdateCounts()获取每条语句的影响行数
-
SQLTimeoutException:操作超时
- 通常由查询超时或连接获取超时引起
健壮的JDBC代码应该:
java复制try {
// JDBC操作
} catch (SQLException e) {
logger.error("SQL Error: " + e.getMessage());
logger.error("SQL State: " + e.getSQLState());
logger.error("Vendor Error: " + e.getErrorCode());
// 根据具体错误码处理
if (e.getErrorCode() == 1062) { // MySQL duplicate key
throw new BusinessException("数据已存在");
}
throw new RuntimeException(e);
}
4.4 性能优化技巧
-
合理设置FetchSize:
java复制Statement stmt = conn.createStatement(); stmt.setFetchSize(100); // 每次从数据库获取100行 ResultSet rs = stmt.executeQuery("SELECT * FROM large_table"); -
使用流式处理大结果集:
java复制Statement stmt = conn.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); stmt.setFetchSize(Integer.MIN_VALUE); // MySQL流式结果集 -
优化PreparedStatement重用:
- 在循环外创建PreparedStatement
- 使用连接池时,考虑启用语句缓存
-
处理大数据类型:
- 使用setBinaryStream/setCharacterStream处理大文本/二进制数据
- 对于BLOB/CLOB,考虑分块传输
-
数据库特定优化:
- MySQL:useServerPrepStmts=true&cachePrepStmts=true
- Oracle:oracle.jdbc.freeMemoryOnEnterImplicitCache=true
4.5 日期时间处理
JDBC中的日期时间处理有几个常见陷阱:
java复制// Java 8日期时间API与JDBC的交互
LocalDate date = LocalDate.now();
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO events(name, event_date) VALUES(?, ?)");
pstmt.setString(1, "Meeting");
pstmt.setObject(2, date); // 直接使用setObject
ResultSet rs = stmt.executeQuery("SELECT event_date FROM events");
while (rs.next()) {
LocalDate retrievedDate = rs.getObject("event_date", LocalDate.class);
}
对于传统代码:
java复制// java.util.Date 转 SQL Date
java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
// 时区处理
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"));
Timestamp ts = rs.getTimestamp("create_time", cal);
4.6 分页查询实现
不同数据库的分页语法不同,JDBC需要针对不同数据库实现:
java复制// MySQL分页
String mysqlSql = "SELECT * FROM users LIMIT ?, ?";
PreparedStatement pstmt = conn.prepareStatement(mysqlSql);
pstmt.setInt(1, (pageNum - 1) * pageSize); // offset
pstmt.setInt(2, pageSize); // limit
// Oracle分页
String oracleSql = "SELECT * FROM (" +
" SELECT a.*, ROWNUM rn FROM (" +
" SELECT * FROM users ORDER BY id" +
" ) a WHERE ROWNUM <= ?" +
") WHERE rn > ?";
PreparedStatement pstmt = conn.prepareStatement(oracleSql);
pstmt.setInt(1, pageNum * pageSize);
pstmt.setInt(2, (pageNum - 1) * pageSize);
对于需要兼容多数据库的应用,可以考虑:
- 使用数据库方言检测
- 借助ORM框架的分页功能
- 在内存中分页(小数据集适用)
4.7 存储过程调用
调用存储过程的完整示例:
java复制// 创建存储过程
String createProc = "CREATE PROCEDURE get_employee(IN emp_id INT, OUT emp_name VARCHAR(255)) " +
"BEGIN " +
" SELECT name INTO emp_name FROM employees WHERE id = emp_id; " +
"END";
Statement stmt = conn.createStatement();
stmt.execute(createProc);
// 调用存储过程
CallableStatement cstmt = conn.prepareCall("{call get_employee(?, ?)}");
cstmt.setInt(1, 1001); // 输入参数
cstmt.registerOutParameter(2, Types.VARCHAR); // 输出参数
cstmt.execute();
String empName = cstmt.getString(2); // 获取输出参数
存储过程调用的注意事项:
- 使用registerOutParameter注册输出参数
- 参数索引从1开始
- 调用语法因数据库而异(Oracle使用{},SQL Server可能不同)
- 对于返回多个结果集的情况,需要使用getMoreResults()处理
5. JDBC与现代Java生态
5.1 JDBC与JPA/Hibernate的关系
虽然ORM框架如Hibernate、JPA(如Spring Data JPA)越来越流行,但JDBC仍然是它们的底层基础:
-
性能对比:
- 简单查询:ORM框架有优势(缓存、延迟加载)
- 复杂查询:JDBC通常性能更好
- 批量操作:JDBC明显更快
-
使用场景选择:
- 使用ORM:常规CRUD、对象关系复杂的场景
- 使用JDBC:复杂报表查询、高性能批量操作、存储过程调用
-
混合使用:
java复制@Repository public class UserRepository { @PersistenceContext private EntityManager em; @Autowired private JdbcTemplate jdbcTemplate; // 使用JPA进行常规操作 public User findById(Long id) { return em.find(User.class, id); } // 使用JDBC进行复杂查询 public List<User> findComplexUsers() { String sql = "复杂SQL查询..."; return jdbcTemplate.query(sql, new UserRowMapper()); } }
5.2 Spring JdbcTemplate
Spring的JdbcTemplate是JDBC的一个轻量级封装,解决了原生JDBC的许多痛点:
java复制@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
return user;
});
}
public List<User> findAll() {
return jdbcTemplate.query("SELECT * FROM users", new BeanPropertyRowMapper<>(User.class));
}
public int updateName(Long id, String newName) {
return jdbcTemplate.update(
"UPDATE users SET name = ? WHERE id = ?",
newName, id);
}
}
JdbcTemplate的主要优点:
- 自动资源管理
- 简化的异常处理(将检查异常转为非检查异常)
- 方便的RowMapper机制
- 与Spring事务管理无缝集成
5.3 响应式JDBC
随着响应式编程的兴起,出现了响应式JDBC驱动(如R2DBC):
java复制// 使用R2DBC的示例
ConnectionFactory connectionFactory = ConnectionFactories.get(
"r2dbc:mysql://user:password@localhost:3306/mydb");
Mono.from(connectionFactory.create())
.flatMapMany(conn ->
conn.createStatement("SELECT * FROM users WHERE age > $1")
.bind("$1", 20)
.execute())
.flatMap(result ->
result.map((row, meta) ->
new User(row.get("id", Long.class),
row.get("name", String.class))))
.subscribe(user -> System.out.println(user));
响应式JDBC的特点:
- 非阻塞IO
- 背压支持
- 适合高并发、低延迟场景
- 目前功能还不及传统JDBC完善
5.4 JDBC与微服务
在微服务架构下,JDBC的使用需要考虑:
-
连接池配置:
- 每个服务实例应有独立的连接池
- 根据服务负载动态调整连接池大小
-
分布式事务:
- 避免跨服务的JDBC事务
- 考虑使用Saga模式替代传统事务
-
数据库拆分:
- 每个微服务使用独立数据库
- 可能需要跨库查询的解决方案
-
健康检查:
java复制@Component public class DatabaseHealthIndicator implements HealthIndicator { @Autowired private DataSource dataSource; @Override public Health health() { try (Connection conn = dataSource.getConnection()) { if (conn.isValid(1)) { return Health.up().build(); } return Health.down().build(); } catch (SQLException e) { return Health.down(e).build(); } } }
5.5 JDBC的未来发展
尽管JDBC已经存在了二十多年,但它仍在不断演进:
-
Java 16+的改进:
- 支持Records作为结果集映射目标
- 更好的日期时间API集成
-
云原生支持:
- 自动识别读写分离
- 故障转移支持
-
新协议支持:
- 如MySQL的X Protocol
-
与新语言特性结合:
java复制// 使用文本块简化复杂SQL String sql = """ SELECT u.id, u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.status = 'ACTIVE' GROUP BY u.id, u.name HAVING COUNT(o.id) > 0 ORDER BY order_count DESC """; -
与GraalVM集成:
- 减少反射使用以提高原生镜像兼容性
- 优化驱动加载机制
6. 综合实战案例
6.1 电商订单处理系统
让我们通过一个电商订单处理的完整案例来综合运用JDBC知识:
java复制public class OrderService {
private final DataSource dataSource;
public OrderService(DataSource dataSource) {
this.dataSource = dataSource;
}
@Transactional
public void placeOrder(Order order) throws BusinessException {
try (Connection conn = dataSource.getConnection()) {
// 1. 检查库存
if (!checkInventory(conn, order.getItems())) {
throw new BusinessException("库存不足");
}
// 2. 扣减库存
updateInventory(conn, order.getItems());
// 3. 创建订单
long orderId = createOrder(conn, order);
// 4. 记录支付
recordPayment(conn, orderId, order.getPayment());
// 5. 发送通知
sendNotification(order);
} catch (SQLException e) {
throw new RuntimeException("订单处理失败", e);
}
}
private boolean checkInventory(Connection conn, List<OrderItem> items) throws SQLException {
String sql = "SELECT quantity FROM inventory WHERE product_id = ? FOR UPDATE";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (OrderItem item : items) {
pstmt.setLong(1, item.getProductId());
ResultSet rs = pstmt.executeQuery();
if (!rs.next() || rs.getInt(1) < item.getQuantity()) {
return false;
}
}
return true;
}
}
private void updateInventory(Connection conn, List<OrderItem> items) throws SQLException {
String sql = "UPDATE inventory SET quantity = quantity - ? WHERE product_id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (OrderItem item : items) {
pstmt.setInt(1, item.getQuantity());
pstmt.setLong(2, item.getProductId());
pstmt.addBatch();
}
pstmt.executeBatch();
}
}
private long createOrder(Connection conn, Order order) throws SQLException {
String orderSql = "INSERT INTO orders(user_id, total_amount, status) VALUES(?, ?, 'CREATED')";
String itemSql = "INSERT INTO order_items(order_id, product_id, quantity, price) VALUES(?, ?, ?, ?)";
try (PreparedStatement orderStmt = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS);
PreparedStatement itemStmt = conn.prepareStatement(itemSql)) {
// 插入订单主表
orderStmt.setLong(1, order.getUserId());
orderStmt.setBigDecimal(2, order.getTotalAmount());
orderStmt.executeUpdate();
// 获取生成的订单ID
long orderId;
try (ResultSet rs = orderStmt.getGeneratedKeys()) {
rs.next();
orderId = rs.getLong(1);
}
// 批量插入订单明细
for (OrderItem item : order.getItems()) {
itemStmt.setLong(1, orderId);
itemStmt.setLong(2, item.getProductId());
itemStmt.setInt(3, item.getQuantity());
itemStmt.setBigDecimal(4, item.getPrice());
itemStmt.addBatch();
}
itemStmt.executeBatch();
return orderId;
}
}
private void recordPayment(Connection conn, long orderId, Payment payment) throws SQLException {
String sql = "INSERT INTO payments(order_id, amount, payment_method, transaction_id) VALUES(?, ?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setLong(1, orderId);
pstmt.setBigDecimal(2, payment.getAmount());
pstmt.setString(3, payment.getMethod());
pstmt.setString(4, payment.getTransactionId());
pstmt.executeUpdate();
}
}
private void sendNotification(Order order) {
// 发送通知的实现...
}
}
这个案例展示了:
- 事务管理(@Transactional)
- 批量操作(executeBatch)
- 获取自增ID(RETURN_GENERATED_KEYS)
- 悲观锁(FOR UPDATE)
- 多层异常处理
6.2 数据库迁移工具
使用JDBC实现简单的数据库迁移工具:
java复制public class DatabaseMigrator {
private final DataSource dataSource;
public DatabaseMigrator(DataSource dataSource) {
this.dataSource = dataSource;
}
public void migrate(List<Migration> migrations) {
try (Connection conn = dataSource.getConnection()) {
// 创建迁移记录表
createMigrationTable(conn);
// 获取已执行的迁移
Set<String> executedMigrations = getExecutedMigrations(conn);
// 执行未应用的迁移
for (Migration migration : migrations) {
if (!executedMigrations.contains(migration.getId())) {
executeMigration(conn, migration);
recordMigration(conn, migration);
}
}
} catch (SQLException e) {
throw new RuntimeException("迁移失败", e);
}
}
private void createMigrationTable(Connection conn) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS schema_migrations (" +
"id VARCHAR(255) PRIMARY KEY, " +
"script TEXT NOT NULL, " +
"executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)";
try (Statement stmt = conn.createStatement()) {
stmt.execute(sql);
}
}
private Set<String> getExecutedMigrations(Connection conn) throws SQLException {
Set<String> migrations = new HashSet<>();
String sql = "SELECT id FROM schema_migrations";
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
migrations.add(rs.getString("id"));
}
}
return migrations;
}
private void executeMigration(Connection conn, Migration migration) throws SQLException {
try (Statement stmt = conn.createStatement()) {
// 执行迁移脚本
for (String sql : migration.getScript().split(";")) {
if (!sql.trim().isEmpty()) {
stmt.execute(sql);
}
}
}
}
private void recordMigration(Connection conn, Migration migration) throws SQLException {
String sql = "INSERT INTO schema_migrations(id, script) VALUES(?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, migration.getId());
pstmt.setString(2, migration.getScript());
pstmt.executeUpdate();
}
}
}
这个工具实现了:
- 迁移脚本的版本管理
- 幂等性执行(不会重复执行已应用的迁移)
- 支持多语句脚本
- 迁移记录跟踪
6.3 性能监控工具
使用JDBC实现简单的SQL性能监控:
java复制public class MonitoringDataSource implements DataSource {
private final DataSource delegate;
private final ConcurrentHashMap<String, QueryStats> stats = new ConcurrentHashMap<>();
public MonitoringDataSource(DataSource delegate) {
this.delegate = delegate;
}
@Override
public Connection getConnection() throws SQLException {
return new MonitoringConnection(delegate.getConnection());
}
// 其他DataSource方法实现...
public Map<String, QueryStats> getQueryStats() {
return new HashMap<>(stats);
}
private class MonitoringConnection implements Connection {
private final Connection delegate;
MonitoringConnection(Connection delegate) {
this.delegate = delegate;
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return new MonitoringPreparedStatement(delegate.prepareStatement(sql), sql);
}
// 其他Connection方法委托给delegate...
}
private class MonitoringPreparedStatement implements PreparedStatement {
private final PreparedStatement delegate;
private final String sql;
MonitoringPreparedStatement(PreparedStatement delegate, String sql) {
this.delegate = delegate;
this.sql = sql;
}
@Override
public ResultSet executeQuery() throws SQLException {
long start = System.nanoTime();
try {
return delegate.executeQuery();
} finally {
recordStats(sql, System.nanoTime() - start);
}
}
// 其他PreparedStatement方法委托给delegate...
}
private void recordStats(String sql, long nanos) {
String normalizedSql = normalizeSql(sql);
stats.compute(normalizedSql, (k, v) -> {
if (v == null) {
return new QueryStats(nanos);
}
v.recordExecution(nanos);
return v;
});
}
private String normalizeSql(String sql) {
// 简单的SQL归一化:去除多余空格,忽略参数值
return sql.replaceAll("\\s+", " ").trim();
}
public static class QueryStats {
private long count;
private long totalTimeNanos;
private long minTimeNanos = Long.MAX_VALUE;
private long maxTimeNanos;
QueryStats(long initialTimeNanos) {
recordExecution(initialTimeNanos);
}
void recordExecution(long timeNanos) {
count++;
totalTimeNanos += timeNanos;
minTimeNanos = Math.min(minTimeNanos, timeNanos);
maxTimeNanos = Math.max(maxTimeNanos, timeNanos);
}
// getter方法...
}
}
这个监控工具可以:
- 统计每个SQL的执行次数
- 记录最小/最大/平均执行时间
- 帮助识别性能瓶颈
- 不影响原有业务逻辑
7. 常见问题解决方案
7.1 连接泄漏排查
连接泄漏是JDBC应用中最常见的问题之一。可以通过以下方式排查:
-
监控活跃连接数:
java复制// 对于HikariCP HikariPoolMXBean pool = dataSource.getHikariPoolMXBean(); System.out.println("活跃连接: " + pool.getActiveConnections()); System.out.println("空闲连接: " + pool.getIdleConnections()); -
添加连接泄漏检测:
java复制// HikariCP配置 config.setLeakDetectionThreshold(30000); // 30秒 -
使用JDBC拦截器:
java复制public class LeakDetectionConnection extends DelegatingConnection { private final String stackTrace; public LeakDetectionConnection(Connection delegate) { super(delegate); this.stackTrace = Thread.currentThread().getStackTrace().toString(); } @Override public void close() throws SQLException { super.close(); LeakDetector.releaseConnection(this); } public String getStackTrace() { return stackTrace; } }
7.2 慢查询优化
当遇到慢查询时,可以:
-
使用JDBC超时设置:
java复制Statement stmt = conn.createStatement(); stmt.setQueryTimeout(5); // 5秒超时 -
分析执行计划:
java复制// MySQL获取执行计划 PreparedStatement pstmt = conn.prepareStatement("EXPLAIN " + sql); ResultSet rs = pstmt.executeQuery(); while (rs.next()) { System.out.println(rs.getString("table") + " | " + rs.getString("type") + " | " + rs.getString("key")); } -
批量获取优化:
java复制// Oracle设置预取行数 Statement stmt = conn.createStatement(); stmt.setFetchSize(1000);
7.3 字符编码问题
处理中文乱码的常见解决方案:
-
连接字符串指定编码:
java复制String url = "jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=UTF-8"; -
处理Blob/Clob数据:
java复制// 读取Clob Clob clob = rs.getClob("content"); String content = clob.getSubString(1, (int)clob.length()); // 写入Clob Clob clob = conn.createClob(); clob.setString(1, content); pstmt.setClob(1, clob); -
处理JSON数据:
java复制// 使用Jackson处理JSON字段 ObjectMapper mapper = new ObjectMapper(); String json = rs.getString("json_field"); Map<String, Object> data = mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
7.4 事务隔离问题
解决常见的事务隔离问题:
-
脏读问题:
java复制
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); -
不可重复读问题:
java复制
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); -
幻读问题:
java复制// MySQL需要设置 conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // 或使用SELECT FOR UPDATE PreparedStatement pstmt = conn.prepareStatement( "SELECT * FROM orders WHERE user_id = ? FOR UPDATE");
7.5 连接池配置
优化连接池配置的实践经验:
- HikariCP推荐配置:
java复制
config.set
