主题
界面
主题
数据库系列的第一步不是写 SQL,而是把数据库跑起来、连上去。
本节讲三件事:服务端与客户端的模型、怎么搭建、如何连接。后面的 SQL、索引、事务都建立在"能连上"这个前提上。
完成本节后,你应该能够:
"连不上数据库"是新手遇到的第一类问题,也是最消耗时间的一类:
Access denied for user 'root'@'localhost' ← 账号密码错
Communications link failure ← 服务没起来 / 端口不通
Unknown database 'shop' ← 库不存在
Public Key Retrieval is not allowed ← 连接参数问题(MySQL 8+)
The server time zone value '中国标准时间' is unrecognized ← 时区参数没配这些问题与 SQL 水平无关,但会卡住你半天。
┌─────────────────┐ TCP 连接 ┌──────────────────┐
│ 客户端 │ ──────────────────────► │ 数据库服务端 │
│ mysql 命令行 │ host:port + 账号密码 │ mysqld 进程 │
│ DBeaver │ ◄────────────────────── │ 监听 3306 端口 │
│ Java 程序(JDBC) │ 查询结果 │ 管理数据文件 │
└─────────────────┘ └──────────────────┘三个关键认知:
| 认知 | 说明 |
|---|---|
| 数据库是独立进程 | 不是"程序里的一个库",而是单独运行的服务,通过网络被访问 |
| 客户端可以很多 | 命令行、图形工具、Java 程序——它们看到的都是同一份数据 |
| 连接是"会话" | 一次连接有独立的会话状态(当前库、事务、临时表),连接断开就没了 |
为什么强调"独立进程":很多"连不上"的问题,本质是那个进程没起来。
| 方式 | 怎么做 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 安装包 | 官方 MSI / DMG | 自动注册服务、配好环境 | 版本固定、卸载不干净 | 个人电脑长期使用 |
| 免安装版(ZIP) | 解压 + mysqld --initialize | 版本可控、可多版本共存 | 需要手工初始化 | 开发机、CI |
| 容器(Docker) | docker run mysql:8 | 一条命令、随时销毁重建 | 需要本机有 Docker | 团队统一环境、CI |
容器方式的示例(推荐你在有 Docker 的机器上用):
docker run -d --name mysql-dev \
-e MYSQL_ROOT_PASSWORD=dev123456 \
-e MYSQL_DATABASE=shop \
-p 3306:3306 \
mysql:8.0 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci三个参数值得注意:MYSQL_ROOT_PASSWORD(不设容器起不来)、MYSQL_DATABASE(自动建库)、--character-set-server=utf8mb4(中文必须)。
本项目用 H2 内存库演示 SQL,理由有三个:零安装(一个 Maven 依赖)、可复现(每次运行都是干净环境)、能自动化(输出与讲义逐字一致)。
连接字符串:
"jdbc:h2:mem:kb;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1"| 片段 | 含义 |
|---|---|
jdbc:h2:mem:kb | H2 的内存库,名字叫 kb |
MODE=MySQL | MySQL 兼容模式(支持 AUTO_INCREMENT 等) |
DATABASE_TO_LOWER=TRUE | 表名/列名统一小写(与 MySQL 在 Linux 上的行为一致) |
DB_CLOSE_DELAY=-1 | 最后一个连接关闭后库仍保留 |
切换到真实 MySQL 只需要改两处:
<!-- 1. 依赖:把 H2 换成 MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.1.0</version>
</dependency>// 2. 连接 URL
"jdbc:mysql://localhost:3306/shop?characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai"必须诚实说明的差异:
| 内容 | H2 能演示吗 |
|---|---|
| SQL 语法、约束、关联查询 | ✅ 完全能(本项目已实测) |
| 事务与隔离级别 | ✅ 能(《事务与隔离级别》) |
| 索引的效果(快慢对比) | ⚠️ 能看趋势,数字与 MySQL 不一致 |
| 执行计划(EXPLAIN)的输出格式 | ❌ 格式不同,讲义给出 MySQL 典型输出对照 |
| InnoDB 的聚簇索引、MVCC 细节 | ❌ 必须用真实 MySQL 理解 |
建议:学完本模块其余课程后,用 Docker 起一个真实 MySQL 把 SQL 重跑一遍。
jdbc:mysql://localhost:3306/shop?characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
└────┬────┘└──┬───┘└──┬──┘└─┬──┘└──────────────────┬──────────────────┘
协议 主机 端口 库名 连接参数(用 & 分隔)常用连接参数:
| 参数 | 作用 | 不配的后果 |
|---|---|---|
characterEncoding=utf8mb4 | 字符编码 | 中文乱码 |
serverTimezone=Asia/Shanghai | 服务端时区 | 时间差 8 小时,或直接报错 |
allowPublicKeyRetrieval=true | 允许取公钥(MySQL 8 认证方式) | Public Key Retrieval is not allowed |
useSSL=false | 关闭 SSL(本地开发) | 一堆 SSL 警告 |
rewriteBatchedStatements=true | 批量插入优化 | 批量插入慢十倍 |
记忆点:中文乱码与时区问题,90% 是连接参数没配全。
| 层 | 含义 | MySQL 里的操作 |
|---|---|---|
| 库(schema) | 一组相关表的集合 | CREATE DATABASE shop; / USE shop; |
| 表(table) | 结构化数据的载体 | CREATE TABLE ... |
| 列(column) | 字段定义(类型 + 约束) | VARCHAR(32) NOT NULL |
查看结构的两套命令:
| MySQL 专有命令 | 标准 SQL(跨库通用) |
|---|---|
SHOW DATABASES; | 查 information_schema.schemata |
SHOW COLUMNS FROM customer; / DESC customer; | 查 information_schema.columns |
SHOW INDEX FROM customer; | 查 information_schema.statistics |
建议:写在代码里的元数据查询一律用 information_schema——它跨数据库通用、能 join 能过滤;DESC 这类命令留给手工操作。
try (SqlRunner db = SqlRunner.open()) {
System.out.print(db.query("SELECT 1 AS connected, CURRENT_TIMESTAMP AS server_time"));
}见第 7 节。注意 H2 里 CREATE SCHEMA 对应 MySQL 的 CREATE DATABASE,SET SCHEMA 对应 USE。
public void query1() {
DriverManager.getConnection("jdbc:h2:mem:kb;MODE=MySQL", "sa", "");
}
public void query2() {
DriverManager.getConnection("jdbc:h2:mem:kb;MODE=MySQL", "sa", ""); // 又抄一遍
}修复:连接信息集中在一处配置(本项目的 SqlRunner.MYSQL_LIKE_URL 常量)——这也是 《配置管理》的主题。
文件:labs/sql-basics/src/main/java/cn/kb/learn/sql/ConnectionLesson.java(节选)
public class ConnectionLesson {
public static void main(String[] args) {
System.out.println("=== 1. 连接数据库 ===");
try (SqlRunner db = SqlRunner.open()) {
System.out.println("连接串:" + SqlRunner.MYSQL_LIKE_URL);
System.out.print(db.query("SELECT 1 AS connected, CURRENT_TIMESTAMP AS server_time"));
System.out.println();
System.out.println("=== 2. 三层结构:库 → 表 → 列 ===");
db.execute("CREATE SCHEMA shop"); // MySQL 里是 CREATE DATABASE
db.execute("SET SCHEMA shop"); // MySQL 里是 USE shop
db.execute("""
CREATE TABLE customer (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(32) NOT NULL,
phone VARCHAR(20)
)
""");
System.out.println("已创建「库 shop」与「表 customer」");
System.out.println();
System.out.println("=== 3. 查看表结构(元数据)===");
System.out.print(db.query("""
SELECT column_name AS 列名,
data_type AS 数据类型,
is_nullable AS 可空,
ordinal_position AS 顺序
FROM information_schema.columns
WHERE LOWER(table_name) = 'customer'
ORDER BY ordinal_position
"""));
System.out.println("真实 MySQL 里对应的命令是 SHOW COLUMNS FROM customer 或 DESC customer");
System.out.println();
System.out.println("=== 4. 多连接:同一个库可以被多条连接访问 ===");
db.execute("INSERT INTO customer (name, phone) VALUES ('张三', '13800000001')");
System.out.println("第一条连接看到的行数:" + db.scalar("SELECT COUNT(*) FROM customer"));
try (SqlRunner second = SqlRunner.open()) {
second.execute("SET SCHEMA shop");
System.out.println("第二条连接看到的行数:" + second.scalar("SELECT COUNT(*) FROM customer"));
System.out.println("→ 两条连接访问的是同一个库(同名 URL 的内存库在同一进程内共享)");
}
}
}
}运行命令:
cd labs
./mvnw -pl sql-basics compile exec:exec -Dexec.mainClass=cn.kb.learn.sql.ConnectionLesson预期输出:
=== 1. 连接数据库 ===
连接串:jdbc:h2:mem:kb;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1
connected | server_time
---------------------------------------------
1 | 2026-09-21T13:16:09.857928+08:00
(共 1 行)
=== 2. 三层结构:库 → 表 → 列 ===
已创建「库 shop」与「表 customer」
=== 3. 查看表结构(元数据)===
列名 | 数据类型 | 可空 | 顺序
-------------------------------------
id | bigint | NO | 1
name | character varying | NO | 2
phone | character varying | YES | 3
(共 3 行)
真实 MySQL 里对应的命令是 SHOW COLUMNS FROM customer 或 DESC customer
=== 4. 多连接:同一个库可以被多条连接访问 ===
第一条连接看到的行数:1
第二条连接看到的行数:1
→ 两条连接访问的是同一个库(同名 URL 的内存库在同一进程内共享)三处值得注意:
| 输出 | 含义 |
|---|---|
character varying | 标准 SQL 的类型名;MySQL 里显示 varchar(同一件事的两种叫法) |
is_nullable 为 NO | 因为 name 声明了 NOT NULL |
| 两条连接都看到 1 行 | 数据库独立于客户端:谁连上都能看到同一份数据 |
SqlRunner.open() 内部断点,观察 DriverManager.getConnection(...) 返回的 Connection 对象——它代表一条真实的会话;db.query(...) 处按 F7 进入,观察"创建 Statement → 执行 → 返回 ResultSet"这三步(这就是 JDBC 的全部骨架,《JDBC 与数据库连接》 会专门讲);SqlRunner.open() 处观察:它创建了新的 Connection,但读到的数据是同一份;mem:kb 改成 mem:other,重跑:第二个连接会因为"表不存在"而报错——证明库名是连接的一部分。Public Key Retrieval is not allowed 原因:MySQL 8 默认用 caching_sha2_password 认证,JDBC 需要取公钥。修复:连接串加 allowPublicKeyRetrieval=true(本地开发)。
| 现象 | 原因 | 修复 |
|---|---|---|
存进去是 ??? | 连接串没配 characterEncoding | 加 characterEncoding=utf8mb4 |
| 建库时字符集不对 | 库/表用了默认的 latin1 | 建库时指定 CHARACTER SET utf8mb4 |
| 一部分能显示一部分不能 | 只配了一端 | 连接串、库、表三层都要一致 |
建议:统一用 utf8mb4(不是 utf8——MySQL 的 utf8 是 3 字节的,存不了 emoji)。
原因:连接串没配 serverTimezone。修复:加 serverTimezone=Asia/Shanghai;更彻底的做法是库里统一存 UTC 时间戳。
Bind on TCP/IP port: Address already in use排查:Windows 用 netstat -ano | findstr 3306,Linux 用 lsof -i:3306——看是谁占着。
现象:程序重启后数据没了。原因:jdbc:h2:mem:kb 是内存库,进程结束即消失——这是刻意设计(保证示例可复现)。需要持久化时用 jdbc:h2:file:./data/kb。
目标:把"连接 → 建库 → 建表 → 查结构"完整走一遍。
步骤:
mem:kb 改成 mem:unknown),观察并记录报错;ConnectionLesson 里加一步:查询 information_schema.tables,列出当前库里所有表;jdbc:h2:file:./data/kb;MODE=MySQL,建表插数据,退出后重跑,观察数据是否还在(提示:CREATE TABLE 会因表已存在报错,需要用 CREATE TABLE IF NOT EXISTS);data_type 与时间格式)。验收方式:能说出第 2 步的报错、第 4 步的数据是否还在。
扩展任务:写一个 MySqlRunner,把连接 URL 与驱动改成 MySQL 的,要求代码结构完全不变(提示:用 SqlRunner.open(String url) 这个重载)。
基础题
jdbc:mysql:// 之后依次是 、、。information_schema 而不是 SHOW 命令查表结构?代码阅读题
String url = "jdbc:mysql://localhost:3306/shop";
try (Connection connection = DriverManager.getConnection(url, "root", "123456")) {
System.out.println("连接成功");
}localhost 应该改成什么?编程题
SqlRunner 支持元数据查询: List<String> tables(SqlRunner db, String schema):查询 information_schema.tables 返回表名列表;int columnCount(SqlRunner db, String table):返回某表的列数;main:建 3 张表,打印表清单与每张表的列数;SHOW TABLES / DESC 更适合写在工具类里?综合任务
提示 1(方向):第 4 题的关键是"字符集"与"时区"这两个最容易被忽略的参数。
提示 2(关键点):第 6 题的 information_schema.tables 在 H2 与 MySQL 里字段名一致,但 schema 名大小写不同——用 LOWER() 兼容。
提示 3(接近答案):第 7 题的排查顺序通常是:① 服务是否在跑(看端口)→ ② 网络是否通(ping / telnet)→ ③ 账号密码(命令行先连一次)→ ④ 库是否存在 → ⑤ 连接参数(字符集/时区/SSL)。
information_schema,而 SHOW 是 MySQL 专有);② 可 join、可过滤、可排序;③ 返回值是结构化的,程序可以直接用,不需要解析文本表格。实践建议:手工看结构用 DESC 更快;写在代码或脚本里的一律用 information_schema。
第 4 题:缺了至少四个常见参数。
| 缺失参数 | 会出什么问题 |
|---|---|
characterEncoding=utf8mb4 | 中文乱码 |
serverTimezone=Asia/Shanghai | 时间差 8 小时,或报"时区无法识别" |
allowPublicKeyRetrieval=true | MySQL 8 报 Public Key Retrieval is not allowed |
useSSL=false | 本地开发时一堆 SSL 警告 |
connectionTimeout=3000(可选) | 网络不通时会卡很久才报错 |
完整版本:
String url = "jdbc:mysql://localhost:3306/shop"
+ "?characterEncoding=utf8mb4"
+ "&serverTimezone=Asia/Shanghai"
+ "&allowPublicKeyRetrieval=true"
+ "&useSSL=false";第 5 题:改成数据库服务器的 IP 或域名(如 192.168.1.100)。
顺带一个安全提醒:不要把生产环境的连接信息(尤其密码)写死在代码里——用配置或环境变量(《Spring Boot 配置管理与多环境》)。
package cn.kb.learn.sql;
import java.util.ArrayList;
import java.util.List;
public class MetadataQueries {
public static List<String> tables(SqlRunner db, String schema) {
List<List<String>> rows = db.rows("""
SELECT table_name
FROM information_schema.tables
WHERE LOWER(table_schema) = LOWER('%s')
ORDER BY table_name
""".formatted(schema));
List<String> names = new ArrayList<>();
for (List<String> row : rows) {
names.add(row.get(0));
}
return names;
}
public static int columnCount(SqlRunner db, String table) {
return Integer.parseInt(db.scalar("""
SELECT COUNT(*)
FROM information_schema.columns
WHERE LOWER(table_name) = LOWER('%s')
""".formatted(table)));
}
public static void main(String[] args) {
try (SqlRunner db = SqlRunner.open()) {
db.execute("CREATE SCHEMA meta_demo");
db.execute("SET SCHEMA meta_demo");
db.execute("CREATE TABLE customer (id BIGINT PRIMARY KEY, name VARCHAR(32))");
db.execute("CREATE TABLE orders (id BIGINT PRIMARY KEY, customer_id BIGINT, amount INT)");
db.execute("CREATE TABLE order_item (order_id BIGINT, product VARCHAR(32))");
System.out.println("表清单:" + tables(db, "meta_demo"));
for (String table : tables(db, "meta_demo")) {
System.out.println(" " + table + " 有 " + columnCount(db, table) + " 列");
}
}
}
}为什么比 SHOW / DESC 更适合写在工具类里:
| 原因 | 说明 |
|---|---|
| 返回值结构化 | List<String> / int 直接可用;SHOW 的结果是给人看的表格文本 |
| 跨数据库通用 | 换到 PostgreSQL 或 H2 时这段代码不用改 |
| 能参数化 | tables(db, "shop") 与 tables(db, "hr") 复用同一个方法 |
这就是"把命令变成查询"的价值:命令是给人用的,查询是给程序用的。
排查手册(按顺序执行):
## 第五步:连接参数
症状与参数对应:
- Public Key Retrieval is not allowed → 缺 allowPublicKeyRetrieval=true
- The server time zone value ... is unrecognized → 缺 serverTimezone
- 中文乱码 → 缺 characterEncoding=utf8mb4
## 最小排查流程(四步定位)
1. 服务在跑吗? netstat -ano | findstr 3306 (没监听 → 服务没起)
2. 网络通吗? Test-NetConnection db-host -Port 3306 (不通 → 防火墙/安全组)
3. 认证对吗? mysql -h db-host -u root -p (失败 → 账号密码或 host 白名单)
4. 库存在吗? mysql -e "SHOW DATABASES;" (不存在 → 建库或改连接串)
## 四种典型症状对照
| 报错 | 根因 | 排查位置 |
|------|------|---------|
| Communications link failure | 服务没起 / 端口不通 | 第 1、2 步 |
| Access denied for user 'root'@'%' | 账号密码错,或 host 白名单不含来源 IP | 第 3 步 |
| Unknown database 'shop' | 库不存在 | 第 4 步 |
| Public Key Retrieval is not allowed | 连接参数缺失 | 第五步 |
**注意 `'root'@'localhost'` 与 `'root'@'%'` 是两个不同的账号**——
本地能连、远程连不上,通常是 host 白名单的问题。
## 一条安全提醒
**数据库端口永远不要对公网开放**——需要远程访问时,走跳板机、VPN 或内网。这份手册的价值:它把"凭感觉试"变成了"按顺序查"。遇到连接问题时,前两步能过滤掉 80% 的"环境没起"类问题。
utf8mb4 与 serverTimezone 这两类参数为什么必须有;information_schema 查表结构;《SQL 基础》:DDL、DML、DQL 的完整走一遍。
无,可直接开始
暂无