qinghuan
← Java Web / 全部笔记

单元 37 / Java Web

P0 · 必会主线L1 · 基础

MySQL 入门:CRUD、约束与事务

用商户表练习增删改查,再通过唯一约束和事务提交、回滚检查哪些数据规则由数据库保证。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能在练习库执行 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,并检查影响行数。多个账户转账还要保护两次修改在同一事务中,保证余额约束和总量不变。事务隔离与锁决定并发交互,后续需要专门实验,不能从单连接回滚测试推断高并发正确。

正在加载流程图…

建表不混入普通DML回滚实验;提交结果未知需要额外恢复逻辑,不能简单归入成功或失败。
查看流程图源码(Mermaid)
flowchart TD
 A["独立建表与约束"] --> B["准备确定初态"]
 B --> C["开始事务"]
 C --> D["执行相关DML"]
 D --> E{"业务及语句结果符合要求"}
 E -->|"是"| F["提交"]
 E -->|"否"| G["回滚或错误恢复"]
 F --> H["重新查询最终状态"]
 G --> H

按顺序看,每一步做了什么

  1. 选择隔离练习库,创建InnoDB表并核对约束。
  2. 插入固定初态并保存查询结果。
  3. 在同一连接开始事务,执行有限范围的更新和删除。
  4. 分别回滚与提交,查询并对比最终状态。
  5. 测试重复业务键及条件更新影响行数,记录失败边界。

03对照实现代码

Connector/J的提交请求与结果未知 (新标签页)↗

MySQL Connector/J9.0.0 · ConnectionImpl#commit

原创Java客户端状态模型,非源码节选。它表示提交请求和确认边界,MySQL服务端事务由其自身引擎实现,不能把这个类称为数据库内核。

核心机制代码 · java
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。客户端没有收到确认,不等于数据库没有提交;可靠业务需要业务键和状态查询处理不确定性。

代码先调用谁,再调用谁

  1. 业务事务管理器或JDBC调用commit
  2. ConnectionImpl检查连接和模式
  3. session.execSQL发送COMMIT
  4. 接收结果或通信异常
  5. 应用处理已知结果与未知状态

04动手写一个例子

复制工作状态后一次发布的批量转账

在单线程内存账户表中顺序执行一批正金额转账。账户必须存在、双方不同、余额不能为负且目标余额不能溢出;任一步失败,整个批次不改变原状态。算法只演示原子发布与不变量。

输入 1
A=100、B=0,转账A→B30
输出 1
A=70、B=30
输入 2
先转30再转100,初态A=100
输出 2
整批失败,仍A=100、B=0

先复制账户表形成工作状态,后续每步都在副本中验证和修改。只有全部成功才返回新快照;失败时原表从未改变,达到单线程模型中的整体撤销效果。

一次转账先检查两账户、正金额和余额,再计算目标新余额,保证溢出发生在修改前。金额使用最小货币单位的整数,避免浮点误差。

每一步源账户减少量与目标增加量相同,因此总额不变;同时余额非负。在真实数据库中需要条件更新、事务与并发控制,这个复制模型不具备多线程提交冲突检测。

为什么这样写是对的每个已完成转账保持账户总额不变且余额非负;批次失败时调用方原始映射保持不变。

时间 均摊O(A+T)额外空间 O(A)

A为账户数,复制一次;T笔转账每笔常数次哈希查询与更新,最后生成不可变快照。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 49 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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 更新边界

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01设计评审

    为课程、学生和选课三张表选择列类型及主外键,业务要求同一学生同一课程只选一次,课程删除时须先决定历史记录政策。

    提交内容
    建表草案和约束对应表。
    怎样算完成
    • 数据类型依据取值范围
    • 唯一性与引用关系明确

    练到的知识点表与列类型 · 主键非空唯一 · 外键与级联规则

  2. 作业 02编程实践

    在独立练习库编写新增课程、按 ID 查询、只更新指定学生选课状态和删除指定试验记录的 SQL;每次修改前后保留受影响行核验。

    提交内容
    SQL 文件、自写零行/单行/重复输入测试及执行证据。
    怎样算完成
    • 写操作边界可验证
    • 不修改其他库或未知记录

    练到的知识点INSERT/SELECT/UPDATE/DELETE · WHERE 更新边界

综合应用

  1. 作业 03排错实验

    用两个本地连接观察同一条练习记录:连接 A 显式开始事务并修改,分别选择提交与回滚;连接 B 的读事务设置必须记录。比较每个阶段的查询结果。

    提交内容
    会话脚本、时间线、自写检查及实际查询记录。
    怎样算完成
    • 观察前提包含隔离级别和会话状态
    • 不把客户端显示当作所有会话可见

    练到的知识点InnoDB 事务 · autocommit/commit/rollback

  2. 作业 04面试场景

    迁移脚本在 DML 后执行 ALTER TABLE,再发生失败并尝试回滚。制定隔离环境中的验证方案,列出恢复策略需要了解的事实。

    提交内容
    语句时间线、版本前提与恢复验证清单。
    怎样算完成
    • 考虑 DDL 的提交边界
    • 恢复计划不依赖未经测试的假设

    练到的知识点DDL 隐式提交

  3. 作业 05原理问答

    数据导入程序同时做应用层重复检查和数据库约束检查。用两个并发导入同一选课关系的时间线说明各层检查应承担的职责,并定义冲突反馈。

    提交内容
    并发场景图和错误合同。
    怎样算完成
    • 约束与友好提示分工明确
    • 失败后业务状态可核验

    练到的知识点主键非空唯一 · 外键与级联规则 · WHERE 更新边界 · InnoDB 事务

  4. 作业 06编程实践

    建立题目专用签到表,学生与日期组成唯一业务关系;批量插入含重复记录的数据,逐次记录受影响行及失败政策,再按精确条件修正一条记录。

    提交内容
    隔离 SQL、自写重复/不存在/单行更新检查与运行证据。
    怎样算完成
    • 唯一性与主键职责分别说明
    • 写入影响范围逐次核验

    练到的知识点主键非空唯一 · WHERE 更新边界

  5. 作业 07设计评审

    学生身份证明尚未齐全但允许先保存草稿,正式提交后编号必须非空且唯一。设计草稿与正式记录的约束边界,说明允许与禁止的状态。

    提交内容
    状态/字段矩阵与建表草案。
    怎样算完成
    • 空值语义和业务阶段一致
    • 约束可由代表样本验证

    练到的知识点主键非空唯一

  6. 作业 08面试场景

    更新语句原本按 id 定位,改为按姓名定位后同名数据出现。建立上线前检查,比较受影响行数、唯一性前提和失败后的恢复证据。

    提交内容
    查询与写入验证清单。
    怎样算完成
    • 业务标识与展示字段分开
    • 不靠单条历史样本推断唯一

    练到的知识点WHERE 更新边界

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    完成数据库迁移基础后:新增必填列时已有历史行且服务不能长时间停机。设计分阶段变更、兼容读写和回滚验证,不要求执行生产迁移。

    提交内容
    变更阶段表、失败窗口与演练条件。
    怎样算完成
    • 历史数据处理有合同
    • 代码与结构版本兼容可验证

    练到的知识点外键与级联规则 · InnoDB 事务 · autocommit/commit/rollback · DDL 隐式提交

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    单学校系统增加多学校租户,学生编号只在校内唯一。重新审查表约束与更新条件,并构造旧 SQL 在新数据模型下的检查案例。

    提交内容
    新旧键模型、两学校样本和验收合同。
    怎样算完成
    • 身份范围变化被完整传播
    • 读写条件与唯一约束一致

    练到的知识点主键非空唯一 · WHERE 更新边界

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗