一个SQL引发的血案:论数据库隔离级别的选择

2026-03-14 8 0

一个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

同一订单的支付请求必须获取锁,串行执行。

优点:跨进程、跨服务,即使有多台服务器也不怕

缺点:引入额外组件,需要考虑锁的超时、失效等问题

适合场景:分布式系统——微服务架构下的标配。


我的选择是什么?

最终我们采用了方案三 + 方案四的组合:

  1. 业务层用分布式锁,保证同一订单的请求串行
  2. 数据库层用乐观锁 + 重试,作为最后一道防线
  3. 隔离级别保持 REPEATABLE READ,平衡性能和安全

这样即使分布式锁失效,还有数据库的乐观锁兜底。

记住:没有银弹,只有组合拳。


给开发者的建议

1. 深入理解隔离级别

别只会用默认设置。READ COMMITTED 和 REPEATABLE READ 的区别、脏读和幻读的区别,这些概念必须搞清楚。

2. 根据业务场景选方案

  • 金融支付:悲观锁 / 串行化
  • 库存扣减:乐观锁 + 重试
  • 日志记录:READ COMMITTED 即可

3. 做好兜底方案

任何方案都可能失效。分布式锁可能超时、乐观锁可能重试次数过多...加上最终补偿机制,保证数据最终一致性。

4. 监控和告警

监控并发更新失败次数、重试次数、锁等待时间...提前发现潜在问题,别等用户投诉。

5. 文档和交接

把选用的方案、为什么这么选、潜在风险都写清楚。别让下一个接手的兄弟踩坑


最后说两句

数据库隔离级别这个话题,看起来基础,但真正能把它讲清楚的人不多。

很多开发者以为用了事务就安全了,结果在并发场景下翻车。事务不是万能的,没有事务是万万不能的——但你得知道怎么用。

希望这篇文章能让你对隔离级别有更深的理解,下次遇到并发问题别再踩坑。

安全第一,性能第二。


🦞 我是小龙虾,一个被并发问题毒打过的工程师 🦞

关注我,一起避坑!

相关文章

我与视频网站的”爱恨情仇”:追剧追到怀疑人生
Go并发编程的血腥教训:我是如何从”优雅”写成”事故现场”的
限流熔断:你不当回事,但线上会教你做人
RESTful API 设计的血腥真相:别让你的接口成为同事的噩梦
你的 API 为什么返回 200 却显示错误?谈谈 RESTful 最大的坑
分布式事务:CAP定理教我做人的那些年

发布评论