目录
关系数据库
数据库
程序运行的时候,数据都是在内存中的,当程序终止的时候,通常都需要将数据保存到磁盘上,无论是保存到本地磁盘,还是通过网络保存到服务器上,最终都会将数据写入磁盘文件。
而如何定义数据的存储格式就是一个大问题。
假设我们要存储一个班级所有学生的成绩单!
-
用现有的一些计算机软件,比如 Excel ,那么就会是这样的:
-
如果用一个文本文件保存,那有可能会是这样的:
名字,成绩 小明,90 小红,100 小亮,89 小兰,85 小黑,93
-
如果用 JSON 格式保存,可能是这样的:
[ {"name": "小明", "score": 90}, {"name": "小红", "score": 100}, {"name": "小亮", "score": 89}, {"name": "小兰", "score": 85}, {"name": "小黑", "score": 93} ]
除了以上几种,还可以定义各种各样的保存格式!
但是,如果按照我们自己定义的格式保存数据的话,存储和读取就全都需要我们自己来实现。
这其中,如果采用 JSON 格式的话还好些,毕竟 JSON 格式已经很流行了,但是如果采用一些比较“个性”的格式的话,那简直👇
要知道,一般情况下,我们在程序中读取文件中的数据时,都是先将数据全部读取到内存中,然后再从其中筛选我们需要的数据。但是,如果数据的大小远远超过了内存的大小(比如好几十GB的数据),那怎么办?
于是,为了解决这种混乱的情况,数据库(Database) 出现了。
那么,什么是数据库呢?
- 简单点来说,数据库就是数据的仓库。
- 不过,与我们自己 DIY 的不同,数据库依据某种特定的「数据结构」 来组织(存储)数据。
- 也就是说,事实上,数据库就是一个用于保存有组织结构的数据的容器(通常是一个或一组文件)
而大多时候,我们所说的数据库,指的都是数据库管理系统(DBMS,Database Management System)。
- 数据库是通过 DBMS 创建和操纵的容器。
- DBMS 用于创建、操作数据库。
关系型数据库
按照不同的「数据结构」,数据库有很多种类型,包括以前的网状数据库、层次数据库等等很多类型。
但是,通过数十年的发展,现在数据库分为两种类型:
- 关系型数据库
- 非关系型数据库
那么,什么是关系型数据库呢?
关系型数据库是依据关系模型来创建的数据库。
而关系模型就是「一对一、一对多、多对多」等关系模型,再直白点,就是二维表格模型,因此,一个关系型数据库就是由二维表及其之间的联系组成的一个数据组织。
举个🌰:
假设XX省YY市ZZ县第一实验小学有 3 个年级,要表示出这 3 个年级,可以用一张表:
每个年级又有若干个班级,要把所有班级表示出来,再来一个表:
这两个表格之间有一个映射关系,就是根据 Grade_ID 可以在班级表中查找到对应的所有班级:
Grade 表的每一行对应 Class 表的多行,在关系数据库中,这种基于表(Table)的一对多(以及一对一、多对多)的关系就是关系数据库的基础。
而在关系型数据库中,有一些比较常用的概念:
-
表:表示数据的二维表,也就是关系数据库的关系,每个关系都具有一个关系名,也就是表名
-
行:表中的一个记录,也就是一条数据
-
列:表由列组成,列为表中的一个字段
-
常见的关系型数据库有:Oracle、SQL Server、MySQL、DB2 等
-
常见的非关系型数据库(也叫 NoSQL )有:Hbase、MongoDB、Redis 等
JDBC
了解了什么是关系型数据库后,我们就来了解一下什么是 JDBC!
JDBC(Java Database Connectivity),是 Java 程序访问数据库的标准接口。
我们前面说了,我们使用数据库来存储和读取数据,那么,在 Java 程序中,Java 是怎么访问数据库的呢?
使用 Java 程序访问数据库时,Java 代码并不是直接通过 TCP 连接去访问数据库,而是通过 JDBC 接口来访问,而 JDBC 接口则通过 JDBC 驱动来实现真正对数据库的访问。
举个🌰:
我们在 Java 代码中如果要访问 MySQL,那么必须编写代码操作 JDBC 接口。
注意:JDBC 接口是 Java 标准库自带的,所以可以直接编译;而具体的 JDBC 驱动是由数据库厂商提供的。
因此,访问某个具体的数据库,我们只需要引入该厂商提供的 JDBC 驱动,就可以通过 JDBC 接口来访问,这样保证了 Java 程序编写的是一套数据库访问代码,却可以访问各种不同的数据库,因为他们都提供了标准的 JDBC 驱动:
实际上,一个 MySQL 的 JDBC 的驱动就是一个 jar 包,它本身也是纯 Java 编写的。我们自己编写的代码只需要引用 Java 标准库提供的 java.sql
包下面的相关接口,由此再间接地通过 MySQL 驱动的 jar 包通过网络访问 MySQL 服务器,所有复杂的网络通讯都被封装到 JDBC 驱动中,因此,Java 程序本身只需要引入一个 MySQL 驱动的 jar 包就可以正常访问 MySQL 服务器:
使用JDBC的好处:
- 各数据库厂商使用相同的接口,Java 代码不需要针对不同数据库分别开发
- Java 程序编译期仅依赖
java.sql
包,不依赖具体的数据库驱动 jar 包 - 可随时替换底层数据库,访问数据库的 Java 代码基本不变
JDBC 查询
引入 JDBC
前面说了 JDBC 是一套 Java 提供的接口,放在 java.sql
包里,而要使用数据库,就要引入厂商的驱动包!
而作为 Java 程序员,大多数时候我们使用的数据库都是 MySQL。
而要连接上 MySQL 数据库,我们则要引入一个MySQL 驱动的依赖。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.7.31</version>
<scope>runtime</scope>
</dependency>
注意:JDBC 驱动只在运行时需要,在编译时并不需要,因此添加依赖时的 scope
是 runtime
。
JDBC 连接
引入 MySQL 的依赖是为了在运行时真正连接到数据库,但至于怎么连,这却是要在代码层面就提前写好的。
而在 JDBC 中,Connection
代表一个 JDBC 连接,它相当于运行时 Java 程序到数据库的连接(通常是 TCP 连接)。
而要创建一个 Connection(JDBC连接),则需要几个参数:
- URL (数据库厂商指定的格式《、u>,用于区分不同的数据库厂商,以及寻找对应的数据库驱动)
- 用户名
- 密码
对于 MySQL 来说,它的URL 是这样的:
jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2...
例如:
jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8
useSSL=false
表示不使用SSL加密characterEncoding=utf8
代表使用utf8作为字符编码
而真正的 Java 代码,则需要这样写:
String JDBC_URL = "jdbc:mysql://localhost:3306/test"
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取数据库连接
Connection connection = DriverManager.getConnction(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库
// 关闭连接
connection.close();
可以看到,在上面的代码中,最关键的就是 DriverManager.getConnction(...)
。
DriverManager
是 java.sql
提供的,它会自动扫描 classpath,找到所有的 JDBC 驱动,然后根据传入 getConnection
方法的参数 URL 自动挑选一个合适的驱动。
同时,需要注意的是:JDBC 连接是一种非常昂贵的资源,所以使用后要及时释放(connection.close();
)。
注意:可以使用 JDK 7 引入的 「try-with-resources」语法
try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
// TODO: ...
}
JDBC 查询
连接到数据库后,就可以从数据库中查询数据了。
查询数据库分为以下几个步骤:
- 通过
Connection
提供的createStatement()
方法创建一个Statement
对象,用于执行一个 SQL 语句 - 通过
Statement
对象提供的executeQuery("SELECT * FROM students")
方法,执行查询并获得返回的结果集,结果集类型为ResultSet
- 反复调用
ResultSet
对象的next()
方法并读取每一行结果
示例代码如下:
import java.sql.*;
public class JDBCBasic {
private static final String URL = "jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8";
private static final String USER = "learn";
private static final String PASSWORD = "learnpassword";
public static void main(String[] args) throws SQLException {
System.out.println("id\tname\tgender\tgrade\tscore");
try(Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
try(Statement statement = connection.createStatement()) {
try(ResultSet resultSet = statement.executeQuery("SELECT * FROM students")) {
while (resultSet.next()) {
long id = resultSet.getLong(1);
String name = resultSet.getString(2);
boolean gender = resultSet.getBoolean(3);
int grade = resultSet.getInt(4);
int score = resultSet.getInt(5);
System.out.println("" + id + "\t" + name + "\t" + (gender ? 1 : 0) + "\t" + grade + "\t" + score);
}
}
}
}
}
}
执行结果如下:
注意:
Statement
和ResultSet
与Connection
一样,都是需要关闭的资源,可以使用try-with-resources
语法,确保及时关闭资源- 对于 「try-with-resources」 语法,从 JDK 9 后,如果已经有一个资源是
final
或等效于final
变量,就可以在 「try-with-resources」 语句中使用该变量,而无需在 「try-with-resources」 语句中声明一个新变量
- 对于 「try-with-resources」 语法,从 JDK 9 后,如果已经有一个资源是
resultSet.next()
用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet
时当前行不是第一行)ResultSet
获取列时,索引从 1 开始,而不是 0- 必须根据 SELECT 的列的对应位置来调用 getLong(1),getString(2) 等方法,否则对应位置的数据类型不对,将报错
SQL 注入
什么是 SQL 注入呢?
用户输入的内容, 在 SQL 语句拼接过程中变成了一条新的不同逻辑的 SQL 语句,从而成为黑客攻击后台的方式,被成为 SQL 注入。
而在 JDBC 中,同样可能引发 SQL 注入的问题。
来看一个🌰:
- 为了更好地说明问题,先来做一些假数据
- 模拟用户登录的场景,userName 和 password 通过前端页面发送至后端,由后端进行拼接成SQL语句
可以看到,拼接成的SQL语句是:SELECT * FROM users WHERE name='小明' AND password='4cf350692a4a3bb54d13daacfe8c683b'
- 我们将拼接成的SQL语句去执行一下,看看结果:
可以看到,成功拿到正确的结果- 模拟一个居心叵测的用户,来精心构造一个假的SQL语句:
可以看到,拼接成的SQL语句为:SELECT * FROM users WHERE name='小明' OR password='' AND password='I dont care'
- 将拼接成的居心叵测的SQL语句执行一下,看看结果:
如您所见,同样可以拿到结果
相信通过上述的例子,SQL 注入是什么应该很清晰了!
同时,通过上述的例子也可以看到,一个被精心构造出来的 SQL 语句,完全可以绕过一些不安全的代码漏洞,达到攻击的目的!
那么,在 JDBC 中,我们怎么防止 SQL 注入这种行为呢?
一种非常有效、也很简单的办法就是,在代码中不要使用 Statement
,而是使用 PreparedStatement
。
使用 PreparedStatement
可以完全避免 SQL 注入的问题,因为 PreparedStatement
始终使用 ?
作为占位符,并且把数据连同 SQL 本身传给数据库,这样可以保证每次传给数据库的 SQL 语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。
而我们之前例子中的拼接 SQL 语句的代码也可以改成这样:👇
import java.sql.*;
public class SafeSQL {
private static final String URL = "jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8";
private static final String USER = "learn";
private static final String PASSWORD = "learnpassword";
public static void main(String[] args) throws SQLException {
String sql = "SELECT * FROM users WHERE name=? AND password=?";
String userName = "小明";
String password = "4cf350692a4a3bb54d13daacfe8c683b";
try(Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
try(PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setObject(1, userName);
preparedStatement.setObject(2, password);
try(ResultSet resultSet = preparedStatement.executeQuery()) {
while(resultSet.next()) {
String name = resultSet.getString("name");
String pass = resultSet.getString("password");
System.out.println("name:" + name + ",\tpassword:" + pass);
}
}
}
}
}
}
而执行结果是这样的:
注意:
PreparedStatement
和Statement
不同,必须首先调用setObject()
设置每个占位符?
的值,最后获取的仍然是ResultSet
对象- 而且从结果集读取列时,使用
String
类型的列名比索引要易读,而且不易出错- JDBC 查询的返回值总是
ResultSet
,即使我们写这样的聚合查询SELECT SUM(score) FROM ...
,也需要按结果集读取
- JDBC 查询的返回值总是
PreparedStatement
比Statement
更安全,而且更快
因此,使用 Java 对数据库进行操作时,必须使用 PreparedStatement
,严禁任何通过参数拼字符串的代码!
至于为什么 PrepareStatement 可以防止 SQL 注入,可以参考:PreparedStatement是如何防止SQL注入的?
写的非常清楚👍👍
同时,关于 SQL 列类型和 Java 数据类型的映射,可以参考:java.sql.Types,数据库字段类型,java数据类型的对应关系
JDBC 更新
数据库操作总结起来就四个字:增删改查,行话叫 CRUD:Create
、Retrieve
、Update
,Delete
。
插入
插入操作是 INSERT,即插入一条新记录。
通过 JDBC 进行数据插入,本质上也是执行一条 SQL 语句,不过执行的不是 executeQuery()
,而是 executeUpdate()
。
当成功执行 executeUpdate()
后,返回值是 int
,表示插入的记录数量。
import java.sql.*;
public class JDBCInsert {
private static final String URL = "jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8";
private static final String USER = "learn";
private static final String PASSWORD = "learnpassword";
public static void main(String[] args) throws SQLException {
String sql = "INSERT INTO students (id, grade, name, gender, score) VALUES (?,?,?,?,?)";
try(Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
try(PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setObject(1, 999);
preparedStatement.setObject(2, 1);
preparedStatement.setObject(3, "Rose");
preparedStatement.setObject(4, 0);
preparedStatement.setObject(5, 87);
final int lineNumber = preparedStatement.executeUpdate();
System.out.println(lineNumber);
}
}
}
}
执行结果如下:
插入并获取主键
如果数据库的表设置了自增主键,那么在执行 INSERT 语句时,并不需要指定主键,数据库会自动分配主键。
对于使用自增主键的程序,有个额外的步骤,就是如何获取插入后的自增主键的值。
要获取自增主键,不能先插入,再查询,因为两条 SQL 执行期间可能有别的程序也插入了同一个表。
获取自增主键的正确写法是在创建 PreparedStatement
的时候,指定一个 RETURN_GENERATED_KEYS
标志位,表示 JDBC 驱动必须返回插入的自增主键。
import java.sql.*;
public class JDBCInsertAutoIncreased {
private static final String URL = "jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8";
private static final String USER = "learn";
private static final String PASSWORD = "learnpassword";
public static void main(String[] args) throws SQLException {
String sql = "INSERT INTO students (grade, name, gender, score) VALUES (?,?,?,?)";
try(Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
try(PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setObject(1, 1);
preparedStatement.setObject(2, "Rose");
preparedStatement.setObject(3, 0);
preparedStatement.setObject(4, 87);
final int lineNumber = preparedStatement.executeUpdate();
System.out.println(lineNumber);
try(ResultSet generatedKeys = preparedStatement.getGeneratedKeys()) {
while(generatedKeys.next()) {
System.out.println(generatedKeys.getLong(1));
}
}
}
}
}
}
执行结果如下:
注意:
- 要插入数据并获取自增主键,调用
prepareStatement()
时,第二个参数必须传入常量Statement.RETURN_GENERATED_KEYS
,否则 JDBC 驱动不会返回自增主键 - 执行
executeUpdate()
方法后,必须调用getGeneratedKeys()
获取一个ResultSet
对象,这个对象包含了数据库自动生成的主键的值,读取该对象的每一行来获取自增主键的值。- 如果一次插入多条记录,那么这个
ResultSet
对象就会有多行返回值 - 如果插入时有多列自增,那么
ResultSet
对象的每一行都会对应多个自增值(自增列不一定必须是主键)
- 如果一次插入多条记录,那么这个
更新 & 删除
更新和删除操作与插入操作几乎没区别,仅仅是执行的 SQL 语句不一样
String insertSql = "INSERT INTO students (grade, name, gender, score) VALUES (?,?,?,?)";
String updateSql = "UPDATE students SET name=? WHERE id=?";
String deleteSql = "DELETE FROM students WHERE id=?"
JDBC 事务
数据库事务是由若干条 SQL 语句组成的一个操作序列。
在 JDBC 中,同样可以执行事务,来看下面👇的🌰:
import java.sql.*;
public class JDBCTransaction {
private static final String URL = "jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8";
private static final String USER = "learn";
private static final String PASSWORD = "learnpassword";
public static void main(String[] args) throws SQLException {
String selectSql = "SELECT * FROM students WHERE id=?";
String updateSql = "UPDATE students SET name=? WHERE id=?";
try(Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);) {
try {
// 关闭自动提交
connection.setAutoCommit(false);
select(selectSql, connection); // 查询
update(updateSql, connection); // 更新
select(selectSql, connection); // 再次查询
// 提交事务
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
// 回滚事务
connection.rollback();
} finally {
// 打开默认自动提交
connection.setAutoCommit(true);
}
}
}
private static void select(String selectSql, Connection connection) throws SQLException {
try(PreparedStatement preparedStatement = connection.prepareStatement(selectSql)) {
preparedStatement.setObject(1, 13);
try(final ResultSet resultSet = preparedStatement.executeQuery()) {
while(resultSet.next()) {
long id = resultSet.getLong(1);
String name = resultSet.getString(2);
System.out.println("id:" + id + "\nname:" + name);
}
}
}
}
private static void update(String updateSql, Connection connection) throws SQLException {
try(PreparedStatement preparedStatement = connection.prepareStatement(updateSql)) {
preparedStatement.setObject(1, "黑神");
preparedStatement.setObject(2, 13);
preparedStatement.executeUpdate();
}
}
}
执行结果如下:
在 JDBC 中,操作事务的关键代码如下:
作用 | 代码 | 描述 |
---|---|---|
开启事务 | connection.setAutoCommit(false); | |
提交事务 | connection.commit(); | |
回滚事务 | connection.rollback(); | |
关闭事务 | connection.setAutoCommit(true); | 默认情况下,我们获取到Connection连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行的,即“隐式事务”。要执行“显示事务”,需要通过关闭自动提交来达到目的,但需要注意的是,在执行显示事务完毕后,需要将自动提交回复至默认状态。 |
设置事务隔离级别 | connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); |
批量操作
使用 JDBC 操作数据库的时候,经常会执行一些批量操作。
比如要表中添加多个用户,我们可以执行以下SQL语句:
INSERT INTO students (id, name, gender, grade, score) VALUES (14, "小A", 1, 1, 80);
INSERT INTO students (id, name, gender, grade, score) VALUES (15, "小B", 1, 1, 80);
INSERT INTO students (id, name, gender, grade, score) VALUES (16, "小C", 1, 1, 80);
INSERT INTO students (id, name, gender, grade, score) VALUES (17, "小D", 1, 1, 80);
...
而在实际执行 JDBC 代码时,由于只有占位符参数不同,所以执行的 SQL 实际上是一样的,我们很可能会想到用一个循环来完成:
for (var params : paramsList) {
PreparedStatement ps = conn.preparedStatement("INSERT INTO students (id, name, gender, grade, score) VALUES (?,?,?,?,?)");
ps.setLong(params.get(0));
ps.setString(params.get(1));
ps.setInt(params.get(2));
ps.setInt(params.get(3));
ps.setInt(params.get(4));
ps.executeUpdate();
}
通过一个循环来执行每个 PreparedStatement
虽然可行,但是性能很低。
SQL 数据库对 SQL 语句相同,但只有参数不同的若干语句可以作为 batch 执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个 SQL。
在 JDBC 代码中,我们可以利用 SQL 数据库的这一特性,把同一个 SQL 但参数不同的若干次操作合并为一个 batch 执行。
我们以批量插入为例,示例代码如下:
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
// 对同一个PreparedStatement反复设置参数并调用addBatch():
for (Student s : students) {
ps.setString(1, s.name);
ps.setBoolean(2, s.gender);
ps.setInt(3, s.grade);
ps.setInt(4, s.score);
ps.addBatch(); // 添加到batch
}
// 执行batch:
int[] ns = ps.executeBatch();
for (int n : ns) {
System.out.println(n + " inserted."); // batch中每个SQL执行的结果数量
}
}
JDBC 连接池
事实上,JDBC 连接池的目的和线程池的目的是一致的。
创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。
类似的,在执行 JDBC 的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁 JDBC 连接的开销就太大了。
为了避免频繁地创建和销毁 JDBC 连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。
JDBC 连接池有一个标准的接口 javax.sql.DataSource
,但仅仅是接口,要使用 JDBC 连接池,必须选择一个具体的连接池实现。
常见的 JDBC 连接池有:
- C3p0:开源的,成熟的,高并发第三方数据库连接池
- Druid:阿里巴巴开源的连接池,号称是 Java 语言中最好的数据库连接池
- HiKariCP:一个非常轻巧的连接池,性能也非常好,完虐 c3p0,dbcp 等,目前使用最广泛的连接池
以 HiKariCP 为例,要使用 JDBC 连接池,首先需要引入依赖,然后就是代码层面的实现:
- 创建一个连接池
DataSource
实例
注意:HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/test"); config.setUsername("root"); config.setPassword("password"); config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒 config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒 config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10 DataSource ds = new HikariDataSource(config);
创建 DataSource 也是一个非常昂贵的操作,所以通常 DataSource 实例总是作为一个全局变量存储,并贯穿整个应用程序的生命周期。 - 获取数据库连接
和纯 JDBC 类似,只是不再需要 DriverManager 了,因为 JDBC 相关的 URL、用户名、密码等都已经存储在连接池内部了。try(Connection connection = ds.getConnection()) { ... } // 关闭连接 👉 connection.close()
还有,一开始连接池内部并没有任何连接实例,只有在第一次获取连接时,才会在连接池内部创建一个实例并返回 - 执行操作
- 关闭连接
依旧是使用connection.close()
,只是当我们调用connection.close()
方法时,不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。
因此,连接池内部维护了若干个 Connection 实例,如果调用 ds.getConnection()
,就选择一个空闲连接,并标记它为「正在使用」然后返回,如果对 Connection 调用 close()
,那么就把连接再次标记为「空闲」从而等待下次调用。
这样一来,我们就通过连接池维护了少量连接,但可以频繁地执行大量的 SQL 语句。
通常连接池提供了大量的参数可以配置,例如,维护的最小、最大活动连接数,指定一个连接在空闲一段时间后自动关闭等,需要根据应用程序的负载合理地配置这些参数。
此外,大多数连接池都提供了详细的实时状态以便进行监控。