抽象场景描述
在实际业务系统中,我们经常遇到同一条数据记录被多个角色、多个客户端并发操作的情况。典型如“内容审核”、“任务状态更新”、“订单流转”等场景。
本案例抽象为以下数据模型:
id | user_id | word | review_status | review_opinion | review_user_id
这张表用于记录用户提交的内容(word),由后台审核人员进行审核处理,审核状态存储在 review_status 字段,审核意见写入 review_opinion,而 review_user_id 表示执行审核操作的管理员 ID。
很多时候可能都没有 review_user_id 这个内容,但是为了严谨和安全这个审核人也是需要加上的。
在实际业务中,存在以下典型角色:
- 普通用户:提交或修改 word 内容,触发审核流程。
- 审核人员(运营):基于 review_status 审核用户提交内容,可能打回或通过。
- 系统服务或定时任务:自动更改 review_status,例如长时间未审核的内容自动打回。
🎯 具体场景举例
- 用户 A 提交文案 word,此时 review_status = 0(待审核)。
- 同时运营 B 正在审核该内容,正准备将状态置为 2(已通过)。
- 此时,用户 A 发现错误,在运营未审核前修改了内容,review_status 被用户接口自动回退成 0(待审核),运营端页面未刷新,仍提交 2(已通过)。
- 结果:状态冲突,最终保存结果不一致,导致运营看到通过,用户看到未通过,系统状态混乱。
状态流转:
[至少有 5 个内容,但是 0和 1 可以在某些场景 共用】
| review_status | 状态含义 | 备注说明 | | — | — | — | | -1 | 草稿 | 用户刚填写内容,尚未提交审核 | | 0 | 待审核 | 用户点击“提交审核”按钮后进入审核流程 | | 1 | 审核中(可选) | 审核人员点进详情页或锁定审核任务 | | 2 | 审核通过 | 内容通过 | | 3 | 审核不通过 / 打回 | 审核失败,需要用户修改后重新提交 |
- 用户 A 编辑文案 word,初始为 review_status = -1(草稿)
- 用户点击“提交审核”,状态改为 0(待审核)
- 运营 B 开始审核,准备提交为 2(通过)
- 用户又修改文案内容,系统逻辑应自动将状态重置为 -1(草稿),并中断审核流程或提示运营重新刷新内容
- 否则就可能出现状态混乱问题
加乐观锁
- 数据量不大,不适合加悲观锁(行锁或表锁开销太高)
- 用户 A 和运营 B 可能通过不同接口/前端并发操作一行数据
- 如果没有控制机制,会导致运营审核通过的记录被用户刷新覆盖
核心思想:在写入前先检查版本号 / 修改时间 /状态是否被其他人改动过
加一个字段:
version INT DEFAULT 1
✅ 如果版本号一致,说明没人改过,更新成功;
❌ 如果版本号不一致,说明被别人改过了,更新失败 → 返回冲突提示。
UPDATE xxx
SET word = '新的内容', review_status = 0, version = version + 1
WHERE id = 123 AND version = 当前版本号;
但是“前端传来的 version 不可信,如何在多角色多端并发场景下保证状态一致性?”
1、服务端查询 version,不信任前端传参
用户提交内容时:
- 不接受用户传来的 version
- 服务端查询当前 version 并进行判断逻辑:
# 查询数据库中最新 version
row = db.query("SELECT version, review_status FROM your_table WHERE id = 123")
# 如果 review_status != -1(草稿),说明不允许用户编辑
# 如果 review_status == -1,则执行 UPDATE ... WHERE version = row.version
然后执行:
UPDATE your_table
SET word = '新内容', review_status = 0, version = version + 1
WHERE id = 123 AND version = 数据库中读出的 version;
2、UUID版乐观锁
比较项 | 整型 version | UUID version |
---|---|---|
可预测性 | 可预测(+1) | 不可预测,难以伪造 |
冲突检测 | 依赖数据库 current version | 更安全,防止前端伪造 |
安全性 | 可能被篡改 | 前端无法猜测 |
多端并发控制 | 依赖统一接口流程 | 可天然支持多端(用户、运营、系统) |
可扩展性 | 固定递增 | 可嵌入修改来源、时间戳等元信息 |
✅ UUID 版本号方案设计(建议用字段version_uuid)
用户提交时(草稿 → 待审核):
- 服务端查询当前版本 UUID 是否匹配
- 若匹配,生成新的 UUID 作为新版本
current = db.query("SELECT version_uuid FROM your_table WHERE id = 123")
if client_version_uuid != current.version_uuid:
raise Exception("内容已变更,请刷新后重试")
new_uuid = str(uuid4())
UPDATE your_table
SET word = ..., review_status = 0, version_uuid = :new_uuid
WHERE id = 123 AND version_uuid = :client_version_uuid
运营审核时:
- 运营提交时也要附带 version_uuid
- 服务端判断 version_uuid 是否匹配
- 否则提示“用户已修改内容,请刷新页面”
风险场景 | 是否规避 | 说明 |
---|---|---|
用户刷旧页面覆盖运营审核 | ✅ | UUID 不匹配无法更新 |
多人同时编辑,最后一人覆盖前者结果 | ✅ | UUID 控制写入成功与否 |
恶意构造 version 参数尝试绕过审核 | ✅ | UUID 无法预测或伪造 |
多端(Web/APP/运营后台)并发操作冲突 | ✅ | UUID 自然解耦所有端 |
“用 UUID 来代替整型 version,作为乐观锁的唯一标识”,既解决了并发一致性,又规避了版本信息泄露问题。
审核状态限制
✅ 只允许在草稿状态(review_status = -1)下编辑,审核中/待审核/已通过等状态下禁止修改。
-
天然避免并发写冲突
如果一旦用户提交审核(状态为 0 或 1),就锁定该条数据的编辑权限,用户端无法再提交修改,自然避免了和运营端审核产生的数据冲突。
-
业务语义清晰
审核中、待审核、已通过的内容,按理都应视为“已提交作品”,不应再被用户随意改动,符合大多数业务流程对稳定性的要求。
-
简化乐观锁处理逻辑
只需要处理少数“草稿”状态下的编辑,不再需要对每次修改都进行复杂的版本校验或冲突回滚。
if review_status != -1:
raise Exception("内容已提交,无法修改")
✅ 最终好处
- 提高稳定性:避免运营、用户之间“互相覆盖”问题。
- 减少冲突处理成本:不用频繁处理版本对比、状态回滚。
- 提升用户体验:流程清晰,操作有反馈,错误可避免。
使用哈希防止误触
通过对 user_id + word 做 Hash 判断是否内容变化
✅ 目标问题
- 审核失败(review_status = 3)后允许用户再次修改并提交。
- 但用户未做任何修改,仅误点击“提交”按钮,不应触发审核流程。
- 或者内容重复,系统也不应发起无意义的审核请求。
content_hash VARCHAR(64) COMMENT '内容哈希值,用于识别是否变更'
提交时:
- 服务端生成新的 content_hash = sha256(user_id + word)。
- 查询数据库中现有的 content_hash。
- 对比:
- 一致:说明没有修改 → 拒绝发起审核。
- 不一致:说明内容变化 → 执行提交审核流程。
优点 | 说明 |
---|---|
✅ 防止误提交 | 用户内容没变,误点击也不会发起审核流程 |
✅ 降低系统负担 | 重复审核流程被拦截,减少系统流量 |
✅ 语义清晰 | 用内容 Hash 判断是否“确实修改” |
✅ 安全性强 | 不依赖前端判断是否变更 |
✅ 扩展性强 | 后续可加入内容版本控制(记录历史 hash) |
按职责拆表设计
❗问题本质
- 用户和运营在同一张表修改同一行数据。
- 容易产生并发冲突、双写问题、状态不一致。
- 如果用乐观锁,还得追踪 version / update_time,逻辑复杂。
表 A:用户提交表(user_word_submit)
字段名 | 说明 |
---|---|
id | 主键 |
user_id | 用户 ID |
word | 用户输入的内容 |
submit_time | 用户提交时间 |
content_hash | 内容 hash,避免重复提交 |
status | 草稿 / 已提交 |
表 B:审核状态表(word_review_status)
字段名 | 说明 |
---|---|
id | 主键 |
submit_id | 对应 user_word_submit.id |
review_status | 审核状态(0-3等) |
review_user_id | 审核人员 ID |
review_opinion | 审核意见 |
version_uuid | 乐观锁控制字段(UUID) |
update_time | 最后审核修改时间 |
优点 | 说明 |
---|---|
✅ 物理隔离 | 用户只能写 submit 表,运营只能写审核表,不会互相覆盖 |
✅ 写入更安全 | 业务逻辑天然分离,审计也更清晰 |
✅ 并发更友好 | 多端操作无双写,update_time 不冲突 |
✅ 便于流程解耦 | 审核逻辑变更不会影响用户内容逻辑 |
方案 | 优点 | 缺点 |
---|---|---|
不拆表(合一) | 结构简单、字段集中 | 双写问题严重,乐观锁逻辑复杂 |
拆表(推荐) | 多角色写分离,逻辑更清晰稳定 | 表设计略复杂,需要 JOIN 等操作 |
• 多角色多端同时写入同一行数据的并发问题,最合适的办法是按职责拆表。