一、JDBC 事务处理
1.1 事务基础
数据一旦提交,就不可回滚。
数据什么时候意味着提交?
- 当一个连接对象被创建时,默认情况下是自动提交事务:每次执行一个 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚
- 关闭数据库连接,数据就会自动的提交。如果多个操作每个使用自己单独的连接,则无法保证事务
核心原则:同一个事务的多个操作必须在同一个 Connection 对象下执行。
1.2 JDBC 事务 API
| 方法 |
说明 |
conn.setAutoCommit(false) |
取消自动提交,开启事务 |
conn.commit() |
提交事务 |
conn.rollback() |
回滚事务 |
conn.setSavepoint() |
设置保存点 |
conn.rollback(Savepoint) |
回滚到指定保存点 |
1 2 3 4 5 6 7 8 9 10
| 开启事务 setAutoCommit(false) │ ├── SQL 1 执行成功 ├── SQL 2 执行成功 ├── SQL 3 执行失败 ✗ │ ├── 有异常 → rollback() 回滚所有操作 │ └── 全部成功 → commit() 提交事务
|
1.3 事务代码模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); conn.commit(); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } e.printStackTrace(); } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } DBUtil.closeResource(null, null, conn); }
|
注意:若 Connection 没有被关闭,还可能被重复使用(如连接池场景),则需要恢复其自动提交状态 setAutoCommit(true)。执行 close() 方法前,建议恢复自动提交状态。
二、事务案例:银行转账
2.1 创建数据库表
1 2 3 4 5 6 7 8 9
| CREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, balance DECIMAL(10, 2) DEFAULT 0.00 );
INSERT INTO account (name, balance) VALUES ('A', 1000); INSERT INTO account (name, balance) VALUES ('B', 1000);
|
2.2 实体类
1 2 3 4 5 6 7 8 9 10
| package com.qiufenfen.entity;
import lombok.Data;
@Data public class Account { private Integer id; private String name; private Double balance; }
|
2.3 DAO 层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public interface AccountDAO { Account findByName(Connection conn, String name) throws SQLException; void updateBalance(Connection conn, String name, double balance) throws SQLException; }
public class AccountDAOImpl implements AccountDAO { @Override public Account findByName(Connection conn, String name) throws SQLException { String sql = "SELECT id, name, balance FROM account WHERE name = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, name); ResultSet rs = ps.executeQuery(); Account account = null; if (rs.next()) { account = new Account(); account.setId(rs.getInt("id")); account.setName(rs.getString("name")); account.setBalance(rs.getDouble("balance")); } ps.close(); rs.close(); return account; } @Override public void updateBalance(Connection conn, String name, double balance) throws SQLException { String sql = "UPDATE account SET balance = ? WHERE name = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setDouble(1, balance); ps.setString(2, name); ps.executeUpdate(); ps.close(); } }
|
2.4 Service 层(事务控制核心)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| public interface AccountService {
void transfer(String fromName, String toName, double amount); }
public class AccountServiceImpl implements AccountService { private AccountDAO accountDAO = new AccountDAOImpl(); @Override public void transfer(String fromName, String toName, double amount) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); Account fromAccount = accountDAO.findByName(conn, fromName); if (fromAccount == null) { throw new RuntimeException("转出账户不存在"); } if (fromAccount.getBalance() < amount) { throw new RuntimeException("余额不足"); } Account toAccount = accountDAO.findByName(conn, toName); if (toAccount == null) { throw new RuntimeException("转入账户不存在"); } accountDAO.updateBalance(conn, fromName, fromAccount.getBalance() - amount); accountDAO.updateBalance(conn, toName, toAccount.getBalance() + amount); conn.commit(); System.out.println("转账成功!"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); System.out.println("转账失败,已回滚"); } catch (SQLException e1) { e1.printStackTrace(); } } e.printStackTrace(); } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } DBUtil.closeResource(null, null, conn); } } }
|
2.5 测试
1 2 3 4 5 6
| @Test public void testTransfer() { AccountService service = new AccountServiceImpl(); service.transfer("A", "B", 100); }
|
关键点: 事务必须使用同一个连接对象(Connection)才能保证 ACID 特性。DAO 层的方法接收外部传入的 Connection,而不是自己创建。
三、数据库连接池
3.1 传统模式的弊端
1 2 3 4 5 6 7 8 9 10 11
| 传统模式: 1. 在主程序中建立数据库连接 2. 进行 SQL 操作 3. 断开数据库连接
问题: - 每次建立连接都要验证用户名和密码(0.05s~1s),消耗大量资源和时间 - 连接资源没有得到很好的重复利用 - 频繁连接操作将占用大量系统资源,可能导致服务器崩溃 - 每次连接使用完必须断开,否则会导致内存泄漏 - 无法控制创建的连接对象数,可能导致内存泄漏、服务器崩溃
|
3.2 连接池技术
基本思想: 为数据库连接建立一个”缓冲池”。预先在缓冲池中放入一定数量的连接,需要时从”缓冲池”中取出一个,使用完毕后再放回去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ┌─────────────────────────────────────────────┐ │ 数据库连接池 │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │连接1│ │连接2│ │连接3│ │连接4│ │连接5│ │ │ └────┘ └────┘ └────┘ └────┘ └────┘ │ │ │ │ 最小连接数: 2 最大连接数: 10 │ ├─────────────────────────────────────────────┤ │ 应用程序 │ │ 1. 请求连接 → 从池中获取 │ │ 2. 使用连接 → 执行 SQL │ │ 3. 归还连接 → 放回池中(不是物理关闭) │ └─────────────────────────────────────────────┘
|
3.3 工作原理
连接池的工作原理由三部分组成:
- 连接池的建立:系统初始化时,连接池根据配置建立,并在池中创建几个连接对象
- 连接池的管理:
- 客户请求连接时,查看是否有空闲连接 → 有则分配,无则检查是否达到最大连接数
- 未达到最大 → 创建新连接;已达到 → 等待,超时则抛异常
- 客户释放连接时,连接归还到池中供其他客户使用
- 连接池的关闭:应用程序退出时,关闭所有连接,释放资源
3.4 连接池的优点
- 避免数据库连接频繁创建和销毁,节省系统开销
- 提高数据库访问速度,加快响应
- 有效控制连接数量,防止资源耗尽
3.5 常用连接池
| 连接池 |
说明 |
| DBCP |
Apache 提供,Tomcat 自带,速度较快但有 BUG |
| C3P0 |
开源组织提供,速度较慢,稳定性还可以,Hibernate 推荐 |
| Druid |
阿里巴巴提供,集 DBCP、C3P0、Proxool 优点于一身,自带日志监控 |
| HikariCP |
号称业界跑得最快的连接池,Spring Boot 2.0+ 默认 |
DataSource 通常被称为数据源,包含连接池和连接池管理两部分。DataSource 用来取代 DriverManager 获取 Connection。
特别注意:
- 数据源无需创建多个,整个应用只需要一个数据源
conn.close() 并没有关闭物理连接,只是把连接释放归还给连接池
四、Druid 连接池
4.1 添加依赖
1 2 3 4 5 6
| <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>
|
4.2 配置文件 druid.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| initialSize=5
maxActive=20
minIdle=5
maxWait=3000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
validationQuery=SELECT 1
testWhileIdle=true testOnBorrow=false testOnReturn=false
poolPreparedStatements=true maxOpenPreparedStatements=20
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/qiufenfen_db?useSSL=false&serverTimezone=UTC username=root password=123456
|
4.3 Druid 工具类封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| package com.qiufenfen.util;
import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.*; import java.util.Properties;
public class DruidUtil {
private static DataSource dataSource;
static { try { Properties props = new Properties(); InputStream is = DruidUtil.class.getClassLoader() .getResourceAsStream("druid.properties"); props.load(is);
dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } }
public static DataSource getDataSource() { return dataSource; }
public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }
public static void closeResource(ResultSet rs, Statement ps, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps != null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
|
4.4 Druid 配置参数详解
| 参数 |
说明 |
推荐 |
initialSize |
初始化连接数。第一次 getConnection 时创建,非应用启动时 |
5 |
maxActive |
最大连接数 |
20 |
minIdle |
最小空闲连接数 |
5 |
maxWait |
获取连接的超时等待时间(毫秒),0 表示无限等待 |
1200(内网) |
validationQuery |
验证连接是否有效的 SQL(SELECT 1) |
SELECT 1 |
testWhileIdle |
是否检查空闲连接有效性 |
true |
testOnBorrow |
获取连接时是否检查有效性 |
false |
testOnReturn |
归还连接时是否检查有效性 |
false |
timeBetweenEvictionRunsMillis |
检查空闲连接的间隔(毫秒) |
60000 |
minEvictableIdleTimeMillis |
连接可空闲的最小时间 |
300000 |
poolPreparedStatements |
是否缓存 PreparedStatement |
true |
maxWait 配置不当的案例:
1 2 3 4 5 6 7 8
| maxWait=0(不设等待超时) → 突发大流量涌入,连接池耗尽 → 所有请求无限等待 → 等待队列越排越长 → 接口大量超时,打满线程 → 服务不可用
推荐配置:maxWait=1200(内网网络状况好时)
|
五、HikariCP 连接池
5.1 添加依赖
1 2 3 4 5 6
| <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.1.0</version> </dependency>
|
5.2 配置与使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| package com.qiufenfen.util;
import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.*;
public class HikariUtil {
private static HikariDataSource dataSource;
static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/qiufenfen_db?useSSL=false&serverTimezone=UTC"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(3000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); dataSource = new HikariDataSource(config); }
public static DataSource getDataSource() { return dataSource; }
public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }
public static void closeResource(ResultSet rs, Statement ps, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps != null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
|
5.3 HikariCP 配置参数
| 参数 |
说明 |
jdbcUrl |
数据库连接 URL |
username |
用户名 |
password |
密码 |
maximumPoolSize |
连接池最大连接数 |
minimumIdle |
最小空闲连接数 |
connectionTimeout |
连接超时时间(毫秒) |
idleTimeout |
空闲连接超时时间(毫秒) |
maxLifetime |
连接最大生命周期(毫秒) |
六、ThreadLocal
6.1 简介
ThreadLocal 是 Java 提供的线程本地存储类,为每个使用该变量的线程提供独立的变量副本。
主要用途:
- 线程隔离:在多线程环境下,某些数据不应该被多个线程共享,使用 ThreadLocal 避免数据在多线程共享
- 线程内共享:在单个线程内部隐式共享数据
6.2 ThreadLocal 案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class ThreadLocalDemo { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) { Thread t1 = new Thread(() -> { threadLocal.set(100); System.out.println("t1: " + threadLocal.get()); threadLocal.remove(); });
Thread t2 = new Thread(() -> { threadLocal.set(200); System.out.println("t2: " + threadLocal.get()); threadLocal.remove(); });
t1.start(); t2.start(); } }
|
6.3 ThreadLocal 在 JDBC 中的应用
场景: 同一个线程的不同方法获取同一个 Connection(用于事务控制)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.qiufenfen.util;
import java.sql.*;
public class ConnectionUtil {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
public static Connection getConnection() throws SQLException { Connection conn = threadLocal.get(); if (conn == null || conn.isClosed()) { conn = DruidUtil.getConnection(); threadLocal.set(conn); } return conn; }
public static void closeConnection() { Connection conn = threadLocal.get(); if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } threadLocal.remove(); } } }
|
ThreadLocal 保证同一个线程在 DAO 层和 Service 层使用的是同一个 Connection,从而保证事务的 ACID 特性。
七、连接池关键问题分析
7.1 并发问题
使用 synchronized 确保线程同步:
1 2 3
| public synchronized Connection getConnection() { }
|
7.2 事务处理
Connection 本身提供事务支持:
1 2 3
| conn.setAutoCommit(false);
conn.commit();
|
每个事务独占一个连接,可以大大降低事务管理的复杂性。
7.3 连接池分配与释放
1 2 3 4 5 6 7 8 9
| 请求连接时: 1. 检查空闲池是否有空闲连接 有 → 分配建立时间最长的连接(先检查是否有效) 无 → 检查是否达到最大连接数 未达到 → 新建连接 已达到 → 等待(timeout),超时返回 null
释放连接时: 归还到空闲池,供其他请求使用
|
7.4 连接池配置建议
- 最小连接数:开发时设小(启动快),生产时设大(响应快)
- 最大连接数:根据系统访问量反复测试找到最佳点
- 动态策略:每隔一定时间检查连接数,小于最小连接数时补充
💡 小结:本章深入讲解了 JDBC 事务管理和数据库连接池。事务部分包括 setAutoCommit、commit、rollback 的使用,通过银行转账案例展示了事务控制的完整流程。连接池部分介绍了 Druid 和 HikariCP 两种主流连接池的配置与封装,以及连接池工作原理和参数调优。最后讲解了 ThreadLocal 在 JDBC 事务中的应用,确保同一线程使用同一连接。