打开项目笔记时“爱不是温存是嵌入骨髓的一根钉……”这句话莫名停在我眼前。如果把它放到后端开发语境里我脑海里第一反应不是写情感文而是数据库里那些最容易被当成“没事找事”、却在关键时刻决定数据能否干净的那群东西——约束。很多团队在起步阶段为了速度尽量不在表上加约束所有校验放在应用层理由是“改起来灵活不影响性能”。起初看着确实温存接口好写、代码直观、上线速度快。可等并发上来重复订单、脏关联、空账号、负数金额接二连三地冒出来线上问题就像一根钉子一样嵌进系统里越早无视它越早吃苦头。这篇文章我不写抽象口号而是从数据库约束视角完整复盘约束到底解决什么问题、每类约束背后是什么机制、如何用唯一约束设计一个可靠幂等接口以及生产环境中加约束、改约束时真正要避开的坑。适合刚从 CRUD 起步的初学者也适合正在做订单、会员、支付类业务经常被重复数据和脏数据困扰的后端工程师。1. 从一句略带情绪的话聊到数据库约束1.1 最温柔的错觉应用层校验就够了很多开发新手写接口时第一反应是“插入前先查一下不存在再 insert 进去”觉得这样已经万无一失。比如用户注册// 伪代码 if (userMapper.getByUsername(username) null) { userMapper.insert(user); }这样的代码确实在正常访问时有效但一旦两个注册请求几乎同时到达两个线程都执行了“查询”这一步发现数据库里没有该用户名随后都进入 insert就会插入两条相同用户名的记录。应用层的校验像一种温存的模糊承诺它没有硬性兜底天然存在“先查后写”的并发窗口。这也是我后来对“数据库约束”态度变化的原因。约束不是开发效率的阻碍而是数据正确性最后的防线。它不是锦上添花而是“嵌入骨髓的钉子”建立表结构的瞬间约束就与这张表的命运绑定决定这张表在长时间运行中是否会被脏数据腐蚀。1.2 约束在系统里的真实位置从整体系统看数据正确性有多层保障层级手段特点前端表单校验、按钮置灰用户体验好但可绕过后端接口参数校验、业务判断能挡大部分错误但存在并发窗口数据库约束、事务、锁兜底保证最终数据不出错越是靠近底层的机制越不“温柔”它不会照顾调用方的情绪只会按规则执行。数据库约束尤其如此。比如唯一索引遇到重复写入会直接报 1062 错误而不是说“你能不能换个用户名”外键检查失败会直接抛错并回滚不会给你打感情牌。但数据最终是可靠的。1.3 约束全景每根钉都钉在特定位置约束不是一种技术而是一组互补规则。通常把约束分成主键约束、唯一约束、非空约束、默认值约束、检查约束、外键约束再配合索引和事务一起使用。下表是总览约束类型核心作用典型报错/现象PRIMARY KEY唯一标识一行插入相同主键报 1062UNIQUE某列或联合列值唯一插入重复值报 1062NOT NULL列不允许为空插入 NULL 报 1048DEFAULT未传值时使用默认值不会报错自动填充CHECK列值满足条件不满足报 3819FOREIGN KEY维护表与表引用关系无对应父行报 1452后面会逐类展开。这里先建立一个观念约束本质是规则声明是把业务正确性下沉到数据引擎层让应用层更轻、数据更稳。2. 实验环境与示例表设计2.1 环境准备本文 SQL 基于 MySQL 8.x 编写使用 InnoDB 引擎。如果你使用其他版本比如 5.7大部分示例仍然通用涉及 CHECK 约束时需要注意版本差异8.0.16 之前 MySQL 并不强制执行 CHECK这一点后面会单独说明。建议的本地环境工具用途MySQL 8.x数据库服务Navicat / DBeaver / MySQL CLI执行 SQLJDK 8可选运行 Java 示例如果用 Docker 快速起一个 MySQL 实例可以执行docker run --name mysql-demo \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEdemo_db \ -p 3306:3306 \ -d mysql:8.0进入容器docker exec -it mysql-demo mysql -uroot -p123456 demo_db日常开发学习中一条命令就能拥有可反复实验的数据库环境。真实项目里请按照团队标准设置密码和权限不要把生产账号直接拿来练习。2.2 本文示例场景后面大量示例围绕一个极简业务展开一个用户表和一个订单表。这种结构在电商、内容付费、约单类业务中都很常见。CREATE DATABASE IF NOT EXISTS demo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE demo_db; CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, phone VARCHAR(20), age INT, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id INT NOT NULL, amount DECIMAL(12, 2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里先故意不写业务唯一键和外键方便后面演示不加约束时数据能插入多少离谱内容加上约束后数据库如何拦截错误。3. 每一种约束都是一颗钉3.1 主键约束最基础的一根钉主键是每张表最重要的钉子。它要求这一列的值既不能为 NULL也不能重复用来唯一确定一行记录。通常我们习惯使用自增整数主键CREATE TABLE t_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB;如果在同一张表里插入两个 id 相同的数据会得到如下错误ERROR 1062 (23000): Duplicate entry 1 for key t_user.PRIMARY这一行报错信息很典型“Duplicate entry”代表要写入的值和已有主键冲突。主键不仅保证唯一性在 InnoDB 中它还和聚簇索引绑定直接影响查询性能和写入顺序。这里有一个容易被忽视的问题主键选择并非越短越好也并非一定要自增。比如用户系统里可以用手机号做天然业务主键但手机号一旦允许变更主键变更会引起非常麻烦的连锁更新。因此更稳妥的设计是“独立 id 业务唯一键”两条路线分开走id 只是内部标识username、order_no 这类业务字段单独加唯一约束。这么做可以让主键稳定、单一、无业务含义而业务唯一性由唯一约束负责。3.2 唯一约束防止重复业务数据如果说主键是身份唯一唯一约束则负责“业务唯一”。例如用户注册时用户名不能重复ALTER TABLE users ADD CONSTRAINT uk_username UNIQUE KEY (username);这时再插入重复用户名INSERT INTO users (username, phone, age) VALUES (张三, 13800000001, 20); INSERT INTO users (username, phone, age) VALUES (张三, 13800000002, 21);第二条语句会失败。因为数据库已经存在 username 为“张三”的记录唯一约束直接拒绝写入。另一个高价值场景是联合唯一约束。比如一张收藏表用户对同一篇文章只能收藏一次就需要把 user_id 和 article_id 联合起来做唯一CREATE TABLE user_favorite ( user_id BIGINT NOT NULL, article_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_article (user_id, article_id) ) ENGINEInnoDB;联合唯一约束的语义是“组合值不允许重复”而不是“单个字段不允许重复”。也就是说 user_id 或 article_id 允许重复但只要同一对 user 与 article 重复数据库就会阻止。很多“先查询再插入”的缺陷用唯一约束就能解决。查询判断会并发穿透唯一约束却是在数据库引擎内部原子判断不存在“两个线程都以为不存在”的空子。3.3 非空约束拒绝隐形的 NULLNULL 是一种特殊状态它表示“未填写”“未知”并不是空字符串。很多人写表结构时不加 NOT NULL让字段默认允许 NULL表面看“很自由”实际上很容易污染数据。比如用户表里 phone 允许 NULL后续报表统计写SELECT COUNT(*) FROM users WHERE phone ! 13800000000;直觉上这句 SQL 想查出“手机号不是某个值”的用户数但结果会漏掉 phone 为 NULL 的记录。因为在 SQL 三值逻辑中NULL 与任何值比较的结果都是 NULL条件不会将 NULL 记录保留。非空约束能在源头避免这种问题。如果不希望某个业务字段为空就直接声明CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, phone VARCHAR(20) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB;之后再插入缺 phone 的数据INSERT INTO users (username, phone) VALUES (李四, NULL);会得到ERROR 1048 (23000): Column phone cannot be null设计表结构时要给每个字段认真问一句我真的允许这个字段缺失吗多数情况下手机号、状态、金额、创建时间都不应该为空。真正允许为空的字段往往只有备注、扩展信息等少数几类而且代码里要做好空值处理。3.4 DEFAULT 约束温和但重要的补位DEFAULT 不算“刺人”的约束它更像是默认安抚机制当插入语句没有指定该列的值时数据库自动写入默认值。它和 NOT NULL 经常配合使用。CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB;插入时只提供 order_nostatus 和 created_at 会自动填充INSERT INTO orders (order_no) VALUES (202501010000001); SELECT order_no, status, created_at FROM orders;查询结果类似--------------------------------------------- | order_no | status | created_at | --------------------------------------------- | 202501010000001| 0 | 2025-01-01 10:00:00 | ---------------------------------------------使用 DEFAULT 要注意一个常见误解插入 NULL 并不会触发默认值除非字段也声明了 NOT NULL。区别如下INSERT INTO users (username, phone, age) VALUES (王五, 13900000000, NULL);如果 age 列只有 DEFAULT 而没有 NOT NULL这条 SQL 会成功age 保存为 NULL而不是默认值。想要“没传值就自动用默认值”通常要靠代码层不传入该字段或者把列设置为 NOT NULL DEFAULT。3.5 CHECK 约束给字段值画一条合法边界CHECK 约束用于限制字段的值域比如年龄不能为负、金额必须大于 0、状态只能用 1/2/3。它表达的是一种业务规则而不是数据库生成的随机约束。在 MySQL 8.0.16 及以上版本中CHECK 约束会被强制执行。建表示例CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, age INT NOT NULL, PRIMARY KEY (id), CONSTRAINT chk_users_age CHECK (age 0 AND age 120) ) ENGINEInnoDB;插入非法年龄INSERT INTO users (username, age) VALUES (赵六, -1);会得到类似ERROR 3819 (HY000): Check constraint chk_users_age is violated.如果你维护的是 MySQL 8.0.16 之前的版本CHECK 会被解析但不会真正生效这时只能退而使用应用层校验或通过触发器模拟。因此读到这篇文章时先确认你的数据库版本别把“语法能执行”误认为“约束生效了”。3.6 外键约束跨表关系里的锚钉外键是我心中最像“嵌入骨髓的钉”的一种约束。它让两张表之间形成引用关系例如订单表里的 user_id 必须真实存在于用户表否则不允许创建订单。CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id INT NOT NULL, amount DECIMAL(12, 2) NOT NULL, PRIMARY KEY (id), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINEInnoDB;如果插入一条 user_id 为 9999 的订单而 users 表没有这个 id会得到ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails删除父表记录时也可能被拦截ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails外键带来的最大价值是让数据库保证引用完整性代码写错时不是报业务异常而是让错误尽早暴露。最大的代价则是插入、更新、删除时的额外检查开销以及在高并发大流量系统里跨表外键可能让锁的粒度变大、更容易产生锁等待。所以业界的做法很分裂传统企业级项目、后台管理系统放心使用外键高并发互联网系统更倾向于在应用层维护关系因为业务拆分后订单服务和用户服务可能连数据库都不一样了物理外键根本加不上。这里不建议盲目跟风需要先理解自己系统的拆分程度和一致性要求。4. 约束与事务钉子也需要装甲4.1 约束检查发生在哪一刻很多人误以为约束是在事务提交时才统一校验实际上 InnoDB 在语句执行阶段就会检查约束。例如在一个事务内插入两条冲突的用户名BEGIN; INSERT INTO users (username, phone) VALUES (张三, 13700000000); INSERT INTO users (username, phone) VALUES (张三, 13600000000); COMMIT;第二条语句就会立刻报重复错误而不是等 COMMIT。遇到约束冲突时单条语句失败事务本身还可以继续处理或回滚。如果想保证一批数据要么全部成功、要么全部失败需要显式使用事务BEGIN; INSERT INTO users (username, phone) VALUES (张三, 13700000000); UPDATE accounts SET balance balance - 100 WHERE user_id 1; COMMIT;这里的约束不是事务的替代品两者是配合关系事务负责原子性约束负责规则正确性。没有约束的事务只是保证“要么全做、要么全不做”但无法保证“做的是对的”。4.2 唯一约束与高并发写入当多个事务并发写入同一条唯一键时通常只有一个会成功。比如用户同时提交相同手机号注册InnoDB 会通过唯一索引上的记录锁和插入意向锁实现互斥。实际表现是其中一个事务先拿到锁完成插入另一个事务等待后继续执行发现唯一键已经存在然后报 1062。在应用层处理这种冲突一般有两种思路第一捕获数据库唯一键错误把失败请求重新转为“已存在”的返回第二先重试一次如果还是失败再执行更新或查询。如下是常见处理函数画法public void register(String username, String phone) { try { userMapper.insert(username, phone); } catch (DuplicateKeyException e) { User user userMapper.findByUsername(username); if (user null) { throw new BizException(注册失败请重试); } // 返回已存在用户或抛出“用户已存在” } }这里体现的唯一性思想在后面幂等接口部分还会继续展开。5. 实战用唯一约束实现幂等写入5.1 业务背景支付回调重复推送做一个订单/支付类系统时最怕回调接口被重复调用。第三方支付平台为了保证通知到达可能连续推送多次如果回调处理逻辑每次都生成新流水、发放权益用户就会收到多份内容账目也会对不上。解决思路是在业务表上放一个“业务唯一键”让重复请求被数据库天然挡住。5.2 表结构设计假设一张支付回调流水表 payment_notify核心字段如下CREATE TABLE payment_notify ( id BIGINT NOT NULL AUTO_INCREMENT, notify_no VARCHAR(64) NOT NULL COMMENT 回调方提供的唯一流水号, order_no VARCHAR(64) NOT NULL COMMENT 业务订单号, notify_type VARCHAR(32) NOT NULL COMMENT 回调类型PAY / REFUND, payload TEXT NOT NULL COMMENT 原始回调报文, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_notify_no (notify_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里把 notify_no 设为唯一键含义非常明确同一个回调流水号只允许落库一次。5.3 先查询再插入的失败写法很多初版代码是这样写的// 错误示例并发下不幂等 PaymentNotify record notifyMapper.findByNotifyNo(notifyNo); if (record null) { notifyMapper.insert(notifyNo, orderNo, payload); doBizAction(orderNo); }只要两个回调线程同时走到查询两个都发现查不到随后都会执行 insert。在没有唯一约束的旧表结构里表里会出现两条相同 notify_no 的记录业务动作会执行两次这就是严重的幂等漏洞。5.4 借助唯一约束的可靠方案正确做法是不要信“先查后插”直接 insert把重复判断交给数据库// 文件路径src/main/java/com/example/demo/service/NotifyService.java Service public class NotifyService { private final NotifyMapper notifyMapper; private final OrderService orderService; public NotifyService(NotifyMapper notifyMapper, OrderService orderService) { this.notifyMapper notifyMapper; this.orderService orderService; } public void handle(String notifyNo, String orderNo, String payload) { try { notifyMapper.insert(notifyNo, orderNo, payload); } catch (DuplicateKeyException e) { // 重复回调直接忽略不重复处理业务 return; } // 只有第一次插入成功时才执行业务动作 orderService.markPaid(orderNo); } }这段代码的核心思路是唯一约束保证同一 notify_no 只能插入一次谁插入成功谁才处理业务其余重复请求在捕获数据库异常后直接结束。这里的 DuplicateKeyException 在 Spring 框架中通常由底层数据库异常转换而来如果你不使用 Spring则自行捕获 SQLException 并判断错误码 1062。5.5 验证幂等效果执行下面的 SQL 两次INSERT INTO payment_notify (notify_no, order_no, notify_type, payload) VALUES (NOTIFY202501011000001, ORDER202501010000001, PAY, {result:SUCCESS});第一次插入成功Query OK, 1 row affected (0.01 sec)第二次插入会失败ERROR 1062 (23000): Duplicate entry NOTIFY202501011000001 for key payment_notify.uk_notify_no这正好说明幂等落库不是靠应用层小心而是靠数据库约束托底。在线上真实系统里最好还要给 order_no notify_type 加联合唯一约束避免同一种类型重复处理实现“业务唯一”。6. 常见问题与排查思路6.1 高频问题速查表问题现象常见原因解决思路插入数据报 1062 Duplicate entry主键或唯一索引冲突检查写入字段是否重复确认业务场景是否需要捕获后转为幂等成功插入数据报 1048 Column cannot be null违反非空约束补全必填字段或调整表设计让字段可空并给出默认值更新/删除父表记录失败存在外键关联子记录先处理子表数据或修改外键的 ON DELETE 策略CHECK 约束不生效版本低于 MySQL 8.0.16查看版本用触发器实现或升级数据库查询结果中看不到 NULL 记录对 NULL 使用比较运算符用 IS NULL / IS NOT NULL 判断空值并发插入时偶尔死锁多个事务争用同一唯一键和间隙锁减少事务执行时间统一插入顺序必要时 review 索引结构6.2 排查步骤建议遇到约束相关报错不建议直接把错误“吞掉”。按以下顺序排查第一步读完整错误信息。MySQL 的报错通常会告诉你“Duplicate entry ‘xxx’ for key ‘表名.索引名’”先确认重复的是哪个索引。第二步查询现有数据。SELECT * FROM users WHERE username 张三;第三步分析业务含义。如果是重复注册判断应返回“用户已存在”还是允许登录如果是支付回调重复判断应忽略重复事件还是更新已有记录。第四步如果是死锁检查事务内语句顺序是否一致。比如两个事务都先更新 A 再用 B顺序不同就容易互相等待。尽量让所有事务按相同顺序更新资源。第五步报错高频且原因不明时打开 InnoDB 状态信息SHOW ENGINE INNODB STATUS\G查看“LATEST DETECTED DEADLOCK”段落能定位相互等待的 SQL。6.3 关于删除和更新的特别提醒带外键的表删除父行时若被拦截很多人第一反应是直接加 SET FOREIGN_KEY_CHECKS0 绕过检查。这条命令只适合在可控数据迁移时临时使用线上随意关闭外键检查可能产生孤儿数据而且当前会话关闭后要记得恢复。更稳妥的做法是先确认子表数据确实可以删除再设计迁移顺序删除前先备份删除后验证。7. 数据库约束的最佳实践与工程建议7.1 约束要下沉规则要前置在项目初期约束一旦缺失后续补数据、写清洗脚本、做兼容逻辑都会变得非常痛苦。建议把“数据正确性校验”尽量沉淀到数据库应用层负责业务提示和体验控制数据库负责最后防线。例如用户注册时应用层友好提示“用户已存在”数据库层仍然保留 username 唯一索引双保险才能覆盖并发漏洞。7.2 约束命名规范可读性强的约束命名对后期 review 和排错很有帮助。主键通常由系统默认但外键、唯一键、检查键建议显式命名避免在报错里看到一串随机索引名不好定位。约束类型前缀示例主键PK_PK_orders_id唯一约束UK_uk_orders_order_no外键FK_fk_orders_user_id检查约束CK_chk_users_age命名不是强制要求但要做到“见名知意”。团队规范里最好统一方便 DBA 和开发排查线上问题。7.3 唯一键和业务逻辑要明确对应唯一键不是拍脑袋加的。每建一个唯一键都应该能在业务规则里找到出处。比如用户表 username 唯一、订单表 order_no 唯一、支付回调 notify_no 唯一、收藏表 user_id article_id 联合唯一。如果团队成员对“到底是哪个组合唯一”理解不一致很容易搞出错误索引和错误约束。7.4 生产环境变更前先想回滚方案给已有大表添加唯一约束或外键时要特别谨慎。以 MySQL InnoDB 为例添加唯一约束本质是创建唯一索引会扫描表数据、检查重复值并可能造成锁与 IO 开销添加外键也可能对已有数据做一致性校验。因此涉及生产环境的结构变更至少做到在测试环境先模拟大数据量变更记录耗时与锁情况。线上操作尽量安排在低峰期并提前备份数据。准备好回滚脚本——如果本次变更导致性能抖动可以快速删除新增约束。变更前需要评估存量数据是否已经存在脏数据。比如加唯一约束前如果表里已经有两行相同 username那么加约束会直接失败必须先清洗。7.5 不要盲目禁用外键也不要盲目全表外键外键的使用在业界是分场景的。业务系统内部表与表关系稳定、事务边界相对清晰使用外键能显著减少脏关联微服务拆分后订单服务和用户服务不在同一个库外键通常无法使用这时要靠应用层事务、领域模型、最终一致性等措施弥补。最怕的是一个团队既没有物理外键也没有严肃的应用层校验接口里只做简单的增删改查最后出现大量孤单单据。7.6 软删除表与唯一约束的冲突很多系统喜欢用 deleted0 正常 1 删除做软删除并希望在业务字段上加唯一约束。但普通唯一索引不会把已删除记录排除在外再次注册同一个用户名时会插入失败。常见处理方案有几种从逻辑删除改为状态隔离用 username is_deleted 做联合唯一但只能保证“删除状态下也不重复”不能解决多次软删除后的重复问题。使用删除时间戳字段 deleted_at未删除时默认 NULL联合唯一键放在 username deleted_at。MySQL 中多个 NULL 并不互斥因此软删除后填充删除时间同一用户名在已有 NULL 时互斥旧删除记录与其他删除记录之间又不会冲突。示例CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, deleted_at DATETIME NULL, PRIMARY KEY (id), UNIQUE KEY uk_username_deleted (username, deleted_at) ) ENGINEInnoDB;第一次插入 张三 时 deleted_at 为 NULL第二个正常用户无法再插入“张三”。如果张三被删除则把 deleted_at 更新为当前时间因第二条旧记录是 NULL新删除记录和 NULL 不会冲突因此允许再次注册。这样的方案也有边界限制但它展示了一个很典型的设计思路唯一约束需要和业务状态一同设计不是定了就不动。8. 总结与学习路线写到这里我发现那句“爱不是温存是嵌入骨髓的一根钉”其实很适合送给数据库约束。主键在身份边界上钉住每一行记录唯一约束在业务维度上钉死重复数据非空和检查约束在高频入口处拦住脏字段外键则在跨表引用上守住关系完整性。它们看起来冰冷、死板、不讲情面但正是这种不妥协的规则才让系统在千万次并发写入后依然能保持干净。如果你是刚学 SQL 的初学者建议按顺序练透这些能力先会用 CREATE TABLE 设计带约束的表结构再理解约束报错信息的含义然后结合事务写一写“先查后写”为什么有并发漏洞的场景最后把唯一索引做进真实的接口里。如果已经做过一段后端开发下一步值得深入的是 InnoDB 索引结构、锁机制和隔离级别因为约束的很多行为本质上是索引与锁在背后支撑。希望这篇文章能让你对数据库约束有一个更“硬核”的认识。在动手改线上表结构之前多问自己一句这根“钉”钉下去是保护业务还是在给后来挖坑如果你也在项目里遇到过因为缺少约束产生的脏数据问题可以在评论区一起聊聊解决方案。
