两个人同时(或多个并发用户/线程/进程)操作同一个数据库,最核心的问题是可能导致数据不一致、丢失更新、脏读、幻读等异常。
数据库(以及应用层)主要通过并发控制(Concurrency Control) 来解决这个问题。下面从最常见、最实用的角度给你讲清楚怎么处理。
1. 数据库层面的主流解决方案(最推荐优先使用)
| 控制方式 | 核心思路 | 什么时候加锁/检查冲突 | 典型实现方式 | 适用场景 | 优缺点对比 |
|---|---|---|---|---|---|
| 悲观锁 (Pessimistic) | 先假设会冲突,先锁住再说 | 读/写时就加锁 | SELECT … FOR UPDATE 行锁、表锁 | 高冲突场景(如库存扣减、抢票、转账) | 并发低,但安全;可能死锁或等待时间长 |
| 乐观锁 (Optimistic) | 先假设不会冲突,最后提交时再检查 | 只在提交时检查 | 版本号(version) 或时间戳、CAS | 读多写少、低冲突场景 | 并发高,但冲突时需重试;适合大部分业务 |
| MVCC 多版本并发控制 | 每条记录保留多个历史版本,读不阻塞写 | 读写都不锁(或轻锁) | InnoDB 的 undo log + 隐藏列(trx_id, roll_ptr) | 几乎所有现代 OLTP 数据库(MySQL InnoDB、PostgreSQL、Oracle 等) | 读写并发最高;空间换时间,需定期清理旧版本 |
结论:2025–2026 年主流做法
- 大部分业务首选 MVCC + 乐观锁 的组合(读不阻塞写,写冲突靠版本号解决)
- 高冲突、强一致性场景(如秒杀、账户余额)再加 悲观锁 或 分布式锁
2. 经典例子:库存扣减(两个人同时买最后一个商品)
场景:商品库存 = 1,A 和 B 同时下单。
方式1:悲观锁(数据库行锁)
-- 事务A 先执行
BEGIN;
SELECT stock FROM goods WHERE id = 1 FOR UPDATE; -- 拿到行锁,stock=1
UPDATE goods SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- 事务B 同时执行,会在 FOR UPDATE 这里阻塞等待,直到A提交后才继续
-- 如果超时或死锁,可能回滚
优点:简单、安全,不会超卖
缺点:并发上来锁等待严重,吞吐下降
方式2:乐观锁(版本号 / CAS)
-- 事务A
SELECT stock, version FROM goods WHERE id = 1; -- 读到 stock=1, version=10
-- 业务逻辑...
UPDATE goods
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 10; -- 如果 version 还是10才更新成功
-- 事务B 同时读到 version=10
-- 但A先提交,version变成11
-- B的 UPDATE 影响行数=0 → 说明冲突,重试或提示“库存不足”
优点:高并发时不阻塞读,性能好
缺点:冲突时需要业务重试(通常重试3–5次)
方式3:真正的库存安全写法(推荐组合)
-- 方式3:乐观锁 + 行级条件更新(最常用)
UPDATE goods
SET stock = stock - 1
WHERE id = 1 AND stock > 0; -- 数据库原子判断
-- 然后检查 affected rows
-- 如果 == 1 → 扣减成功
-- 如果 == 0 → 库存不足,失败
或者用 SELECT … FOR UPDATE + 乐观锁 双保险(高并发下仍可能有性能瓶颈)。
3. 应用层辅助手段(数据库锁不够用时)
当单机数据库锁粒度不够、或分布式系统时,常用这些:
| 手段 | 适用场景 | 实现工具/方式 | 备注 |
|---|---|---|---|
| 分布式锁 | 分布式系统、微服务、多库 | Redis(Redlock)、ZooKeeper、etcd | 强一致,但引入外部依赖 |
| 消息队列削峰 | 高并发场景先排队 | Kafka / RabbitMQ 顺序消费 | 最终一致性,延迟稍高 |
| 幂等性 + 防重表 | 防止重复扣款/重复下单 | 唯一索引 / 业务唯一键 + 防重表 | 必须配合幂等设计 |
| 事务补偿 / Saga | 分布式事务 | Seata、DTM、Saga 模式 | 最终一致性,适合长事务 |
4. 快速决策表(根据你的业务选)
| 你的业务场景 | 推荐优先级顺序 |
|---|---|
| 库存、余额、积分、抢票 | 1. UPDATE + 条件判断 2. 乐观锁 version 3. 悲观锁 FOR UPDATE |
| 普通表单修改(文章、配置) | MVCC + 乐观锁(版本号) |
| 读多写少(浏览量、点赞) | 纯乐观锁 或 延迟写(Redis + 定时同步) |
| 分布式系统、高并发扣款 | 分布式锁 + 防重 + 幂等 |
| 最终一致性可接受(日志、统计) | 消息队列 + 幂等消费 |
一句话总结:
大多数业务先用“数据库原生的 MVCC + 乐观锁/条件更新”就能解决 80% 的并发问题,性能和复杂度都可控。只有真正高冲突、强一致性场景才需要悲观锁或分布式锁。
你的具体场景是什么?是库存扣减、订单状态变更、还是其他?是单机 MySQL/PostgreSQL,还是分布式(TiDB、CockroachDB 等)?告诉我,我可以给你更精确的代码/方案。