模块 11 · 第 2 课(共 4)课程中的42/57~10 min
模块课程(2/4)
比赛条件
了解数据库如何在单个终端上独立工作很简单,但在真实的 Web 场景中,将会有数百个并发的“Node/PHP/Go”进程发送请求。这就是著名的比赛条件诞生的地方。
“丢失更新”问题
当两个线程读取相同的旧行,在 JavaScript 服务器中对其进行一些数学计算,然后发回 UPDATE 以保存结果时,就会发生丢失更新。
设想:
- 产品 #5 有 10 个单位。
- 用户 A 购买一个(服务器 A 读取“10”,计算出“9”)。
- 在同一毫秒内,用户 B 在 A 的购买完成 COMMIT 阶段之前购买了另一个。
- 服务器 B 也读取“10”,计算“9”。
- 服务器 A 发送
UPDATE: set quantity = 9。 - 服务器 B 发送
UPDATE: set quantity = 9。 - 双方都认为他们成功地减少了数字。但最终的值应该是8个单位!虚拟购买已丢失。
无大量事务的解决方案(原子更新)
不要在外面阅读和进行数学计算,而是让数据库在更新期间自行完成,这始终是同步的。
SQL
-- Wrong
UPDATE products SET stock = 9 WHERE id = 5;
-- Correct and natively thread-safe
UPDATE products SET stock = stock - 1 WHERE id = 5;更极端的异常(脏读和幻象)
- 脏读:您读取了其他人尚未通过 COMMIT 确认的临时数据(并且可能会突然转到 ROLLBACK 使您无效)。
- 幻读:您扫描所有行,同时第二个线程插入新记录,然后您在同一事务期间进行另一次扫描,您会神奇地发现新记录不知从何而来,从而损害了计算的财务报告。
锻炼#sql.m11.l2.e1
尝试:0加载中...
以原子方式修改客户记录以进行小字符串操作。\n假设我们要重命名客户的“城市”以添加星号。 \n通过利用 id=10 的行自己的值,将“-Old”连接到当前值(使用 postgres'|| 连接管道)来完成此操作。
正在加载编辑器...
显示提示
更新客户 SET city = city || ' - 旧' WHERE id = 10;
3 次尝试后可用的解决方案
锻炼#sql.m11.l2.e2
尝试:0加载中...
典型的丢失更新发生在付款和价格中。\n假设出现紧急价格上涨:将“order_items”表(针对订单 1 的产品 1)的“unit_price”自动增加 20%,即乘以 * 1.2,而不是插入在 JS 中手动预先计算的新价格。
正在加载编辑器...
显示提示
UPDATE order_items SET unit_price = unit_price * 1.2 WHERE order_id = 1 AND Product_id = 1;
3 次尝试后可用的解决方案