Loading...

sql和JDBC事务连接池

2026-04-18
0
-
- 分钟
|

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:最后一次修改这行数据的事务ID
  • roll_pointer:指向 undo log(历史版本链)

也就是说,一行数据背后其实是一个“版本链”:

当前版本(最新)
   ↓
旧版本
   ↓
更旧版本

这个链条就是 MVCC 的基础。

再理解一个点:数据库里有两种“读”:

1. 当前读(Current Read)
   → 读最新数据(可能未提交)
   → 会加锁

2. 快照读(Snapshot Read)
   → 读历史版本(通过 undo log)
   → 不加锁

不同隔离级别,本质就是:

什么时候用“当前读”,什么时候用“快照读”,以及快照怎么选

四个隔离级别详解

接下来按机制推导四个隔离级别。

Read Uncommitted

它的规则非常简单:

所有 select 都是“当前读(无锁版)”

数据库执行 select 时:

  1. 不加锁
  2. 不创建快照
  3. 不检查 trx_id 是否已提交
  4. 直接返回当前版本

所以才会出现:

事务A改了但没提交,事务B已经能看到。

因为数据库根本没做任何隔离处理。

Read Committed

它开始使用 MVCC,但方式是:

每次 select 都创建一个新的“Read View(读视图)”

Read View 可以理解为一个“可见性规则”:

  1. 执行 select
  2. 创建 Read View(当前时刻)
  3. 沿着版本链(undo log)往回找
  4. 找到“已提交且可见”的版本

关键点在这里:

每次查询都会创建新的 Read View

所以:

第一次查询 → 用的是旧视图

第二次查询 → 用的是新视图

如果中间有事务提交:

你第二次查询就能看到新数据

这就是不可重复读的根本原因,不是“读错了”,而是你主动换了一套可见性规则

Repeatable Read

核心变化只有一个:

Read View 在“事务开始时”创建,并且一直复用

执行流程:

  1. start transaction
  2. 创建 Read View
  3. 所有 select 都用这个 Read View
  4. 永远沿版本链找“同一批可见数据”

所以:

即使其他事务提交了新数据
你也看不到

因为:

你的 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

文章目录