Daily Study
更新: 10/26/2025 字数: 0 字 时长: 0 分钟
Daily Plan
#todo
- [ ]
mcp仓库
国内一些mcp仓库:
Mysql中Select for update
核心问题:竞争条件 (Race Condition) 与丢失更新 (Lost Update)
想象一个在线票务系统,两个用户在同一瞬间尝试预订最后一张演唱会门票。如果没有适当的保护措施,可能会发生以下情况:
两个用户的请求同时查询数据库。
系统都告诉他们“还有一张票”。
两个用户都继续执行购票操作。
这种多个线程/进程争夺同一资源而导致意外结果的现象,被称为竞争条件 (Race Condition)。在上述场景中,它会导致一张票被卖了两次,这种情况通常被称为丢失更新 (Lost Update)。
解决方案:使用 SELECT FOR UPDATE 实现悲观锁
当一个事务执行 SELECT ... FOR UPDATE 语句时,它会对查询到的数据行加上一个排他锁 (Exclusive Lock)。这个锁会带来以下效果:
阻塞其他事务的修改和删除操作:任何其他事务想对这些锁定的行执行
UPDATE或DELETE,都必须等待,直到持有锁的事务提交 (COMMIT) 或回滚 (ROLLBACK)。阻塞其他事务的加锁请求:其他事务无法对这些行再施加
FOR UPDATE或FOR SHARE类型的锁。
现在,我们再用 SELECT FOR UPDATE 来重演一次票务系统的场景:
| 时间点 | 事务 1 (用户 A) | 事务 2 (用户 B) | 数据库状态 (余票) |
|---|---|---|---|
| T1 | BEGIN; | 1 | |
| T2 | BEGIN; | 1 | |
| T3 | SELECT ... FROM events WHERE event_id = 123 FOR UPDATE; (查询并锁定该行) | 1 | |
| T4 | SELECT ... FROM events WHERE event_id = 123 FOR UPDATE; (等待锁释放) | 1 | |
| T5 | UPDATE events SET available_seats = 0 WHERE event_id = 123; | (仍在等待) | 0 |
| T6 | COMMIT; (提交事务,释放锁) | (仍在等待) | 0 |
| T7 | (获取到锁,开始执行查询,此时查到余票为 0) | 0 | |
| T8 | (发现余票为 0,通知用户 B 购票失败) | 0 | |
| T9 | ROLLBACK; (或 COMMIT;) | 0 |
SELECT FOR UPDATE 的常见应用场景
SELECT FOR UPDATE 特别适用于那些需要“先读取、再修改、最后写入”且数据一致性要求极高的场景:
库存管理系统:防止多个订单同时出售最后一件商品。
金融交易系统:确保一个账户的余额不会被两个交易同时修改,避免最终金额计算错误。
预订和预约系统:保证一个座位、一个房间或一个时间段不会被重复预订。
任务队列处理:允许一个工作进程从队列中取出一个任务并将其锁定,确保没有其他进程会重复处理同一个任务。
陷阱
- 降低并发性:因为锁会阻塞其他事务,如果一个锁被长时间持有,会迫使其他请求排队等待,可能成为系统的性能瓶颈
WHERE条件要精确:只锁定你真正需要修改的行。在WHERE子句中使用索引至关重要。如果没有合适的索引,数据库可能会将行锁升级为表锁,从而可能会导致死锁,这将严重影响整个系统的并发性能。
针对 Race Condition 问题其他解决方案
方式一:利用数据库的唯一约束 (UNIQUE Constraint)
这是最简单、最直接,也是最容易被忽略的方式。它的核心思想是让数据库本身来保证数据的唯一性,从而从根本上杜绝脏数据的产生。
核心思想:事后监督,不符合规则的直接拒绝。
如何实现:在表中的一个或多个列上创建
UNIQUE索引或约束。当有重复数据插入或更新时,数据库会直接报错,应用程序捕获这个错误并处理即可。适用场景:
用户注册:用户名或邮箱不能重复。给
username列加上唯一约束。秒杀抢购:一个用户只能抢购一次。在订单表里建立
(user_id, product_id)的联合唯一索引。预订座位:一个座位只能被一个人预订。在预订表里建立
(event_id, seat_number)的联合唯一索引。
优点:
极其简单:只需一条 DDL 语句定义约束,无需修改应用逻辑。
性能极高:依赖数据库底层实现,高效可靠。
最终保障:是数据一致性的最后一道防线,即使应用逻辑有漏洞,数据库也能守住底线。
缺点:
不灵活:只能处理“重复”问题,对于更复杂的“状态更新”竞争(如库存扣减)无能为力。
需要处理异常:应用代码需要优雅地捕获并处理数据库抛出的唯一性冲突异常。
-- 为用户表的'username'列添加唯一约束
ALTER TABLE users ADD CONSTRAINT uk_username UNIQUE (username);
-- 应用程序尝试插入,如果用户名已存在,数据库会报错
INSERT INTO users (username, password) VALUES ('testuser', '123456');
-- 如果'testuser'已存在,这里会失败,你的代码需要catch这个Exception。方式二:使用原子操作 (Atomic Operations)
这个方式的核心是将“读取-修改-写入”这个过程合并成一个不可分割的数据库操作,从而消除竞争条件发生的窗口。
核心思想:一步到位,不给竞争留机会。
如何实现:利用
UPDATE语句的WHERE条件来实现“检查与更新”的原子性。适用场景:
库存扣减:这是最经典的场景。
账户余额更新:在扣款前检查余额是否充足。
优点:
性能好:单条 SQL 语句,数据库执行效率高,没有事务加锁的开销。
逻辑简单:应用代码逻辑清晰,只需根据
UPDATE语句影响的行数来判断操作是否成功。
缺点:
- 适用性有限:只适用于简单的、可以用单条 SQL 表达的“检查并更新”逻辑。
UPDATE products
SET stock = stock - 1
WHERE id = 123 AND stock > 0;方式三:使用乐观锁 (Optimistic Locking)
乐观锁与悲观锁(SELECT FOR UPDATE)相对,它假设数据冲突是小概率事件。它不会在操作前加锁,而是在提交更新时检查数据是否已经被别人修改过。
核心思想:先斩后奏,提交时再检查。
如何实现:
在数据表中增加一个
version(版本号) 列或timestamp(时间戳) 列。读取数据:查询数据时,把
version字段也一并读出。提交更新:执行
UPDATE时,把version作为一个条件。只有当数据库中的version与你当初读到的一致时,才执行更新,并同时将version加一。
适用场景:
- 读多写少的场景,例如编辑商品信息、更新文章内容。冲突概率低,使用乐观锁可以避免加锁带来的性能开销。
优点:
- 并发性高:在整个操作过程中都不会阻塞其他事务的读取,只有在最后更新的瞬间才会产生冲突检测。
缺点:
实现稍复杂:需要应用程序自己处理版本号和更新失败后的逻辑(如重试或提示用户)。
“ABA”问题:如果使用版本号,可以避免这个问题。但如果使用时间戳,理论上存在数据从 A -> B -> A 的变化,但乐观锁无法感知。版本号是更稳妥的选择。
实践(以更新商品价格为例):
-- 1. 读取商品信息,包括版本号
SELECT price, version FROM products WHERE id = 123;
-- 假设查到 price = 100, version = 1
-- 2. 应用程序计算新价格,比如打九折
new_price = 90;
-- 3. 提交更新,带上版本号作为条件
UPDATE products
SET price = 90, version = version + 1
WHERE id = 123 AND version = 1;总结与如何选择
| 解决方法 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 唯一约束 | 让数据库兜底,事后监督 | 简单、高效、可靠 | 不灵活,只能防重复 | 用户注册、防重提交等 |
| 原子操作 | 一步到位,不留竞争窗口 | 性能好,逻辑简单 | 适用性有限,仅限简单逻辑 | 库存扣减、余额变更 |
| 乐观锁 | 先执行,提交时再检查 | 并发性极高 | 实现稍复杂,需要应用处理冲突 | 读多写少的场景,如编辑信息 |
悲观锁 (SELECT FOR UPDATE) | 事先加锁,防止一切冲突 | 严谨,数据绝对安全 | 并发性差,可能死锁 | 写多读少,冲突概率高的场景 |
