数据库事务:提交前未锁表的致命风险
说白了,数据库事务就是你写代码时的“承诺书”。
你写了一条 SQL,执行完它,你要保证:要么全部成功,要么全部失败。
可如果这条事务没在提交前把表锁住,那结果可能不是“失败”,而是“崩盘”。
这事儿真不是危言耸听。
圈内有个老梗:
“你以为你加了个事务?你只是在给数据埋雷。”
一、你真的以为事务“安全”?
我们先看个最简单的例子:
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 此时,别的会话也可以读取到这个未提交的值。
这时候,如果另一个线程也查 users 表,读到了那个 balance = -100 的值——
这就是“脏读”。
更可怕的是,如果你没在提交前加锁,那这不光是脏读,还可能是“幻读”、“不可重复读”、“丢失更新”——
一个小小的“没锁表”,能让你整个系统直接“原地爆炸”。
二、数据库锁机制的“假象”
很多人以为,只要用了事务,就万事大吉。
其实不然。
数据库的锁分两种:
- 共享锁(S Lock):读操作用,允许多个事务同时读。
- 排他锁(X Lock):写操作用,不允许其他事务读或写。
但如果你在事务中只执行了 UPDATE,却没手动加锁(比如用 SELECT ... FOR UPDATE),那别的事务还是能读到你“正在修改”的数据。
这就像你刚把门打开,还没锁上,别人就进来了。
你再怎么解释“我不是故意的”,系统已经帮你“背锅”了。
三、真实案例:一次“看似正常”的数据灾难
某金融平台,用户提现流程是这样的:
BEGIN;
SELECT balance FROM users WHERE user_id = 12345;
UPDATE users SET balance = balance - 100 WHERE user_id = 12345;
COMMIT;
看起来没问题吧?
但问题是,这中间没加锁!
假设两个用户同时进行这笔操作:
- 用户 A:余额 1000,准备提 100;
- 用户 B:余额 1000,也准备提 100;
他们几乎同时发起请求,系统都读到了 1000 的余额,然后都扣了 100,最后都变成 900。
但问题来了:
- 用户 A 提现成功;
- 用户 B 也提示成功;
- 可是最后账户只剩 900。
这就是典型的“丢失更新”。
如果你在事务里没加锁,数据库默认是“读已提交”隔离级别,你连“读到谁”都控制不了。
四、专业对比表:加锁 vs 不加锁的性能与安全性差异
| 场景 | 是否加锁 | 性能影响 | 数据一致性 | 安全性 |
|---|---|---|---|---|
| 事务中普通 UPDATE | 否 | 低 | 差 | 低 |
| 事务中加 SELECT FOR UPDATE | 是 | 中高 | 高 | 高 |
| 使用悲观锁 | 是 | 高 | 极高 | 极高 |
| 使用乐观锁(版本号) | 否 | 低 | 中 | 中 |
五、避坑指南(3个常见误区)
❌误区1:“事务就是安全的,不需要额外加锁”
这是典型的“工具迷信”。
事务只保证原子性、一致性、隔离性和持久性(ACID),但它不等于锁机制。
你没主动加锁,系统不会替你挡子弹。
❌误区2:“我用的是 MySQL InnoDB,它默认就是行锁”
你没错,InnoDB 确实是行锁。
但问题是,你得在事务中显式加上 FOR UPDATE 才能锁住行。
否则你只锁了“事务本身”,没锁“数据”。
❌误区3:“我只处理单线程,不用考虑并发问题”
纯属扯淡。
现在的系统,哪怕你单线程跑,也可能会因为异步任务、定时任务、缓存刷新等触发并发。
你以为你安全,其实是“没准备好迎接风暴”。
六、实战建议:怎么真正“锁住数据”?
-
显式加锁:
BEGIN; SELECT balance FROM users WHERE user_id = 12345 FOR UPDATE; UPDATE users SET balance = balance - 100 WHERE user_id = 12345; COMMIT; -
使用悲观锁 + 重试机制:
- 如果获取不到锁,等待几秒后重试。
- 防止死锁,避免阻塞太久。
-
结合乐观锁(版本号):
- 给每条记录加一个 version 字段。
- 更新时判断版本是否一致,不一致就回滚。
七、FAQ(真实场景下的“毒舌问答”)
Q:是不是所有事务都要加锁?
A:不是。
但你得知道,你在做什么。
比如你只是读取数据,不修改,那就无所谓。
但一旦涉及“读改”或者“写写”,不加锁就是拿数据开玩笑。
Q:加锁会影响性能吗?
A:当然。
但你要是因为怕慢就“不锁”,最后数据全乱了,那才是真的慢。
Q:有没有办法“不加锁也能安全”?
A:可以,但只适用于非常明确的业务场景。
比如:幂等性操作、只读操作、无状态接口。
其他情况,别天真了。
Q:为什么有些框架不自动加锁?
A:因为它们觉得你“知道你在干嘛”。
但现实是,你根本不知道你“不知道”。
Q:能不能用分布式锁代替数据库锁?
A:可以,但代价是复杂度和性能下降。
数据库锁快、可靠、透明。
除非你真要用 Redis 做全局锁,否则别折腾自己。
别再信“事务万能论”了。
你写的每一行 SQL,都是在和并发“博弈”。
你不加锁,就是在等系统“崩”。