Skip to content

MySQL 搭建与客户端使用 ​

S3-014必学入门约 45 分钟JDK 21企业开发基础 · MySQL 与数据建模

1. 本节定位 ​

数据库系列的第一步不是写 SQL,而是把数据库跑起来、连上去。

本节讲三件事:服务端与客户端的模型、怎么搭建、如何连接。后面的 SQL、索引、事务都建立在"能连上"这个前提上。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说清数据库"服务端 / 客户端 / 连接"三者的关系;
  • 说出 MySQL 的三种部署方式及各自适用场景;
  • 读懂一个 JDBC 连接字符串的每一段;
  • 说清"库 → 表 → 列"三层结构,并用元数据查询查看表结构;
  • 知道本项目的演示环境(H2 MySQL 兼容模式)与真实 MySQL 的差异。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

"连不上数据库"是新手遇到的第一类问题,也是最消耗时间的一类:

text
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 水平无关,但会卡住你半天。

5. 核心概念 ​

5.1 服务端 / 客户端 / 连接 ​

text
┌─────────────────┐         TCP 连接          ┌──────────────────┐
│  客户端          │  ──────────────────────►  │  数据库服务端      │
│  mysql 命令行    │    host:port + 账号密码    │  mysqld 进程      │
│  DBeaver        │  ◄──────────────────────  │  监听 3306 端口    │
│  Java 程序(JDBC) │        查询结果            │  管理数据文件      │
└─────────────────┘                            └──────────────────┘

三个关键认知:

认知说明
数据库是独立进程不是"程序里的一个库",而是单独运行的服务,通过网络被访问
客户端可以很多命令行、图形工具、Java 程序——它们看到的都是同一份数据
连接是"会话"一次连接有独立的会话状态(当前库、事务、临时表),连接断开就没了

为什么强调"独立进程":很多"连不上"的问题,本质是那个进程没起来。

5.2 三种部署方式 ​

方式怎么做优点缺点适用
安装包官方 MSI / DMG自动注册服务、配好环境版本固定、卸载不干净个人电脑长期使用
免安装版(ZIP)解压 + mysqld --initialize版本可控、可多版本共存需要手工初始化开发机、CI
容器(Docker)docker run mysql:8一条命令、随时销毁重建需要本机有 Docker团队统一环境、CI

容器方式的示例(推荐你在有 Docker 的机器上用):

bash
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(中文必须)。

5.3 本项目的演示环境:H2 的 MySQL 兼容模式 ​

本项目用 H2 内存库演示 SQL,理由有三个:零安装(一个 Maven 依赖)、可复现(每次运行都是干净环境)、能自动化(输出与讲义逐字一致)。

连接字符串:

java
"jdbc:h2:mem:kb;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1"
片段含义
jdbc:h2:mem:kbH2 的内存库,名字叫 kb
MODE=MySQLMySQL 兼容模式(支持 AUTO_INCREMENT 等)
DATABASE_TO_LOWER=TRUE表名/列名统一小写(与 MySQL 在 Linux 上的行为一致)
DB_CLOSE_DELAY=-1最后一个连接关闭后库仍保留

切换到真实 MySQL 只需要改两处:

xml
<!-- 1. 依赖:把 H2 换成 MySQL 驱动 -->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <version>9.1.0</version>
</dependency>
java
// 2. 连接 URL
"jdbc:mysql://localhost:3306/shop?characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai"

必须诚实说明的差异:

内容H2 能演示吗
SQL 语法、约束、关联查询✅ 完全能(本项目已实测)
事务与隔离级别✅ 能(《事务与隔离级别》)
索引的效果(快慢对比)⚠️ 能看趋势,数字与 MySQL 不一致
执行计划(EXPLAIN)的输出格式❌ 格式不同,讲义给出 MySQL 典型输出对照
InnoDB 的聚簇索引、MVCC 细节❌ 必须用真实 MySQL 理解

建议:学完本模块其余课程后,用 Docker 起一个真实 MySQL 把 SQL 重跑一遍。

5.4 连接字符串解剖 ​

text
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% 是连接参数没配全。

5.5 三层结构:库 → 表 → 列 ​

层含义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 这类命令留给手工操作。

6. 递进示例 ​

6.1 最小示例:连接并查询 ​

java
try (SqlRunner db = SqlRunner.open()) {
    System.out.print(db.query("SELECT 1 AS connected, CURRENT_TIMESTAMP AS server_time"));
}

6.2 业务示例:建库建表并查看结构 ​

见第 7 节。注意 H2 里 CREATE SCHEMA 对应 MySQL 的 CREATE DATABASE,SET SCHEMA 对应 USE。

6.3 反例:把连接信息写死在每个方法里 ​

java
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 常量)——这也是 《配置管理》的主题。

7. 完整可运行代码 ​

文件:labs/sql-basics/src/main/java/cn/kb/learn/sql/ConnectionLesson.java(节选)

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 的内存库在同一进程内共享)");
            }
        }
    }
}

运行命令:

powershell
cd labs
./mvnw -pl sql-basics compile exec:exec -Dexec.mainClass=cn.kb.learn.sql.ConnectionLesson

预期输出:

text
=== 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 行数据库独立于客户端:谁连上都能看到同一份数据

8. Debug 观察点 ​

  1. 在 SqlRunner.open() 内部断点,观察 DriverManager.getConnection(...) 返回的 Connection 对象——它代表一条真实的会话;
  2. 在 db.query(...) 处按 F7 进入,观察"创建 Statement → 执行 → 返回 ResultSet"这三步(这就是 JDBC 的全部骨架,《JDBC 与数据库连接》 会专门讲);
  3. 在第二个 SqlRunner.open() 处观察:它创建了新的 Connection,但读到的数据是同一份;
  4. 把 URL 里的 mem:kb 改成 mem:other,重跑:第二个连接会因为"表不存在"而报错——证明库名是连接的一部分。

9. 常见错误 ​

错误 1:MySQL 8 的 Public Key Retrieval is not allowed ​

原因:MySQL 8 默认用 caching_sha2_password 认证,JDBC 需要取公钥。修复:连接串加 allowPublicKeyRetrieval=true(本地开发)。

错误 2:中文乱码 ​

现象原因修复
存进去是 ???连接串没配 characterEncoding加 characterEncoding=utf8mb4
建库时字符集不对库/表用了默认的 latin1建库时指定 CHARACTER SET utf8mb4
一部分能显示一部分不能只配了一端连接串、库、表三层都要一致

建议:统一用 utf8mb4(不是 utf8——MySQL 的 utf8 是 3 字节的,存不了 emoji)。

错误 3:时间差 8 小时 ​

原因:连接串没配 serverTimezone。修复:加 serverTimezone=Asia/Shanghai;更彻底的做法是库里统一存 UTC 时间戳。

错误 4:端口被占用(3306) ​

text
Bind on TCP/IP port: Address already in use

排查:Windows 用 netstat -ano | findstr 3306,Linux 用 lsof -i:3306——看是谁占着。

错误 5:以为"内存库"能持久化 ​

现象:程序重启后数据没了。原因:jdbc:h2:mem:kb 是内存库,进程结束即消失——这是刻意设计(保证示例可复现)。需要持久化时用 jdbc:h2:file:./data/kb。

10. 动手实验 ​

目标:把"连接 → 建库 → 建表 → 查结构"完整走一遍。

步骤:

  1. 运行第 7 节的命令,核对输出;
  2. 故意改错连接串(如 mem:kb 改成 mem:unknown),观察并记录报错;
  3. 在 ConnectionLesson 里加一步:查询 information_schema.tables,列出当前库里所有表;
  4. 持久化实验:把 URL 改成 jdbc:h2:file:./data/kb;MODE=MySQL,建表插数据,退出后重跑,观察数据是否还在(提示:CREATE TABLE 会因表已存在报错,需要用 CREATE TABLE IF NOT EXISTS);
  5. 如果本机有 Docker:起一个真实 MySQL(命令见 5.2 节),把第 7 节的代码连过去,观察输出差异(尤其是 data_type 与时间格式)。

验收方式:能说出第 2 步的报错、第 4 步的数据是否还在。

扩展任务:写一个 MySqlRunner,把连接 URL 与驱动改成 MySQL 的,要求代码结构完全不变(提示:用 SqlRunner.open(String url) 这个重载)。

11. 练习题 ​

基础题

  1. 判断:数据库服务端和客户端必须在同一台机器上。(对 / 错)
  2. 填空:MySQL 的默认端口是 ;连接串里 jdbc:mysql:// 之后依次是 、、。
  3. 简答:为什么建议在代码里用 information_schema 而不是 SHOW 命令查表结构?

代码阅读题

java
String url = "jdbc:mysql://localhost:3306/shop";
try (Connection connection = DriverManager.getConnection(url, "root", "123456")) {
    System.out.println("连接成功");
}
  1. 这个连接串缺了哪些常见参数?分别会导致什么问题?
  2. 如果客户端与服务器不在同一台机器,localhost 应该改成什么?

编程题

  1. 扩展 SqlRunner 支持元数据查询:
    • 新增方法 List<String> tables(SqlRunner db, String schema):查询 information_schema.tables 返回表名列表;
    • 新增方法 int columnCount(SqlRunner db, String table):返回某表的列数;
    • 写一个 main:建 3 张表,打印表清单与每张表的列数;
    • 说明:为什么这两个方法比 SHOW TABLES / DESC 更适合写在工具类里?

综合任务

  1. 写一份"数据库连接排查手册":
    • 列出 6 种最常见的连接失败错误信息(可以自己制造);
    • 每种给出:错误信息原文、根因、排查步骤、修复方式;
    • 按"排查顺序"排列(先查什么、再查什么);
    • 额外要求:设计一个"最小排查流程"(几步命令),能快速区分"服务没起 / 网络不通 / 认证失败"。
分级提示

提示 1(方向):第 4 题的关键是"字符集"与"时区"这两个最容易被忽略的参数。

提示 2(关键点):第 6 题的 information_schema.tables 在 H2 与 MySQL 里字段名一致,但 schema 名大小写不同——用 LOWER() 兼容。

提示 3(接近答案):第 7 题的排查顺序通常是:① 服务是否在跑(看端口)→ ② 网络是否通(ping / telnet)→ ③ 账号密码(命令行先连一次)→ ④ 库是否存在 → ⑤ 连接参数(字符集/时区/SSL)。

12. 参考答案 ​

基础题
  1. 错。数据库是独立的服务端进程,客户端通过网络访问它。绝大多数生产环境里,客户端与数据库不在一台机器上。
  2. 3306;主机、端口、库名。
  3. 三个理由:① 跨数据库通用(MySQL / PostgreSQL / H2 都有 information_schema,而 SHOW 是 MySQL 专有);② 可 join、可过滤、可排序;③ 返回值是结构化的,程序可以直接用,不需要解析文本表格。

实践建议:手工看结构用 DESC 更快;写在代码或脚本里的一律用 information_schema。

代码阅读题(第 4、5 题)

第 4 题:缺了至少四个常见参数。

缺失参数会出什么问题
characterEncoding=utf8mb4中文乱码
serverTimezone=Asia/Shanghai时间差 8 小时,或报"时区无法识别"
allowPublicKeyRetrieval=trueMySQL 8 报 Public Key Retrieval is not allowed
useSSL=false本地开发时一堆 SSL 警告
connectionTimeout=3000(可选)网络不通时会卡很久才报错

完整版本:

java
String url = "jdbc:mysql://localhost:3306/shop"
        + "?characterEncoding=utf8mb4"
        + "&serverTimezone=Asia/Shanghai"
        + "&allowPublicKeyRetrieval=true"
        + "&useSSL=false";

第 5 题:改成数据库服务器的 IP 或域名(如 192.168.1.100)。

顺带一个安全提醒:不要把生产环境的连接信息(尤其密码)写死在代码里——用配置或环境变量(《Spring Boot 配置管理与多环境》)。

编程题(第 6 题)
java
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") 复用同一个方法

这就是"把命令变成查询"的价值:命令是给人用的,查询是给程序用的。

综合任务(第 7 题)

排查手册(按顺序执行):

markdown
## 第五步:连接参数
症状与参数对应:
- 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% 的"环境没起"类问题。

13. 掌握标准 ​

  • [ ] 能说清服务端 / 客户端 / 连接的关系;
  • [ ] 能说出三种部署方式的取舍,并知道容器方式的启动命令;
  • [ ] 能读懂并写出完整的 JDBC 连接字符串;
  • [ ] 知道 utf8mb4 与 serverTimezone 这两类参数为什么必须有;
  • [ ] 能用 information_schema 查表结构;
  • [ ] 知道 H2 演示环境与真实 MySQL 的差异边界。

14. 扩展资源 ​

15. 下一步学习 ​

《SQL 基础》:DDL、DML、DQL 的完整走一遍。

前置知识

无,可直接开始

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引