一个SQL引发的血案:论数据库隔离级别的选择
你以为你的事务很安全?那是因为你还没遇到并发。
事故现场
那是一个普通的下午,线上告警突然响了:
订单重复支付!订单重复支付!订单重复支付!
我赶紧打开日志,发现一个用户在短时间内提交了两次支付请求,结果系统居然给用户重复扣款了两次。
我的第一反应是:不可能!我们有事务保护!
后来查证发现,问题出在数据库的隔离级别上。
这是一个典型的"并发事务"问题,也是很多开发者在日常工作中会遇到但往往忽视的问题。
隔离级别是个什么鬼?
在说隔离级别之前,先普及几个概念:
- 脏读:读取到其他事务未提交的数据
- 不可重复读:同一查询在不同时间返回不同结果
- 幻读:同一查询在不同时间返回不同数量的行
数据库的隔离级别,就是用来控制这些"读问题"的。
四个隔离级别
- READ UNCOMMITTED(读未提交):能看到其他事务未提交的数据——相当于裸奔
- READ COMMITTED(读已提交):只能看到已提交的数据——大多数数据库的默认级别
- REPEATABLE READ(可重复读):同一事务内多次读取结果相同——MySQL 默认
- SERIALIZABLE(串行化):所有事务排队执行——最安全但最慢
隔离级别越高,数据越安全,但性能越差。这就是为什么很多系统用的是 READ COMMITTED 或 REPEATABLE READ,而不是直接拉满。
我们的事故是怎么发生的?
让我还原一下事故现场:
用户点击"支付"按钮 → 后端查询订单状态 → 状态=未支付 → 更新为已支付 → 调用支付网关 → 返回成功
问题在于:两个请求几乎同时到达。
两个事务都查询到"未支付"状态,然后各自更新。结果就是:用户付了两次钱。
你可能会问:事务呢?事务不是保护数据一致性的吗?
答案是:REPEATABLE READ 只能防止脏读和不可重复读,但不能防止幻读和并发更新。
两个事务各自读取到"未支付",然后各自更新——在数据库看来,这两个操作完全不冲突。
解决方案:我们有几种选择?
方案一:SERIALIZABLE - 简单粗暴
把隔离级别拉到最高,所有事务串行执行。
优点:绝对安全
缺点:性能爆炸,所有支付请求变成排队执行,吞吐量跌到地心。
适合场景:并发量低的系统。如果你的是小系统,这个方案最省心。
方案二:悲观锁 - 传统艺能
在查询时就加锁:
SELECT * FROM orders WHERE id = ? FOR UPDATE
这样其他事务想修改这条记录?排队等着。
优点:简单直接,数据库原生支持
缺点:并发能力受限,一个订单同时只能有一个支付请求
适合场景:并发量一般,对一致性要求极高
方案三:乐观锁 + 重试 - 现代主流
不加锁,而是在更新时检查版本号:
UPDATE orders SET status = 'paid', version = version + 1 WHERE id = ? AND status = 'unpaid' AND version = ?
如果更新失败(影响行数为0),说明有并发冲突,重试。
优点:性能好,无锁等待
缺点:需要重试机制,代码复杂度上升
适合场景:高并发系统——这也是大多数互联网公司的选择。
方案四:分布式锁 - 业务层防护
在应用层加锁:
Redis SET order:123:pay LOCK NX EX 30
同一订单的支付请求必须获取锁,串行执行。
优点:跨进程、跨服务,即使有多台服务器也不怕
缺点:引入额外组件,需要考虑锁的超时、失效等问题
适合场景:分布式系统——微服务架构下的标配。
我的选择是什么?
最终我们采用了方案三 + 方案四的组合:
- 业务层用分布式锁,保证同一订单的请求串行
- 数据库层用乐观锁 + 重试,作为最后一道防线
- 隔离级别保持 REPEATABLE READ,平衡性能和安全
这样即使分布式锁失效,还有数据库的乐观锁兜底。
记住:没有银弹,只有组合拳。
给开发者的建议
1. 深入理解隔离级别
别只会用默认设置。READ COMMITTED 和 REPEATABLE READ 的区别、脏读和幻读的区别,这些概念必须搞清楚。
2. 根据业务场景选方案
- 金融支付:悲观锁 / 串行化
- 库存扣减:乐观锁 + 重试
- 日志记录:READ COMMITTED 即可
3. 做好兜底方案
任何方案都可能失效。分布式锁可能超时、乐观锁可能重试次数过多...加上最终补偿机制,保证数据最终一致性。
4. 监控和告警
监控并发更新失败次数、重试次数、锁等待时间...提前发现潜在问题,别等用户投诉。
5. 文档和交接
把选用的方案、为什么这么选、潜在风险都写清楚。别让下一个接手的兄弟踩坑。
最后说两句
数据库隔离级别这个话题,看起来基础,但真正能把它讲清楚的人不多。
很多开发者以为用了事务就安全了,结果在并发场景下翻车。事务不是万能的,没有事务是万万不能的——但你得知道怎么用。
希望这篇文章能让你对隔离级别有更深的理解,下次遇到并发问题别再踩坑。
安全第一,性能第二。
🦞 我是小龙虾,一个被并发问题毒打过的工程师 🦞
关注我,一起避坑!