sql和JDBC事务连接池
sql 和JDBC事务连接池
数据库?
数据库本质是一个数据仓库系统,用于持久化存储数据。
核心特点:
- 数据结构化存储(表)
- 支持 SQL 操作
- 支持并发访问
语言分类
| 分类 | 含义 | 常见语句 |
|---|---|---|
| DDL | 定义结构 | create / alter / drop |
| DML | 操作数据 | insert / update / delete |
| DQL | 查询数据 | select |
| DCL | 控制权限 | grant / revoke |
SQL 核心
-- 创建数据库
create database mydb;
-- 使用数据库
use mydb;
-- 查看数据库
show databases;
-- 删除数据库
drop database mydb;
-- 创建表
create table user(
id int primary key auto_increment,
username varchar(30),
password varchar(30)
);
-- 查看表结构
desc user;
-- 删除表
drop table user;
数据类型
| 类型 | 示例 |
|---|---|
| 字符串 | varchar / char |
| 数值 | int / double |
| 日期 | date / datetime |
| 大文本 | text |
varchar:可变长度(推荐)
char:固定长度(性能略高)
插入
insert into user(username,password) values ('aaa','123');
修改
update user set password = '456' where username = 'aaa';
删除
delete from user where username = 'aaa';
查询(重点)
-- 查询全部
select * from user;
-- 条件查询
select * from user where id > 1;
-- 模糊查询
select * from user where username like '%a%';
-- 排序
select * from user order by id desc;
聚合函数
select count(*) from user;
select avg(score) from stu;
select sum(score) from stu;
分组查询(非常重要)
select product, sum(price)
from orders
group by product
having sum(price) > 100;
JDBC 基础
JDBC( Java ** Database Connectivity):
本质是一套 接口规范
- Java 定义接口
- 数据库厂商实现(MySQL驱动)
核心理解:
JDBC = 接口 + 驱动实现
五步流程
1. 加载驱动
2. 获取连接
3. 获取执行SQL对象
4. 执行SQL
5. 释放资源
代码完整示例
import java.sql.*;
public class JdbcDemo {
public static void main(String[] args) throws Exception {
// 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
Connection conn = DriverManager.getConnection(
"jdbc:mysql:///jdbcdemo",
"root",
"root"
);
// 获取执行SQL对象
Statement stmt = conn.createStatement();
// 执行SQL
ResultSet rs = stmt.executeQuery("select * from t_user");
// 处理结果
while(rs.next()){
System.out.println(rs.getInt("id") + " "
+ rs.getString("username"));
}
// 释放资源
rs.close();
stmt.close();
conn.close();
}
}
JDBC 核心对象详解
DriverManager(驱动管理)
作用:
- 注册驱动(反射加载驱动类)
- 获取连接
Class.forName("com.mysql.jdbc.Driver");
Connection(连接对象)
作用:
- 连接数据库
- 创建 SQL 执行对象
- 管理事务
Connection conn = DriverManager.getConnection(...);
Statement(执行SQL)
Statement stmt = conn.createStatement();
方法:
executeQuery() // 查询
executeUpdate() // 增删改
ResultSet(结果集)
类 ** 似“表格游标”
while(rs.next()){
rs.getInt("id");
rs.getString("username");
}
特点:
- 游标默认在第一行之前
- 必须 next() 才能读取
SQL 注入问题
错误写法:
String sql = "select * from user where username='"
+ username + "' and password='" + password + "'";
本质:字符串拼接 → 用户输入被解析成sql关键字导致错误的sql代码被执行
攻击示例
输入:
username: aaa' OR 1=1 --
password: 任意
SQL变成:
select * from user
where username = 'aaa' OR 1=1 --'
and password = 'xxx'
永远成立,原因在于sql对于or和and关键字的执行优先级不同,and优先,因此原sql被解析为:
select * from user
where username = 'aaa' OR (1=1 --'
and password = 'xxx')
又因为 -- 是sql的注释,所以真正的语句变为了:
select * from user
where username = 'aaa' OR (1=1)
永远成立
正确解决方案
使用 PreparedStatement
String sql = "select * from user where username=? and password=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
原理:
- SQL 先编译
- 参数后传入
- 不参与 SQL 结构
? 是 SQL 预编译语句中的参数占位符,通过参数绑定机制将用户输入作为数据值传入,使其不会参与 SQL 语法解析,从而有效防止基于字符串拼接产生的 SQL 注入问题。但是占位符只能对查询中的参数值进行安全绑定,防止用户输入参与 SQL 语法解析,但对于表名、字段名、排序条件等 SQL 结构部分无法使用占位符,如果这些内容通过字符串拼接且来源不可信,仍然可能导致 SQL 注入。因此,占位符只能防止“值注入”,不能防止“结构注入”。
JDBC 工具类
为什么要封装?
问题:
- 每次都写连接代码
- 重复率高
- 不易维护
配置文件(db.properties)
url=jdbc:mysql:///jdbcdemo
user=root
password=root
driver=com.mysql.jdbc.Driver
工具类实现
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;
public class JDBCUtils {
private static String url;
private static String user;
private static String password;
private static String driver;
// 静态代码块:类加载时执行一次
static {
try {
// 加载配置文件
Properties prop = new Properties();
InputStream is = JDBCUtils.class
.getClassLoader()
.getResourceAsStream("db.properties");
prop.load(is);
// 取配置
url = prop.getProperty("url");
user = prop.getProperty("user");
password = prop.getProperty("password");
driver = prop.getProperty("driver");
// 加载驱动
Class.forName(driver);
} catch (Exception e) {
throw new RuntimeException("初始化数据库连接失败", e);
}
}
// 获取连接
public static Connection getConnection() throws Exception {
return DriverManager.getConnection(url, user, password);
}
// 关闭资源
public static void close(Connection conn){
if(conn != null){
try { conn.close(); } catch(Exception ignored){}
}
}
}
事务、隔离级别与连接池
围绕三个关键问题展开:
- 数据为什么会“出错”(事务与并发问题)
- JDBC 如何控制这些问题(事务控制机制)
- 为什么必须使用连接池(性能与架构问题)
事务
数据库中的事务,本质是将一组操作绑定为一个不可分割的执行单元。
最经典的例子是转账:
update t_account set money = money - 1000 where username = '冠希';
update t_account set money = money + 1000 where username = '美美';
这两条语句必须同时成功或同时失败,否则就会出现数据不一致问题。
事务的本质(ACID)
事务并不是“功能”,而是一种约束机制:
- 原子性:要么全部成功,要么全部失败
- 一致性:执行前后数据必须满足业务规则
- 隔离性:多个事务互不干扰
- 持久性:提交后数据永久保存
这些特性不是 JDBC 提供的,而是数据库本身保证的。
在 MySQL 中使用事务
默认情况下:
-- MySQL 默认自动提交
update t_account set money = money - 1000 where username = '冠希';
每一条 SQL 都是一个独立事务。
如果想控制事务:
start transaction;
update t_account set money = money - 1000 where username = '冠希';
update t_account set money = money + 1000 where username = '美美';
commit; -- 提交
-- rollback; 回滚
JDBC 中的事务控制(核心)
在 Java 中,事务控制完全依赖 Connection 对象。
关键点只有三个:
conn.setAutoCommit(false); // 关闭自动提交(开启事务)
conn.commit(); // 提交事务
conn.rollback(); // 回滚事务
事务并发问题与隔离级别
当多个事务同时执行时,会出现三类经典问题。
脏读(Dirty Read)
一个事务读到了另一个事务未提交的数据。
本质:读到了“未来可能不存在的数据”。
不可重复读(Non-repeatable Read)
同一事务中,两次查询结果不同。
原因:另一个事务提交了 update 操作。
幻读(Phantom Read)
查询结果“多了一行”。
原因:另一个事务执行了 insert。
隔离级别(解决方案)
数据库通过“隔离级别”控制这些问题:
| 隔离级别 | 能解决的问题 |
|---|---|
| Read Uncommitted | 什么都不解决 |
| Read Committed | 防止脏读 |
| Repeatable Read | 防止脏读 + 不可重复读 |
| Serializable | 全部解决 |
MySQL 默认是:
Repeatable Read
隔离级别不是越高越好,而是:
- 越高 → 安全性高 → 性能差
- 越低 → 性能高 → 风险大
隔离级别解决的不是“事务”,而是:
多个事务同时执行时,彼此能看到什么数据
数据库的核心手段只有三类:
- 锁(Lock)
- 多版本并发控制(MVCC)
- 间隙锁 / 范围锁(Gap Lock / Next-Key Lock)
sql读取机制*
数据库里的“一行数据”,在 InnoDB 里并不是只有一份。
一条记录实际结构类似这样:
id | money | trx_id | roll_pointer
trx_id:最后一次修改这行数据的事务IDroll_pointer:指向 undo log(历史版本链)
也就是说,一行数据背后其实是一个“版本链”:
当前版本(最新)
↓
旧版本
↓
更旧版本
这个链条就是 MVCC 的基础。
再理解一个点:数据库里有两种“读”:
1. 当前读(Current Read)
→ 读最新数据(可能未提交)
→ 会加锁
2. 快照读(Snapshot Read)
→ 读历史版本(通过 undo log)
→ 不加锁
不同隔离级别,本质就是:
什么时候用“当前读”,什么时候用“快照读”,以及快照怎么选
四个隔离级别详解
接下来按机制推导四个隔离级别。
Read Uncommitted
它的规则非常简单:
所有 select 都是“当前读(无锁版)”
数据库执行 select 时:
- 不加锁
- 不创建快照
- 不检查 trx_id 是否已提交
- 直接返回当前版本
所以才会出现:
事务A改了但没提交,事务B已经能看到。
因为数据库根本没做任何隔离处理。
Read Committed
它开始使用 MVCC,但方式是:
每次 select 都创建一个新的“Read View(读视图)”
Read View 可以理解为一个“可见性规则”:
- 执行 select
- 创建 Read View(当前时刻)
- 沿着版本链(undo log)往回找
- 找到“已提交且可见”的版本
关键点在这里:
每次查询都会创建新的 Read View
所以:
第一次查询 → 用的是旧视图
第二次查询 → 用的是新视图
如果中间有事务提交:
你第二次查询就能看到新数据
这就是不可重复读的根本原因,不是“读错了”,而是你主动换了一套可见性规则
Repeatable Read
核心变化只有一个:
Read View 在“事务开始时”创建,并且一直复用
执行流程:
- start transaction
- 创建 Read View
- 所有 select 都用这个 Read View
- 永远沿版本链找“同一批可见数据”
所以:
即使其他事务提交了新数据
你也看不到
因为:
你的 Read View 不变
这就是“可重复读”的本质,不是锁,而是版本快照固定
幻读?
幻读为什么理论上还会发生?
因为 MVCC 只解决“已有行的版本问题”,不解决“新增行”。
举个逻辑过程:
事务A:
select * from t_account where money > 1000;
事务B:
insert into t_account values (..., 2000);
commit;
事务A再次查询:
select * from t_account where money > 1000;
新插入的行:它没有旧版本(没有 undo 链)所以 MVCC 没法“回溯”,这就是幻读的理论来源。
另提一点,MySQL InnoDB 引入了 Next-Key Lock(行锁 + 间隙锁),数据库会锁住操作的行和索引上的一个区间段(range)该区间是基于 B+Tree 索引的有序键值划分的逻辑范围,因此在 MySQL 中Repeatable Read + Next-Key Lock实际上避免了幻读,但是普通的查询不会开启,只有update / delete 等当前读操作或select … for update,select … lock in share mode这些明确要求的sql代码才会执行
Serializable
它完全不依赖 MVCC 的“快照读”,而是:
所有读都变成“加锁的当前读”
执行 select 时:
加共享锁(S锁)
执行 update / insert:
加排他锁(X锁)
效果就是:
读写互斥
写写互斥
所有事务必须排队执行。
所以它的本质不是“更聪明”,而是直接放弃并发。
现在可以把四个级别统一成一句话理解:
RU:不做任何控制,直接读当前数据
RC:每次查询都重新生成“可见性规则”
RR:事务开始时固定“可见性规则”
Serializable:不做版本控制,直接用锁强制排队
连接池
在基础 JDBC 中,每次操作都会:
创建连接 → 使用 → 关闭连接
问题在于:
- 建立连接成本极高(网络 + 认证 + 资源分配)
- 并发场景下会严重拖慢系统
把这个思想抽象一下,本质是三件事:
1. 资源预创建(Pre-allocation)
避免运行时频繁创建
2. 资源复用(Reuse)
同一个资源被多个请求重复使用
3. 生命周期托管(Lifecycle Management)
池负责资源的创建、分配、回收、销毁
DataSource
JDBC 提供了统一标准:
Connection getConnection();
任何连接池都必须实现这个接口。
Druid 连接池实战
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///jdbcdemo");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 连接池参数
dataSource.setInitialSize(5);
dataSource.setMaxActive(10);
dataSource.setMaxWait(2000);
Connection conn = dataSource.getConnection();
conn.close();连接池使用动态代理接管了连接的关闭,会归还当前连接
end