2. 基础概念
在深入多人审批并发冲突之前,必须将数据库领域的三大基础概念——事务、锁、隔离级别——映射到审批业务场景中。这些概念并非纯技术术语,而是描述“谁在什么时候看到什么数据、能否修改、修改后如何生效”的规则集合。
2.1 事务:审批操作的原子边界
事务(Transaction)是一组不可分割的操作单元。在审批流中,一个事务通常对应一次“审批动作”的完整生命周期。
| 事务特性(ACID) | 审批场景映射 | 违反后果示例 |
|---|---|---|
| 原子性(Atomicity) | 审批人点击“通过”后,状态更新、日志写入、通知发送要么全部成功,要么全部回滚 | 状态已改为“已通过”,但日志未记录,后续审计无法追溯 |
| 一致性(Consistency) | 审批完成后,单据状态、审批意见、下一节点责任人必须符合业务规则 | 多人同时审批导致状态变为“已拒绝”但仍有后续节点在流转 |
| 隔离性(Isolation) | 一个审批人的操作在提交前,不应被其他审批人看到中间状态 | A审批中看到B已通过,但B的事务实际已回滚 |
| 持久性(Durability) | 审批结果一旦提交,即使系统崩溃,重启后结果不丢失 | 审批通过后宕机,恢复后单据回到待审批状态 |
关键认知: 在多人审批场景中,事务的隔离性是最常被突破的防线。默认的数据库事务隔离级别(如 Read Committed)在并发审批时会产生“不可重复读”和“幻读”,直接导致审批结果错乱。
2.2 锁:控制并发访问的机制
锁(Lock)是数据库和业务系统用来防止多个事务同时修改同一数据的机制。在审批流中,锁的粒度直接影响并发能力和数据正确性。
2.2.1 锁的类型与审批映射
| 锁类型 | 数据库实现 | 审批场景映射 | 适用条件 |
|---|---|---|---|
| 共享锁(S锁) | SELECT ... LOCK IN SHARE MODE | 审批人读取单据信息,允许其他人同时读取,但不允许修改 | 仅查看审批详情,不执行审批操作 |
| 排他锁(X锁) | SELECT ... FOR UPDATE | 审批人点击“通过/拒绝”时,锁定整条单据记录,阻止其他审批人同时操作 | 执行审批动作,需要保证唯一决策 |
| 行锁 | InnoDB 行级锁 | 只锁定当前审批单据行,其他单据的审批不受影响 | 高并发场景,不同单据可并行审批 |
| 表锁 | LOCK TABLES | 锁定整个审批记录表,所有审批操作串行化 | 极低并发或维护期,不推荐生产使用 |
| 乐观锁 | 版本号(version)或时间戳 | 审批提交时检查版本号是否变化,若变化则重试或提示冲突 | 读多写少、冲突概率低的审批场景 |
| 悲观锁 | SELECT ... FOR UPDATE | 审批人打开审批页面时即锁定记录,直到提交或取消 | 冲突概率高、业务不允许重试的场景 |
常见陷阱: 许多系统在“会签”场景(多人必须全部通过)中错误地使用乐观锁。当最后一人提交时,版本号可能已被前一人修改,导致提交失败。此时应使用悲观锁或分布式锁来保证决策的原子性。
2.2.2 锁的粒度选择策略
- 按单据粒度: 锁定整张审批单据(推荐)。适用于大多数审批流,锁开销适中,冲突范围可控。
- 按节点粒度: 锁定当前审批节点记录。适用于复杂多路分支审批,但需要额外维护节点锁状态。
- 按字段粒度: 仅锁定“审批状态”字段。极少使用,因为业务上通常需要保证整条记录的一致性。
2.3 隔离级别:事务间的可见性规则
隔离级别定义了事务之间数据可见的程度。在审批场景中,不同的隔离级别会导致截然不同的并发行为。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 审批场景表现 |
|---|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 | 审批人A能看到B未提交的“已通过”状态,B回滚后A看到错误数据 |
| Read Committed | 避免 | 可能 | 可能 | A读取单据时状态为“待审批”,B提交后A再次读取变为“已通过”,导致A基于过期数据做决策 |
| Repeatable Read | 避免 | 避免 | 可能(InnoDB通过间隙锁避免) | A在事务内多次读取同一单据,结果始终一致;但可能看到新增的审批记录(幻读) |
| Serializable | 避免 | 避免 | 避免 | 所有审批操作串行执行,无并发冲突,但吞吐量极低 |
推荐配置: 审批流核心操作(状态变更、决策提交)应使用 Repeatable Read 隔离级别,配合行级排他锁(SELECT ... FOR UPDATE)。查询操作(查看审批列表、历史记录)可使用 Read Committed 提升并发性能。
2.4 概念映射关系图
下图展示了事务、锁、隔离级别在多人审批场景中的协同关系:
2.5 实际代码示例:事务+锁的审批提交
以下伪代码展示了在 Repeatable Read 隔离级别下,使用排他锁实现安全的审批提交:
-- 开启事务
START TRANSACTION;
-- 设置隔离级别为 Repeatable Read
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 使用排他锁锁定当前审批单据
SELECT * FROM approval_record
WHERE id = 1001
FOR UPDATE;
-- 检查当前状态是否允许审批
-- 若状态已变更(被其他事务修改),则回滚
IF (status != 'PENDING') THEN
ROLLBACK;
RETURN '审批状态已变更,请刷新后重试';
END IF;
-- 执行审批操作:更新状态、写入日志
UPDATE approval_record
SET status = 'APPROVED', approver = 'user_A', approved_at = NOW()
WHERE id = 1001;
INSERT INTO approval_log (record_id, action, operator, created_at)
VALUES (1001, 'APPROVE', 'user_A', NOW());
-- 提交事务
COMMIT;
关键点:
SELECT ... FOR UPDATE 会在读取时立即加排他锁,其他事务的 FOR UPDATE 或 UPDATE 操作会等待当前事务提交或回滚。这保证了在多人同时点击“通过”时,只有一个事务能成功修改状态。
2.6 概念对比总结
| 维度 | 事务 | 锁 | 隔离级别 |
|---|---|---|---|
| 核心问题 | 操作是否完整执行 | 谁可以修改数据 | 事务间数据可见性 |
| 审批映射 | 一次审批动作的成败 | 防止多人同时修改同一单据 | 审批人看到的数据版本 |
| 并发影响 | 事务越长,锁持有越久 | 锁粒度越细,并发越高 | 级别越高,并发越低 |
| 常见错误 | 事务内包含无关操作 | 未加锁导致覆盖更新 | 使用 Read Committed 导致不可重复读 |
| 推荐实践 | 审批动作事务尽量短小 | 行级排他锁 + 乐观锁兜底 | 核心操作使用 Repeatable Read |