读完这篇,你能做什么
能在练习库执行 CRUD 和回滚实验,并解释应用查询不能代替唯一约束。
学习理由、准备要求与知识点
为什么学它
数据库约束和事务保护业务事实,是 JDBC、ORM 与订单并发控制的基础。
先读这些内容
知识点清单
- 表与列类型
- 主键非空唯一
- 外键与级联规则
- INSERT
/SELECT/UPDATE/DELETE - WHERE 更新边界
InnoDB事务- autocommit
/commit/rollback - DDL 隐式提交
运行前先确认MySQL8.4、InnoDB与Java17;Java客户端源码固定Connector/J9.0.0。算法是单线程内存事务模型,没有持久化、隔离或崩溃恢复能力,不是MySQL内核实现。
01先弄懂它是什么
CRUD 就是新增、查询、修改和删除。练习前先说明表中一行是什么,再选主键和字段限制。例如商户用 id 区分记录,name 不允许缺失,phone 若必须填写且不能重复,就同时使用 NOT NULL 和 UNIQUE。普通索引主要帮助查找,不会自动禁止重复;唯一约束对 NULL 的规则,也不等同于必填。
数据库约束能让不同写入入口都遵守同一规则。应用先查有没有再插入,可以改善提示,却挡不住两个请求同时查到“没有”。唯一约束负责最后阻止重复,应用再把重复键错误转换成业务提示。外键检查引用的数据是否存在;级联删除会顺带影响其他记录,必须先确认这些数据确实应该一起删除。
事务把多条相关修改作为一个提交单元。自动提交开启时,每条独立语句通常各自构成事务;START TRANSACTION开始显式事务,COMMIT确认修改,ROLLBACK撤销尚未提交的事务修改。原子性不代表任何错误都会自动回滚整个事务,InnoDB对语句错误、死锁和锁等待超时有不同处理边界。
文中这几个词,先认一认
- 主键
- 稳定区分每一行的标识;在
InnoDB中,行数据还按主键组织。 - 业务唯一约束
- 把业务不能重复的字段或组合交给数据库检查,即使并发写入也要遵守。
- 事务原子性
- 同一事务中支持事务的修改一起提交或撤销,不会自动覆盖远程服务、Redis 或普通 DDL。
02看看它怎么工作
在已创建的练习库先执行独立DDL:CREATE TABLE study_shop(id BIGINT PRIMARY KEY,name VARCHAR(100) NOT NULL,phone VARCHAR(32) NOT NULL UNIQUE) ENGINE=InnoDB。
确认表结构后再插入一行,执行SELECT观察初态。建表与事务实验分开,因为MySQL许多DDL会隐式提交,不能靠外层ROLLBACK撤销普通建表。
随后START TRANSACTION,按id更新name,再删除同一行,在当前连接查询可看到自己修改;执行ROLLBACK后再次查询,原记录应恢复。把ROLLBACK改为COMMIT可观察持久结果。每次实验重新确认初态和同一连接,避免把不同客户端窗口、自动提交设置或旧数据混进结论。
对库存或余额等规则,带条件UPDATE比先查再写更接近原子裁决:UPDATE stock SET available=available-1 WHERE id=? AND available>0,并检查影响行数。多个账户转账还要保护两次修改在同一事务中,保证余额约束和总量不变。事务隔离与锁决定并发交互,后续需要专门实验,不能从单连接回滚测试推断高并发正确。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["独立建表与约束"] --> B["准备确定初态"]
B --> C["开始事务"]
C --> D["执行相关DML"]
D --> E{"业务及语句结果符合要求"}
E -->|"是"| F["提交"]
E -->|"否"| G["回滚或错误恢复"]
F --> H["重新查询最终状态"]
G --> H按顺序看,每一步做了什么
- 选择隔离练习库,创建
InnoDB表并核对约束。 - 插入固定初态并保存查询结果。
- 在同一连接开始事务,执行有限范围的更新和删除。
- 分别回滚与提交,查询并对比最终状态。
- 测试重复业务键及条件更新影响行数,记录失败边界。
03对照实现代码
MySQL Connector/J9.0.0 · ConnectionImpl#commit
原创Java客户端状态模型,非源码节选。它表示提交请求和确认边界,MySQL服务端事务由其自身引擎实现,不能把这个类称为数据库内核。
class SourceModel {
enum Result { COMMITTED, UNKNOWN }
static Result commit(boolean autoCommit, boolean acknowledgementReceived) {
// JDBC显式提交需遵守当前连接的事务模式。
if (autoCommit) throw new IllegalStateException("autoCommit enabled");
// 教学上表示COMMIT已发送,真实驱动通过会话执行SQL。
boolean sent = true;
if (sent && !acknowledgementReceived) return Result.UNKNOWN;
// 收到成功确认才报告已知提交,未知不能当作已回滚。
return Result.COMMITTED;
}
}真实commit检查连接状态及自动提交设置,说明连接会话模式是提交调用的前提。
部分本地事务状态优化可让无事务提交成为空操作;实际分支受驱动配置与服务端状态影响。
主要路径通过 session.execSQL 把 COMMIT 发给 MySQL。Java 代码在这里负责客户端请求,并没有自己实现数据库内部的 redo 或 undo。
通信失败可被转换为事务结果未知的SQLState。客户端没有收到确认,不等于数据库没有提交;可靠业务需要业务键和状态查询处理不确定性。
代码先调用谁,再调用谁
- 业务事务管理器或JDBC调用commit
ConnectionImpl检查连接和模式session.execSQL发送COMMIT- 接收结果或通信异常
- 应用处理已知结果与未知状态
04动手写一个例子
复制工作状态后一次发布的批量转账
在单线程内存账户表中顺序执行一批正金额转账。账户必须存在、双方不同、余额不能为负且目标余额不能溢出;任一步失败,整个批次不改变原状态。算法只演示原子发布与不变量。
- 输入 1
A=100、B=0,转账A→B30- 输出 1
A=70、B=30
- 输入 2
先转30再转100,初态A=100- 输出 2
整批失败,仍A=100、B=0
先复制账户表形成工作状态,后续每步都在副本中验证和修改。只有全部成功才返回新快照;失败时原表从未改变,达到单线程模型中的整体撤销效果。
一次转账先检查两账户、正金额和余额,再计算目标新余额,保证溢出发生在修改前。金额使用最小货币单位的整数,避免浮点误差。
每一步源账户减少量与目标增加量相同,因此总额不变;同时余额非负。在真实数据库中需要条件更新、事务与并发控制,这个复制模型不具备多线程提交冲突检测。
为什么这样写是对的每个已完成转账保持账户总额不变且余额非负;批次失败时调用方原始映射保持不变。
A为账户数,复制一次;T笔转账每笔常数次哈希查询与更新,最后生成不可变快照。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
import java.math.BigInteger;
public class Main {
record Transfer(String from,String to,long amount) {}
static Map<String,Long> apply(Map<String,Long> original,List<Transfer> transfers) {
Map<String,Long> work=new HashMap<>(original);
for (long balance:work.values())
if (balance<0) throw new IllegalArgumentException("negative initial");
for (Transfer t:transfers) {
if (t.amount()<=0||t.from().equals(t.to())||
!work.containsKey(t.from())||!work.containsKey(t.to()))
throw new IllegalArgumentException("transfer");
long source=work.get(t.from()), target=work.get(t.to());
if (source<t.amount()) throw new IllegalArgumentException("insufficient");
// 先完成所有计算,目标溢出时不修改工作状态。
long newTarget=Math.addExact(target,t.amount());
work.put(t.from(),source-t.amount());
work.put(t.to(),newTarget);
}
// 整批成功才发布新快照,调用方原始状态始终未动。
return Map.copyOf(work);
}
static BigInteger total(Map<String,Long> balances) {
BigInteger sum=BigInteger.ZERO;
// 校验总额用大整数,避免测试断言自身溢出。
for (long x:balances.values()) sum=sum.add(BigInteger.valueOf(x));
return sum;
}
static void eq(Object expected,Object actual) {
if (!Objects.equals(expected,actual)) throw new AssertionError(actual);
}
static void reject(Runnable r) {
try { r.run(); throw new AssertionError("accepted"); }
catch (IllegalArgumentException | ArithmeticException expected) {}
}
public static void main(String[] args) {
Map<String,Long> initial=Map.of("A",100L,"B",0L);
Map<String,Long> result=apply(initial,List.of(new Transfer("A","B",30)));
eq(Map.of("A",70L,"B",30L),result);
eq(total(initial),total(result));
reject(()->apply(initial,List.of(new Transfer("A","B",30),new Transfer("A","B",100))));
eq(Map.of("A",100L,"B",0L),initial);
reject(()->apply(initial,List.of(new Transfer("A","C",1))));
reject(()->apply(Map.of("A",1L,"B",Long.MAX_VALUE),List.of(new Transfer("A","B",1))));
eq(initial,apply(initial,List.of()));
reject(()->apply(initial,List.of(new Transfer("A","A",1))));
System.out.println("PASS 8 transaction invariants");
}
}这些特殊情况也要试一试
- 空批次返回等值快照;缺失账户、自转账与非正金额拒绝。
- 后续转账失败必须保持初态,即使此前步骤在副本中成功。
- 目标余额溢出拒绝;总额断言使用大整数避免测试本身溢出。
进阶选读 · 并发提交的版本冲突与幂等恢复
场景两个事务基于同一初态转账,或客户端提交后断线不知道结果。
做法为业务操作设置唯一键,事务内保存操作结果;并发更新通过锁或版本条件裁决。提交未知后按业务键查询和协调重试,不直接重复执行扣款。
复杂度内存版本比较可O(1),数据库成本取决于索引、锁等待及写入;恢复需要有界重试与记录。
好处与代价幂等与并发控制增加数据和流程成本,但单纯重试或仅保存客户端结果都不能覆盖提交未知窗口。
05项目里怎么用
DML执行前明确WHERE和预计影响行数。更新零行可能表示不存在、条件不满足或记录未发生变化,具体还受接口和驱动影响;不能把任意非异常执行都当业务成功。批量操作先限定主键集合与数量,在隔离环境验证后再设计生产执行流程。
数据库约束与应用校验各有作用。应用校验提供友好提示与业务授权,数据库唯一、非空和外键保护共享写入边界。金额选择精确表示,文本长度与字符集符合输入约定。外键级联会改变删除范围,应明示并测试,而不是为省代码无条件启用。
事务只包必要的数据库修改,尽量别拿着锁等待漫长远程调用。失败时先弄清回滚的是一条语句还是整笔事务;死锁等明确可重试情况,要重新执行整个事务;提交结果未知时先查询或对账。数据库事务不自动覆盖 Redis、MQ 和 HTTP,跨系统步骤要另有可靠处理办法。
容易踩的坑
- 先查不存在代替唯一约束,忽略并发窗口。
- 漏写WHERE或不检查影响行数。
- 把普通DDL放入ROLLBACK实验并期待撤销建表。
- 把通信异常直接当作未提交后重复扣款。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 应用校验 | 提示友好,适合权限和业务上下文。 | 无法单独阻止所有并发入口。 | 用户输入与用例规则。 |
| 数据库约束 | 所有写入共同经过的裁决边界。 | 需要处理冲突和迁移成本。 | 唯一、非空与引用完整性。 |
| 显式事务 | 相关修改整体提交。 | 长事务增加锁等待和资源占用。 | 同库必要关联写入。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01UNIQUE自动禁止NULL吗?
展开参考答案与推导
不自动等同NOT NULL;MySQL唯一索引对NULL有特定语义,必填需另加非空约束。
02任何SQL错误都会回滚整个事务吗?
展开参考答案与推导
不会,不同InnoDB错误可能回滚语句或事务,要依据错误类型处理。
03Java驱动commit实现了MySQL事务内核吗?
展开参考答案与推导
没有,它组织客户端请求和结果处理,服务端引擎负责事务存储机制。
换个情况,还会处理吗
01两个请求同时注册相同手机号怎么办?
展开参考答案与推导
以非空唯一约束裁决,应用捕获重复键并返回明确结果。
02COMMIT后网络断开怎么决定重试?
展开参考答案与推导
视为结果可能未知,按稳定业务键查询或对账,避免直接重复执行。
03回滚后新建表还在说明什么?
展开参考答案与推导
普通DDL不遵守该DML回滚假设,需查隐式提交规则并分离实验。
写代码试一试
01如何验证批次后半段失败时原状态不变?
- 输入 / 条件
先成功转30再因余额不足失败- 输出 / 预期
原始A=100、B=0
提示 所有修改留在副本。
展开参考答案与推导
捕获失败后比较原映射与初始快照,不能只断言异常。
02怎样检查金额守恒而不让断言溢出?
- 输入 / 条件
多个接近long上限余额- 输出 / 预期
精确总额相等
提示 断言使用BigInteger。
展开参考答案与推导
将每项余额转大整数再相加,比较前后总额。
03怎样增加并发版本保护?
- 输入 / 条件
两个事务读取同一版本- 输出 / 预期
仅一个成功发布,其余重试
提示 提交时比较版本。
展开参考答案与推导
将状态与版本一起原子比较替换;数据库可用版本条件UPDATE并检查影响行数。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能在练习库执行 CRUD 和回滚实验,并解释应用查询不能代替唯一约束。
本篇重点加练
重点加练约束与写入范围,保证输入错误和匹配范围变化时不会静默破坏数据。
- 主键非空唯一
- WHERE 更新边界
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01设计评审
为课程、学生和选课三张表选择列类型及主外键,业务要求同一学生同一课程只选一次,课程删除时须先决定历史记录政策。
- 提交内容
- 建表草案和约束对应表。
- 怎样算完成
- 数据类型依据取值范围
- 唯一性与引用关系明确
练到的知识点表与列类型 · 主键非空唯一 · 外键与级联规则
- 作业 02编程实践
在独立练习库编写新增课程、按 ID 查询、只更新指定学生选课状态和删除指定试验记录的 SQL;每次修改前后保留受影响行核验。
- 提交内容
- SQL 文件、自写零行/单行/重复输入测试及执行证据。
- 怎样算完成
- 写操作边界可验证
- 不修改其他库或未知记录
练到的知识点INSERT/SELECT/UPDATE/DELETE · WHERE 更新边界
综合应用
- 作业 03排错实验
用两个本地连接观察同一条练习记录:连接 A 显式开始事务并修改,分别选择提交与回滚;连接 B 的读事务设置必须记录。比较每个阶段的查询结果。
- 提交内容
- 会话脚本、时间线、自写检查及实际查询记录。
- 怎样算完成
- 观察前提包含隔离级别和会话状态
- 不把客户端显示当作所有会话可见
练到的知识点InnoDB 事务 · autocommit/commit/rollback
- 作业 04面试场景
迁移脚本在 DML 后执行 ALTER TABLE,再发生失败并尝试回滚。制定隔离环境中的验证方案,列出恢复策略需要了解的事实。
- 提交内容
- 语句时间线、版本前提与恢复验证清单。
- 怎样算完成
- 考虑 DDL 的提交边界
- 恢复计划不依赖未经测试的假设
练到的知识点DDL 隐式提交
- 作业 05原理问答
数据导入程序同时做应用层重复检查和数据库约束检查。用两个并发导入同一选课关系的时间线说明各层检查应承担的职责,并定义冲突反馈。
- 提交内容
- 并发场景图和错误合同。
- 怎样算完成
- 约束与友好提示分工明确
- 失败后业务状态可核验
练到的知识点主键非空唯一 · 外键与级联规则 · WHERE 更新边界 · InnoDB 事务
- 作业 06编程实践
建立题目专用签到表,学生与日期组成唯一业务关系;批量插入含重复记录的数据,逐次记录受影响行及失败政策,再按精确条件修正一条记录。
- 提交内容
- 隔离 SQL、自写重复/不存在/单行更新检查与运行证据。
- 怎样算完成
- 唯一性与主键职责分别说明
- 写入影响范围逐次核验
练到的知识点主键非空唯一 · WHERE 更新边界
- 作业 07设计评审
学生身份证明尚未齐全但允许先保存草稿,正式提交后编号必须非空且唯一。设计草稿与正式记录的约束边界,说明允许与禁止的状态。
- 提交内容
- 状态/字段矩阵与建表草案。
- 怎样算完成
- 空值语义和业务阶段一致
- 约束可由代表样本验证
练到的知识点主键非空唯一
- 作业 08面试场景
更新语句原本按 id 定位,改为按姓名定位后同名数据出现。建立上线前检查,比较受影响行数、唯一性前提和失败后的恢复证据。
- 提交内容
- 查询与写入验证清单。
- 怎样算完成
- 业务标识与展示字段分开
- 不靠单条历史样本推断唯一
练到的知识点WHERE 更新边界
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
完成数据库迁移基础后:新增必填列时已有历史行且服务不能长时间停机。设计分阶段变更、兼容读写和回滚验证,不要求执行生产迁移。
- 提交内容
- 变更阶段表、失败窗口与演练条件。
- 怎样算完成
- 历史数据处理有合同
- 代码与结构版本兼容可验证
练到的知识点外键与级联规则 · InnoDB 事务 · autocommit/commit/rollback · DDL 隐式提交
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
单学校系统增加多学校租户,学生编号只在校内唯一。重新审查表约束与更新条件,并构造旧 SQL 在新数据模型下的检查案例。
- 提交内容
- 新旧键模型、两学校样本和验收合同。
- 怎样算完成
- 身份范围变化被完整传播
- 读写条件与唯一约束一致
练到的知识点主键非空唯一 · WHERE 更新边界
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
MySQL 8.4:创建表 (新标签页)↗
阅读列类型、非空、主键、唯一键与存储引擎,建立数据合法性约束。
- 实践参考文档英文
MySQL 8.4:提交与回滚 (新标签页)↗
阅读自动提交和显式事务,区分 DML 回滚与 DDL 隐式提交。
- 深入教程英文
Oracle JDBC:PreparedStatement (新标签页)↗
练习参数绑定、查询和更新;教程采用早期 JDK,标准 JDBC 原则仍适用于 Java 17。
- 实践实践指南英文
MySQL 事务小实验:提交与撤销一组修改 (新标签页)↗
先读自动提交和“Grouping DML Operations”。小练习:在专门的练习表中插入两行,回滚后查询,再重新插入并提交;分别写下预期与实际行数。建表与改表语句的提交行为另查手册。 英文手册公开,内容固定为
MySQL8.4 和InnoDB;其他版本或存储引擎应查对应文档。 - 实践实践指南英文
PostgreSQL:让数据库拒绝不合要求的数据 (新标签页)↗
先读 CHECK、NOT NULL,再看唯一约束;暂跳排除约束和复杂级联。给价格列分别设置
CHECK(price > 0)和额外的 NOT NULL,尝试 1、-1、NULL,记录每次是否被接受及原因。 官方文档公开可读,无需登录。执行需要PostgreSQL17 练习库;唯一约束怎样处理 NULL 等细节因数据库而异,不能直接套用到MySQL或 SQL Server。 - 实践实践指南英文
Flyway:把数据库改动按版本保存下来 (新标签页)↗
先按入门页读 V1 建表和 V2 加数据,再读 Versioned migrations;暂跳 Desktop、撤销与修复命令。只在新建练习库执行,交出两个脚本和迁移历史记录;说明已使用的旧版本脚本为什么通常应保留,再增加新版本修正。 文档公开可读。执行需官方对应平台的 Flyway CLI、可用的 JDBC 驱动和可写目录;教程使用本地 SQLite 文件。不同数据库与功能的支持范围需按所选版本核对,不能据此推断全部功能免费。
- 深入参考文档英文
MySQL隐式提交 (新标签页)↗
确认DDL为何不能混入普通回滚实验。
- 深入参考文档英文
InnoDB错误处理 (新标签页)↗
区分语句与事务回滚,建立正确重试范围。
- 深入参考文档英文
Connector/J提交源码 (新标签页)↗
查看COMMIT发送及通信失败时结果未知的客户端处理。