第三章 事务控制与并发机制
第四节 实战:高并发下单系统的事务设计
目标:通过一个典型的电商下单业务场景,掌握如何在 PostgreSQL 中设计合理的事务边界、使用行级锁避免超卖问题,并结合 MVCC 和死锁处理机制实现高并发下单系统的一致性保障和性能优化。
在电商平台中,订单创建是一个典型的高并发操作。多个用户可能同时尝试购买同一商品库存,这会引发诸如“超卖”、“数据不一致”等问题。本节将以一个简化版的电商下单系统为例,讲解如何在 PostgreSQL 中合理设计事务逻辑,确保数据一致性与系统吞吐能力。
🛒 一、业务背景与表结构设计
1. 核心业务流程
- 用户发起下单请求
- 系统检查商品库存是否充足
- 如果库存充足,则创建订单并减少库存数量
- 否则返回“库存不足”
2. 示例数据库表结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -- 商品表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
stock INT NOT NULL CHECK (stock >= 0)
);
-- 订单表
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL REFERENCES products(product_id),
quantity INT NOT NULL CHECK (quantity > 0),
order_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
|
🔐 二、事务设计原则
为保证下单过程的数据一致性与隔离性,应遵循以下事务设计原则:
原则 | 描述 |
---|
ACID 完整性 | 使用事务保证下单操作的原子性和一致性 |
最小事务粒度 | 尽量缩短事务执行时间,提高并发性能 |
显式加锁避免竞争 | 使用 SELECT ... FOR UPDATE 显式锁定商品记录,防止并发修改 |
避免死锁 | 统一访问顺序,设置合理的锁等待超时 |
🧱 三、核心事务逻辑设计(SQL + PL/pgSQL)
1. 单次下单逻辑(手动 SQL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| BEGIN;
-- 锁定商品记录,防止其他事务修改
SELECT stock FROM products WHERE product_id = 1 FOR UPDATE;
-- 检查库存是否足够
IF (SELECT stock FROM products WHERE product_id = 1) >= 1 THEN
-- 创建订单
INSERT INTO orders (user_id, product_id, quantity)
VALUES (1001, 1, 1);
-- 减少库存
UPDATE products SET stock = stock - 1 WHERE product_id = 1;
ELSE
RAISE NOTICE '库存不足';
END IF;
COMMIT;
|
📌 注意:上述代码需在支持事务块的客户端(如 psql、JDBC、PgJDBC)中运行。
2. 使用存储过程封装下单逻辑(PL/pgSQL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| CREATE OR REPLACE FUNCTION place_order(user_id INT, product_id INT, quantity INT)
RETURNS TEXT AS $$
DECLARE
current_stock INT;
BEGIN
-- 开始事务自动由函数调用触发
SELECT stock INTO current_stock
FROM products
WHERE product_id = product_id
FOR UPDATE;
IF current_stock >= quantity THEN
INSERT INTO orders (user_id, product_id, quantity)
VALUES (user_id, product_id, quantity);
UPDATE products
SET stock = stock - quantity
WHERE product_id = product_id;
RETURN '下单成功';
ELSE
RETURN '库存不足';
END IF;
END;
$$ LANGUAGE plpgsql;
|
调用示例:
1
| SELECT place_order(1001, 1, 1);
|
⚙️ 四、高并发测试与性能优化建议
为了验证该设计在高并发下的表现,我们可以使用如下工具进行压力测试:
1. 使用 pgbench 进行模拟并发下单测试
1
2
3
| pgbench -U postgres -i -s 1 yourdb
pgbench -U postgres -c 10 -j 2 -t 1000 yourdb \
--script='call place_order(1001, 1, 1);'
|
参数说明:
-c 10
:10 个并发连接-j 2
:2 个工作线程-t 1000
:每个线程执行 1000 次下单操作
2. 性能监控建议
你可以使用如下方式监控事务行为:
1
2
3
4
5
6
7
8
9
| -- 查看当前锁信息
SELECT * FROM pg_locks;
-- 查看活跃查询
SELECT pid, usename, query, state, wait_event_type, wait_event
FROM pg_stat_statements;
-- 查看死锁日志(查看 PostgreSQL 日志文件)
tail -f /var/log/postgresql/postgresql-17-main.log
|
🛡️ 五、优化策略与高级技巧
1. 使用 NOWAIT 避免长时间阻塞
如果你希望在无法获取锁时立即失败而不是等待,可以在 FOR UPDATE
后加上 NOWAIT
:
1
| SELECT stock FROM products WHERE product_id = 1 FOR UPDATE NOWAIT;
|
如果此时已有事务锁定了该行,将抛出错误:
1
| ERROR: could not obtain lock on row in relation "products"
|
2. 设置事务超时限制
1
2
| SET LOCAL statement_timeout = '5s';
SET LOCAL lock_timeout = '3s';
|
防止事务因锁等待过久而影响用户体验或系统稳定性。
3. 使用乐观锁替代悲观锁(适用于部分场景)
对于低冲突场景,也可以考虑使用“乐观锁”,即不提前加锁,而在更新时判断版本号是否变化:
1
2
3
| UPDATE products
SET stock = stock - 1
WHERE product_id = 1 AND version = expected_version;
|
若返回 0 行受影响,表示有其他事务已修改该记录。
📈 六、扩展建议:分布式事务与分库分表
随着系统规模扩大,单实例 PostgreSQL 可能不能满足高并发写入需求。你可以考虑以下扩展方案:
扩展方向 | 工具/技术 | 适用场景 |
---|
分库分表 | Citus、pgSharding | 海量数据、超高并发 |
读写分离 | BDR、逻辑复制 | 写多读少场景 |
分布式事务 | 两阶段提交(2PC)、XA | 多服务协同下单 |
异步队列 | Kafka + Debezium + PostgreSQL | 解耦下单与库存服务 |
📌 小结
技术 | 功能 | 推荐使用场景 |
---|
FOR UPDATE | 显式锁定行 | 防止并发修改 |
事务控制 | 保证 ACID | 下单、支付等关键操作 |
存储过程 | 封装业务逻辑 | 提升可维护性 |
NOWAIT / lock_timeout | 控制锁等待行为 | 避免阻塞 |
死锁检测 | 自动回滚冲突事务 | 生产环境必须启用 |
pgbench | 并发压测工具 | 性能评估与调优 |
通过本节的学习,你应该已经掌握了如何在 PostgreSQL 中设计一个高并发下单系统的事务逻辑,理解了如何使用事务、锁机制和存储过程来保障数据一致性,并能够在实际项目中应用这些知识提升系统的稳定性和性能。