# go-db-lock-lab **Repository Path**: solidify-the-foundation/go-db-lock-lab ## Basic Information - **Project Name**: go-db-lock-lab - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-24 - **Last Updated**: 2026-06-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # go-db-lock-lab 一个用于学习**数据库事务隔离级别和锁机制**的可交互实验平台。 通过浏览器页面,按步骤手动触发事务 A / 事务 B 的每一条 SQL,实时观察脏读、不可重复读、幻读、丢失更新、行锁、间隙锁、死锁、乐观锁、悲观锁等场景在 MySQL 和 PostgreSQL 上的真实行为。 > 📚 想深入了解每个实验背后的原理、MySQL/PG 行为差异、以及 Go 实现细节,请阅读 [`docs/`](./docs/README.md) 目录下的设计文档。 ## 实验清单 | # | 实验 | 演示现象 | 关注点 | |---|------------------|------------------------------------------------|---------------------------------------| | 1 | 脏读 | 事务 B 读到事务 A 未提交的修改 | READ UNCOMMITTED;PG 静默升级 | | 2 | 不可重复读 | 同事务两次读同一行结果不同 | READ COMMITTED | | 3 | 幻读 | 同事务两次范围查询行数不同 | READ COMMITTED vs REPEATABLE READ | | 4 | 丢失更新 | 并发覆盖 vs `UPDATE ... = balance - delta` | 错误写法 vs 原子写法 | | 5 | 行锁 | `SELECT ... FOR UPDATE` 阻塞其他 UPDATE | InnoDB 行锁 | | 6 | 间隙锁 | 范围 `FOR UPDATE` 阻塞区间内 INSERT | InnoDB RR 下的 Gap Lock;PG 不支持 | | 7 | 死锁 | 两事务交叉锁定,DB 检测并回滚受害者 | deadlock detection | | 8 | 乐观锁 | `WHERE version = ?` 防止覆盖 | 应用层并发控制 | | 9 | 悲观锁 | `FOR UPDATE` 全程持锁实现串行写 | 丢失更新的标准修复 | ## 技术栈 - 后端:Go 1.26 · Gin · `database/sql`(不使用 ORM,保留对事务/隔离级别/连接的精确控制)· go-sql-driver/mysql · lib/pq - 前端:Vue 3 · Vite · Element Plus · axios - 数据库:MySQL 8.0、PostgreSQL 16(默认 docker compose 同时拉起,后端通过环境变量 `DB_DRIVER` 切换) - 编排:Docker Compose ## 项目结构 ``` go-db-lock-lab/ ├── docker-compose.yml # MySQL + PostgreSQL + backend + frontend ├── scripts/ │ ├── init.mysql.sql # MySQL 建表 + 种子数据 │ └── init.postgres.sql # PG 建表 + 种子数据 ├── backend/ # Go 服务 │ ├── cmd/server/main.go │ └── internal/ │ ├── config # 环境变量 │ ├── db # *sql.DB + dialect 抽象 (mysql/postgres) │ ├── txmgr # 跨请求保持的事务会话池 │ └── handler # 每个实验一个文件 └── frontend/ # Vue 工程 └── src/views/*.vue # 9 个实验页面 ``` ## 快速开始(推荐:Docker 一键启动) ```bash docker compose up -d --build # 等 mysql/postgres healthcheck 通过后: # 后端:http://localhost:8080/api/health # 前端:http://localhost:5173/ ``` ### 切换数据源 `docker-compose.yml` 同时拉起 MySQL 与 PostgreSQL,后端默认连 MySQL。要切到 PG: ```bash DB_DRIVER=postgres \ DB_DSN='host=postgres port=5432 user=lab password=lab123456 dbname=lock_lab sslmode=disable' \ docker compose up -d --no-deps backend ``` 页面顶部徽标会显示当前 driver 以及方言能力差异(如 `READ UNCOMMITTED 退化为 READ COMMITTED`、`Gap Lock 不支持`),脏读 / 间隙锁等实验页面会基于这些标志显示对应提示。 ### 本地直接跑(不通过 Docker) ```bash # 1. 启动 MySQL(其它任意方式都行;推荐直接用 docker-compose 把 mysql 单独拉起) docker compose up -d mysql # 2. 后端 cd backend go run ./cmd/server # 默认连 127.0.0.1:3306, lab/lab123456, db=lock_lab # 3. 前端(vite dev server,宿主机推荐用 pnpm) cd frontend corepack enable # Node 16.13+ 自带 corepack,启用 pnpm pnpm install pnpm dev # http://localhost:5173, /api 走 Vite 代理到 :8080 ``` > 包管理器策略:**Docker 镜像内使用 npm 构建**(构建机器开箱即用、CI 友好); > **宿主机本地开发建议使用 pnpm**(依赖磁盘占用小、安装快、避免幽灵依赖)。 > 两者构建出的 `dist/` 一致,可以放心混用。 环境变量: | 名称 | 默认值 | 说明 | |-------------------|-----------------------------------------------------------------------------------|-----------------------------------| | `PORT` | `8080` | HTTP 端口 | | `DB_DRIVER` | `mysql` | `mysql` 或 `postgres` | | `DB_DSN` | 见 `internal/config/config.go` | driver-specific DSN | | `TX_IDLE_TIMEOUT` | `60s` | 实验事务空闲多久后被强制回滚 | ## 端到端验证步骤 1. `docker compose up -d --build`,等四个容器就绪。 2. 打开 `http://localhost:5173/`,应看到 9 个实验卡片以及顶端的 driver 徽标。 3. 进入"脏读实验":依次点 ①BEGIN A → ②UPDATE 未提交 → ③BEGIN B → ④SELECT → ⑥ROLLBACK。 - MySQL 下 ④ 应读到 `500`(脏数据); - 切换到 PG 后 ④ 应读到 `1000`(PG 不脏读)。 4. 进入"死锁实验":点 `运行`,应看到受害事务被回滚,徽标显示 `DB 检测到死锁`。 5. 进入"丢失更新":先点"重置数据",再点"错误写法",最终余额应 ≠ 700;点"原子写法"应稳定 = 700。 6. 进入"行锁 / 间隙锁 / 悲观锁":开启 A 持锁 → 点 B 异步 UPDATE/INSERT,B 应处于 `waiting`;A 提交后变为 `done`。MySQL 下间隙锁会让 B 阻塞,PG 下 B 立即完成。 ## 关键实现说明 - **跨请求事务**:每个实验事务在后端 `txmgr.Manager` 中持有一条 `*sql.Conn`,UI 通过返回的 `sessionId` 在后续请求里复用该连接。`*sql.DB` 的连接池本身不能跨请求保持事务。 - **空闲超时**:`txmgr` 5 秒扫一次,超过 `TX_IDLE_TIMEOUT`(默认 60s)的会话强制 ROLLBACK,避免 UI 异常关闭导致连接泄漏。 - **方言抽象**:`internal/db/dialect.Dialect` 处理 MySQL 与 PG 的差异(`SET TRANSACTION ISOLATION` 时机、`?` vs `$N` 占位符、错误码识别、`TRUNCATE` 语法)。新增实验时一律走 `d.Dialect.Placeholder(n)`,禁止硬编码。 - **异步阻塞 SQL**:行锁/间隙锁/悲观锁里"被阻塞的那一侧"由 goroutine 发出,HTTP 返回 `taskId`,前端轮询 `/status/:taskId` 直到 `done` 或 `error`。 ## 已知行为差异(MySQL vs PostgreSQL) | 场景 | MySQL InnoDB | PostgreSQL | |--------------|-------------------------------------------|--------------------------------------------| | READ UNCOMMITTED | 真正实现脏读 | 静默升级为 READ COMMITTED | | REPEATABLE READ | MVCC + Next-Key Lock 多数场景避免幻读 | 快照隔离,整事务一致 | | Gap Lock | 范围 FOR UPDATE 锁住区间 | 无等价机制,区间内 INSERT 立即成功 | | 死锁错误码 | error 1213 | SQLSTATE 40P01 | | 锁等待超时 | error 1205 | SQLSTATE 55P03 | ## 许可证 学习用途,自取自用。