MySQL 官方是"双轨制":一条 LTS(长期支持,每三年一个大版本,稳定),一条 innovation(创新版,年年小版本尝鲜)。按版本纪律规则 2"只用稳定版 + 支持窗口中点",我们只锁 LTS。
| 线 | 当前版本(2026-10) | 取舍说明 |
|---|---|---|
| LTS 线 | 8.4.12 LTS(2026-08-18) | 本教程锁定。8.4 是长期支持版,补丁持续到 2032 年前后;语法、认证插件、系统表成熟,驱动生态全部兼容。 |
| 新 LTS 线 | 9.7 LTS(9.7.3 补丁) | 已发布但刚进入支持窗口前段,老驱动/运维脚本兼容性待观察;按规则 2 不抢"刚发布",本教程不用。 |
| innovation 线 | 26.7.x | 尝鲜特性版,支持窗口短(约 1 个月下一版就换名),生产禁用。知道存在即可。 |
一句话选型:本机容器开发、服务器原生部署、版本钉死 8.4.12 LTS——这条路线全程 1GB 内存能跑,且与工作台 DB_DRIVER 开关无缝衔接。
两条路都给完整命令。先本机(开发调试),再服务器(原生部署)。装完统一做三件事:首次登录、建库建用户、确认字符集 utf8mb4。
前置:已装 Docker Desktop 4.94.0(安装见 09 模块)。在一个空目录建 docker-compose.yml,内容如下,root 密码、端口、数据卷全部写死,照抄即可。
# docker-compose.yml —— 本机开发用 MySQL 8.4.12 LTS services: mysql: image: mysql:8.4 container_name: mysql84 restart: unless-stopped # 端口映射:本机 3306 -> 容器 3306;若本机已占 3306 可改左侧为 13306 ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "DevRoot#2026" # root 密码,仅开发用,别用于生产 MYSQL_DATABASE: "workbench" # 首次启动自动建库 MYSQL_USER: "wb" # 首次启动自动建用户并授权上面的库 MYSQL_PASSWORD: "WbPass#2026" command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_0900_ai_ci volumes: - mysql84_data:/var/lib/mysql # 数据卷,删容器不丢数据 volumes: mysql84_data:
同目录下启动(PowerShell):
docker compose up -d docker compose ps # 看到 mysql84 状态 running 即成功 docker compose logs -f mysql # 看启动日志,出现 "ready for connections" 再连
进入 mysql 命令行:
docker exec -it mysql84 mysql -uroot -pDevRoot#2026
1GB 服务器用 Debian 13.7 stable(系统选型见 09 模块)。Debian trixie 官方仓库自带的 mysql-server 就是 8.4 系列,直接 apt 装,不需要加 Oracle 第三方源(加源反而引入维护负担)。
# 以 root(或 sudo)执行。Debian 13 trixie 仓库内置 mysql-server 8.4 apt update apt install -y mysql-server systemctl enable --now mysql systemctl status mysql # active (running) # 首次安全初始化:改 root 密码、删匿名用户、禁 root 远程、删测试库 mysql_secure_installation
无论哪条路,连进去后执行下面这段(生产用户不给 root,只给业务库权限):
-- 1) 确认服务器字符集是 utf8mb4(8.4 默认即 utf8mb4,这里复核) SHOW VARIABLES LIKE 'character_set_server'; -- 期望 utf8mb4 -- 2) 建业务库,明确字符集与排序规则 CREATE DATABASE IF NOT EXISTS workbench CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; -- 3) 建业务用户,只允许本机/内网连(生产不要 %) CREATE USER IF NOT EXISTS 'wb'@'127.0.0.1' IDENTIFIED BY 'WbPass#2026'; GRANT ALL PRIVILEGES ON workbench.* TO 'wb'@'127.0.0.1'; FLUSH PRIVILEGES; -- 4) 验证:用业务用户连,能看到 workbench 库 SHOW DATABASES;
字符集纪律:永远用 utf8mb4(不是旧的 utf8——MySQL 的 utf8 是阉割版,存不下 emoji)。建库、建表、连接 DSN 三处都要 utf8mb4,乱码问题从源头消灭(报错表 §08 有对照)。
只讲你写业务真正用得上的。教科书式铺开的(如锁的所有粒度、B+树全部细节)这里点到为止,够用即可。
BIGINT UNSIGNED AUTO_INCREMENT 自增主键,不要用业务字段(手机号、身份证)当主键——业务字段会变。索引让查询从"全表扫描 N 行"变成"树查找 log(N) 行"。InnoDB 的索引是 B+树:叶子节点存整行数据(主键索引)或主键值(二级索引)。你需要记住两条实战规则:
INDEX(a,b,c) 后,查询条件必须从最左列 a 开始连续命中才走索引:WHERE a=?、WHERE a=? AND b=? 走;WHERE b=?、WHERE b=? AND c=? 不走。事务把"一组写操作"绑成要么全成、要么全败。ACID 中你实际要调的是隔离级别——它决定"一个事务能看到别的未提交事务的什么数据"。8.4 InnoDB 默认 REPEATABLE READ,对绝大多数业务不用改。
| 隔离级别 | 解决了什么 / 残留什么 | 你的取舍 |
|---|---|---|
| READ UNCOMMITTED | 能读到别人未提交的数据(脏读),啥都不防 | 不用。 |
| READ COMMITTED | 防脏读;同一事务内两次读同一行可能不同(不可重复读) | Oracle/PG 默认;高并发对账场景可选。 |
| REPEATABLE READ | 8.4 InnoDB 默认。防脏读 + 不可重复读;InnoDB 用间隙锁基本防住幻读 | 保持默认,工作台/业务系统用它。 |
| SERIALIZABLE | 全串行,最严格,锁最重、并发最低 | 极端强一致才用;本教程场景不用。 |
一条 SQL 慢不慢,先 EXPLAIN 看。重点看两列:type(访问类型,从好到差:const > ref > range > index > ALL 全表扫描)和 key(实际用到的索引,NULL 就是没走索引)。
示例:一个慢查询逐步优化。假设有订单表 orders(user_id, status, created_at, amount),要"查某用户已完成订单、按时间倒序":
-- 慢:user_id 没索引,type=ALL,扫描全表 EXPLAIN SELECT * FROM orders WHERE user_id = 1001 AND status = 'done' ORDER BY created_at DESC LIMIT 20; -- 第 1 步优化:给过滤列加联合索引(顺序=最左前缀:先 user_id 再 status,再带 created_at) CREATE INDEX idx_uid_status_ctime ON orders (user_id, status, created_at DESC); -- 再 EXPLAIN:type 变成 ref,key=idx_uid_status_ctime,rows 从全表降到该用户的行数; -- ORDER BY 也被索引覆盖,不再额外 filesort。这就是"建对一个索引,慢查询秒回"。
判断标准一句话:EXPLAIN 出来 type=ALL 且 rows 很大 = 必须加索引或改写;出现 Using filesort / Using temporary 在大数据量下也要警惕。慢查询日志怎么开,见 §07。
下面是工作台业务天天用到的 SQL,按 DDL / DML / 查询 / 索引 / 事务 / 函数 / 权限 分好,直接照抄改表名即可。
-- 建表:对齐工作台 Task 模型(见工作台契约 §5) CREATE TABLE tasks ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL, note TEXT, done TINYINT(1) NOT NULL DEFAULT 0, created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), deleted_at DATETIME(6) NULL DEFAULT NULL, -- 软删除标记列,见 §06 PRIMARY KEY (id), KEY idx_done (done), KEY idx_deleted (deleted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ALTER TABLE tasks ADD COLUMN priority INT NOT NULL DEFAULT 0; -- 加列 ALTER TABLE tasks MODIFY COLUMN title VARCHAR(255) NOT NULL; 改列定义 DROP TABLE IF EXISTS tasks; 删表(生产慎用)
-- 插入(批量) INSERT INTO tasks (title, note) VALUES ('写周报', '本周进展'), ('买菜', '周末'); -- 更新(永远带 WHERE!) UPDATE tasks SET done = 1 WHERE id = 1; -- 删除(硬删)。生产多用软删:UPDATE tasks SET deleted_at=NOW() WHERE id=1; DELETE FROM tasks WHERE id = 1;
-- 条件 + 排序 + 分页(深分页优化见 §06) SELECT id, title, done, created_at FROM tasks WHERE done = 0 AND deleted_at IS NULL ORDER BY id DESC LIMIT 20 OFFSET 0; -- 分组统计 SELECT done, COUNT(*) AS cnt FROM tasks WHERE deleted_at IS NULL GROUP BY done; -- JOIN:用户与其任务(工作台 User 1 : N Task) SELECT u.username, t.title, t.done FROM users u JOIN tasks t ON t.user_id = u.id WHERE u.id = 1 ORDER BY t.id DESC;
BEGIN; -- 开始事务 UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 两条都成功才 COMMIT;任一步出错执行 ROLLBACK 全部回滚 COMMIT; -- ROLLBACK; -- SAVEPOINT:事务内局部回滚 BEGIN; INSERT INTO tasks(title) VALUES ('A'); SAVEPOINT sp1; INSERT INTO tasks(title) VALUES ('B'); ROLLBACK TO sp1; -- 回掉 B,保留 A COMMIT;
-- 常用函数 NOW() -- 当前时间,带秒 UNIX_TIMESTAMP() -- 秒级时间戳 CONCAT(a, b) -- 拼接 IFNULL(x, 0) -- 空值替换 DATE(created_at) -- 取日期部分 -- 用户与权限(生产按最小授权) CREATE USER 'wb'@'127.0.0.1' IDENTIFIED BY '强密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON workbench.* TO 'wb'@'127.0.0.1'; -- 只读/应用账号不给 DROP/ALTER SHOW GRANTS FOR 'wb'@'127.0.0.1'; REVOKE INSERT ON workbench.* FROM 'wb'@'127.0.0.1'; DROP USER 'wb'@'127.0.0.1';
这是本模块核心。方案 A(database/sql 标准库)打功底,方案 B(GORM)是工作台实际用的,方案 C(sqlc)简介。每个方案都给"建表 → CRUD → 事务"完整可运行代码。Go 版本 1.27.x(见 09)。
标准库 database/sql 是一切 ORM 的底座。依赖:github.com/go-sql-driver/mysql v1.10.1(2026-09-02 锁定)。连接池三参数必须设,否则生产必踩坑。
-- go.mod(初始化:go mod init mysql-demo)
module mysql-demo
go 1.27
require github.com/go-sql-driver/mysql v1.10.1
// main.go —— database/sql 完整示例:连接池 + CRUD + 事务 + 预处理 package main import ( "database/sql" "fmt" "log" "time" _ "github.com/go-sql-driver/mysql" // 匿名注册 driver ) func main() { // DSN 关键参数:parseTime=true(否则 time.Time 扫不进来)、loc=Local、charset=utf8mb4 dsn := "wb:WbPass#2026@tcp(127.0.0.1:3306)/workbench?charset=utf8mb4&parseTime=true&loc=Local" db, err := sql.Open("mysql", dsn) if err != nil { log.Fatal(err) } defer db.Close() // 连接池三参数(生产必设) db.SetMaxOpenConns(25) // 最大并发连接数 db.SetMaxIdleConns(5) // 空闲保持的连接数 db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活,防服务端主动断 if err := db.Ping(); err != nil { log.Fatal("连不上 MySQL:", err) } fmt.Println("连接 OK") createTable(db) id := insertTask(db, "用 sql 写周报", "标准库版") getTask(db, id) updateTask(db, id) transferDemo(db) // 事务示例 listTasks(db) } func createTable(db *sql.DB) { _, err := db.Exec(`CREATE TABLE IF NOT EXISTS tasks ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL, note TEXT, done TINYINT(1) NOT NULL DEFAULT 0, created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`) if err != nil { log.Fatal(err) } } // 预处理插入:? 占位符防 SQL 注入 func insertTask(db *sql.DB, title, note string) int64 { res, err := db.Exec("INSERT INTO tasks (title, note) VALUES (?, ?)", title, note) if err != nil { log.Fatal(err) } id, _ := res.LastInsertId() fmt.Println("插入成功 id =", id) return id } func getTask(db *sql.DB, id int64) { var title string var done int err := db.QueryRow("SELECT title, done FROM tasks WHERE id = ?", id).Scan(&title, &done) if err == sql.ErrNoRows { fmt.Println("没这条") return } if err != nil { log.Fatal(err) } fmt.Printf("读到:title=%s done=%d\n", title, done) } func updateTask(db *sql.DB, id int64) { _, err := db.Exec("UPDATE tasks SET done = 1 WHERE id = ?", id) if err != nil { log.Fatal(err) } fmt.Println("已标记完成 id =", id) } // 事务:两步写,要么全成要么全败 func transferDemo(db *sql.DB) { tx, err := db.Begin() if err != nil { log.Fatal(err) } // 任一失败都 rollback;全部成功才 commit if _, err := tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); err != nil { tx.Rollback() log.Fatal(err) } if _, err := tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); err != nil { tx.Rollback() log.Fatal(err) } if err := tx.Commit(); err != nil { log.Fatal(err) } fmt.Println("事务提交成功") } func listTasks(db *sql.DB) { rows, err := db.Query("SELECT id, title, done FROM tasks ORDER BY id DESC LIMIT 10") if err != nil { log.Fatal(err) } defer rows.Close() // 必须 Close,否则连接泄漏 for rows.Next() { var id int64 var title string var done int if err := rows.Scan(&id, &title, &done); err != nil { log.Fatal(err) } fmt.Printf(" #%d %s done=%d\n", id, title, done) } }
运行:go mod tidy 然后 go run .。
工作台(见 11 模块)用 GORM:默认 SQLite,通过 DB_DRIVER 环境变量切 MySQL。版本坑先说清:GORM 模块 tag 是 v1.31.2,但 API 是 v2 线(全系列统一表述)。依赖:gorm.io/gorm v1.31.2 + gorm.io/driver/mysql v1.6.x(驱动随 gorm 主版本取最新兼容补丁,go.sum 锁定)。
工作台切换的核心:只换 DSN 与驱动,模型/handler 一行不改。下面是工作台 internal/db/db.go 的 MySQL 分支写法:
// internal/db/db.go —— 工作台按 DB_DRIVER 切换 sqlite / mysql(契约 §2/§7) package db import ( "fmt" "os" "time" "github.com/glebarez/sqlite" // 工作台默认驱动(纯 Go,无 CGO) "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) func New() (*gorm.DB, error) { driver := os.Getenv("DB_DRIVER") // "sqlite"(默认)或 "mysql" cfg := &gorm.Config{ Logger: logger.Default.LogMode(logger.Warn), // 开发可改 logger.Info 看 SQL } if driver == "mysql" { // MySQL DSN:用户:密码@tcp(主机:端口)/库?参数 dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_NAME")) gdb, err := gorm.Open(mysql.Open(dsn), cfg) if err != nil { return nil, err } // GORM 底层就是 database/sql,连接池照样要设 sqlDB, _ := gdb.DB() sqlDB.SetMaxOpenConns(25) sqlDB.SetMaxIdleConns(5) sqlDB.SetConnMaxLifetime(5 * time.Minute) return gdb, nil } // 默认 SQLite 分支(工作台本地默认) return gorm.Open(sqlite.Open(os.Getenv("DB_PATH")), cfg) }
本地用 MySQL 模式跑工作台,只需设环境变量(PowerShell):
$env:DB_DRIVER="mysql" $env:DB_USER="wb"; $env:DB_PASSWORD="WbPass#2026" $env:DB_HOST="127.0.0.1"; $env:DB_PORT="3306"; $env:DB_NAME="workbench" $env:JWT_SECRET="本地开发随机串" go run ./cmd/server
GORM 的 CRUD / 自动迁移 / 事务 / 钩子,对照工作台模型(契约 §5 的 User、Task):
// CRUD + 自动迁移 + 事务 + 钩子(MySQL 与 SQLite 行为一致) import "gorm.io/gorm" // 自动迁移:建表/加列(不删列不改类型),开发期用,生产用迁移工具更稳 db.AutoMigrate(&User{}, &Task{}) // Create task := Task{Title: "用 GORM 写周报", Note: "ORM 版"} db.Create(&task) // Read:主键查 / 条件查 var t Task db.First(&t, 1) // SELECT * FROM tasks WHERE id=1 db.Where("done = ?", false).Find(&tasks) // 条件列表 // Update db.Model(&t).Update("done", true) db.Model(&t).Updates(Task{Done: true, Note: "改了"}) // Delete:默认是硬删;配 gorm.DeletedAt 字段即变软删(见 §06) db.Delete(&t, 1) // 事务:func 闭包版,自动 commit/rollback db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&Account{}).Update("balance", gorm.Expr("balance - ?", 100)).Error; err != nil { return err // 返回 error 自动回滚 } if err := tx.Model(&Account{}).Where("id=?", 2).Update("balance", gorm.Expr("balance + ?", 100)).Error; err != nil { return err } return nil // 返回 nil 自动提交 }) // 钩子:写库前后自动触发,如创建时加密密码 func (u *User) BeforeCreate(tx *gorm.DB) error { u.Password = hashBCrypt(u.Password) // bcrypt 哈希,见 09 版本自核验项 return nil }
方案 B 的纪律:模型与 handler 写成"DB_DRIVER 无关",只在 db.go 一处感知数据库。这样本地 SQLite、服务器 MySQL 共用一套代码——这正是工作台契约 §2 的设计本意。
sqlc 一句话定位:你写原生 SQL,它用 Go 类型安全地生成代码——既避免手写 Scan(&...) 错位,又没有 ORM 的黑盒。锁定 v1.31.1(2026-04-22)。适合对 SQL 可控性要求高、不想学 ORM 的场景;工作台仍以 GORM 为主,这里给最小示例:
-- query.sql —— 你写 SQL,注释里命名生成的函数
-- name: GetTask :one
SELECT id, title, done FROM tasks WHERE id = ?;
-- name: ListTasks :many
SELECT id, title, done FROM tasks ORDER BY id DESC LIMIT ?;
-- name: CreateTask :execresult
INSERT INTO tasks (title, note) VALUES (?, ?);
# 配置好 sqlc.yaml(指定引擎 mysql)后一行生成类型安全的 Go 代码 sqlc generate # 产物里直接拿到 Queries.GetTask(ctx, id) 这种带类型的函数,编译期就对字段。 # 与 database/sql 的关系:sqlc 底层仍生成 database/sql 调用,DSN/连接池设置与方案 A 完全相同。
业务开发高频遇到的六个点,每个给"怎么做 + 一句原因"。
| 场景 | 怎么做 | 为什么 |
|---|---|---|
| 分页 | 小偏移用 LIMIT 20 OFFSET n;深分页(offset 上万)改用"游标分页":WHERE id < 上次最大id ORDER BY id DESC LIMIT 20。 | OFFSET 越大扫的行越多;游标分页永远只扫 20 行。 |
| 唯一键防重 | 对业务唯一字段加 UNIQUE KEY(如 username);并发重复插入靠唯一约束兜底,捕获 1062 错误。 | 先查再插在并发下有竞态;数据库唯一约束是最后防线。 |
| 软删 vs 硬删 | 业务数据用软删:加 deleted_at 列,GORM 配 gorm.DeletedAt;硬删只用于日志/临时数据。所有查询带 deleted_at IS NULL。 | 软删可恢复、可审计;硬删误删难救。 |
| 时间字段 | 用 DATETIME(6) 存 UTC 或本地;DSN 带 parseTime=true&loc=Local;Go 侧用 time.Time。 | 不带 parseTime,time.Time 扫描直接报错(见 §08)。 |
| 慢查询定位 | 开 slow_query_log(§07),对慢 SQL 跑 EXPLAIN,按 §3.4 加索引。 | 慢查询是 1GB 服务器最大的内存/CPU 杀手。 |
| 连接数管理 | Go 侧 SetMaxOpenConns(25) 等三参数(§5.1);服务器 max_connections 与应用连接数总和匹配(§07)。 | 连接开爆 = Too many connections 全线拒绝服务。 |
生产库"能跑"只是开始,"丢不了"才是底线。本节给:全量备份命令、cron 定时脚本、恢复步骤、binlog 一句话、以及 1GB 服务器的内存压法。
# 全量备份 workbench 库到带时间戳的文件(单服务器备份) mkdir -p /var/backups/mysql mysqldump -u root -p'强密码' \ --single-transaction \ # InnoDB 一致性快照,不锁表(关键) --routines --triggers \ workbench | gzip > /var/backups/mysql/workbench_$(date +%F).sql.gz # 恢复:先建空库再导入 mysql -u root -p'强密码' -e "CREATE DATABASE IF NOT EXISTS workbench DEFAULT CHARSET utf8mb4;" gunzip -c /var/backups/mysql/workbench_2026-10-14.sql.gz | mysql -u root -p'强密码' workbench
binlog 一句话:binary log 记录所有写操作,可做"时间点恢复"(备份后某时刻误删,先恢复全量备份、再重放 binlog 到误删前一秒)。本教程低配场景先靠每日全量 + binlog 保留 7 天,足够;深度运维见 20 模块。
# /usr/local/bin/mysql-backup.sh —— 每天凌晨 3:10 跑,保留最近 7 天 #!/bin/bash set -euo pipefail BK_DIR=/var/backups/mysql mkdir -p "$BK_DIR" FILE="$BK_DIR/workbench_$(date +%F).sql.gz" mysqldump -u root -p'强密码' --single-transaction --routines --triggers workbench | gzip > "$FILE" # 只留最近 7 天,防止磁盘被备份撑爆 find "$BK_DIR" -name 'workbench_*.sql.gz' -mtime +7 -delete echo "$(date) backup -> $FILE"
# 加可执行权限 + 挂到 root crontab(crontab -e) chmod +x /usr/local/bin/mysql-backup.sh # crontab 行:每天 03:10 执行 10 3 * * * /usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1
1GB 内存机器上 MySQL 默认配置会 OOM。核心是把 innodb_buffer_pool_size 压小(它默认会拿物理内存的一半左右,1GB 机器拿一半给数据库,系统和 Go 进程就没了)。编辑 /etc/mysql/my.cnf(或 Debian 的 /etc/mysql/mysql.conf.d/mysqld.cnf):
# /etc/mysql/mysql.conf.d/zz-lowmem.cnf —— 1GB 服务器专用,改完 systemctl restart mysql [mysqld] innodb_buffer_pool_size = 96M # 压到 64–128M,小库足够;留内存给系统+Go进程 innodb_log_file_size = 48M innodb_flush_log_at_trx_commit = 2 # 低配牺牲一点持久性换性能(有备份兜底) max_connections = 30 # 1GB 机器别开大;Go 侧连接池总和要 < 此值 slow_query_log = 1 # 开慢查询日志 long_query_time = 1.0 # 超过 1 秒记为慢查询 slow_query_log_file = /var/log/mysql/slow.log skip-name-resolve # 反查 DNS 拖慢连接,关掉 # 配完重启并确认内存占用 systemctl restart mysql free -h # 看 available 还剩多少给 Go 进程
innodb_doublewrite 可保留(它防崩溃坏页,代价不大);若内存仍紧张再考虑。优先压 buffer_pool,而不是关安全特性。按 08 §8.1 分工表复述一遍,帮你在具体项目里做判断:
| 场景 | 选谁 | 理由 |
|---|---|---|
| 个人工具/原型/单机 | SQLite | 单文件零运维,1GB 机器首选;工作台本地默认(见 19 模块)。 |
| 给客户的网站/管理系统 | MySQL(本模块) | 多用户并发写、要独立服务、生态招人多、你点名常用。 |
| 金融/复杂查询/JSONB/严格事务 | PostgreSQL | 功能最强、类型最丰富(见 17 模块)。 |
| 缓存/限流/队列/会话 | Redis | 内存加速层,不是主存储(见 18 模块)。 |
闭关期间卡住,先查这张表。每条都是真实会遇到的报错原文 + 原因 + 照做解法。
| 报错原文(节选) | 原因 | 解决 |
|---|---|---|
Access denied for user 'wb'@'localhost' (using password: YES) | 密码错 / 用户不存在 / 来源 IP 不匹配 | 核对密码;确认建用户时 host 是 127.0.0.1 还是 localhost;8.4 默认 caching_sha2_password,go-sql-driver v1.10.1 已支持,DSN 正常即可,别用老驱动。 |
Unknown database 'workbench' | 库名拼错或没建库 | SHOW DATABASES; 看;按 §2.3 建库。 |
Table 'workbench.tasks' doesn't exist | 表没建(GORM 自动迁移没跑) | 确认 db.AutoMigrate() 执行过;或按 §4.1 手动建表。 |
Error 1062: Duplicate entry 'xxx' for key 'username' | 违反唯一键,重复插入 | 业务上这是"防重生效";捕获 1062 转为"用户名已存在"提示。 |
Error 1205: Lock wait timeout exceeded | 行锁等待超时:长事务没提交/回滚 | 缩短事务、别在事务里做远程调用;查 information_schema.innodb_trx 找卡住的事务并 KILL。 |
Can't connect to MySQL server on '127.0.0.1:3306' (connect) | 服务没起 / 端口不对 / 防火墙挡 | systemctl status mysql;容器用 docker compose ps;核对端口(本机若改了 13306,DSN 同步改);云服务器安全组放行 3306(生产建议只放内网)。 |
Error 1040: Too many connections | 连接数打满(应用连接池总和 > max_connections) | Go 侧设 SetMaxOpenConns(25);服务器 max_connections 与连接池总和匹配(§7.3);查 SHOW PROCESSLIST; 找泄漏。 |
Error 1153: Got a packet bigger than 'max_allowed_packet' bytes | 一次插入/查询的包超过上限(如传大 TEXT/BLOB) | 调大 max_allowed_packet=64M;或分批插入,别一次塞超大字段。 |
sql: unknown driver "mysql" (forgotten import?) | 忘了匿名导入驱动包 | 确认有 import _ "github.com/go-sql-driver/mysql";GORM 方案则确认导入 gorm.io/driver/mysql。 |
Error 1366: Incorrect string value / 中文变 ???? | 字符集不是 utf8mb4(库/表/连接三处之一) | 建库建表用 utf8mb4;DSN 带 charset=utf8mb4;已有的表用 ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。 |
Scan error on column index N: unsupported Scan... into *time.Time | DSN 没带 parseTime=true | DSN 加 parseTime=true&loc=Local(§5.1),重连。 |
Error 2006: MySQL server has gone away | 连接被服务端主动断开(空闲超时 > wait_timeout) | Go 侧设 SetConnMaxLifetime(5*time.Minute) 小于服务端 wait_timeout;数据库/sql 会自动重连,业务不用管。 |
两项硬验收 + 一组自查。全部打勾,本模块才算通关,可进入 17 PostgreSQL 专题。
DB_DRIVER=mysql 及连接环境变量启动工作台;curl /healthz 正常;登录、任务增删改查在 MySQL 库表中真实落库(用 SELECT * FROM tasks; 亲眼看到数据)。docker compose ps 看到 mysql84 running,字符集查询结果是 utf8mb4。go run . 完整跑通(CRUD + 事务无报错)。free -h available 正常。本页版本号全部来自全系列唯一版本事实来源(09 版本总表 / versions.md)。
| MySQL 8.4.12 LTS 发布说明(2026-08-18);9.7 LTS / 26.7 innovation 双轨说明 | https://dev.mysql.com/doc/relnotes/mysql/8.4/en/ |
| go-sql-driver/mysql v1.10.1(2026-09-02);caching_sha2_password 支持说明 | https://github.com/go-sql-driver/mysql/releases |
| GORM v1.31.2(v2 API 线,模块 tag v1.x 序列)与 mysql 驱动 | https://pkg.go.dev/gorm.io/gorm · https://pkg.go.dev/gorm.io/driver/mysql |
| sqlc v1.31.1(2026-04-22),类型安全 SQL 生成 | https://github.com/sqlc-dev/sqlc/releases |
| Docker Desktop 4.94.0 / Docker Engine 29.8.2(本机容器);1GB 服务器不装 Docker 的依据 | https://docs.docker.com/desktop/release-notes/ · https://docs.docker.com/engine/release-notes/29/ |
| Debian 13.7 "trixie" stable 仓库内置 mysql-server 8.4 打包 | https://www.debian.org/releases/ |
| mysqldump --single-transaction、InnoDB buffer pool、慢查询日志等官方手册 | https://dev.mysql.com/doc/refman/8.4/en/ |
| 四数据库分工(SQLite/MySQL/PG/Redis)与工作台 DB_DRIVER 契约:本系列 08 §8.1、11 工作台契约 | file:///D:/02-Docs/金融知识/08-闭关总纲.html · file:///D:/02-Docs/金融知识/11-工作台实战.html |
(编者整理:本页的安装步骤、Go 示例、cron 脚本、my.cnf 调优参数、报错对照表为基于官方文档与工作台契约的实战化整理,无外部独立来源。)