第三章 事务控制与并发机制

第四节 实战:高并发下单系统的事务设计

目标:通过一个典型的电商下单业务场景,掌握如何在 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 中设计一个高并发下单系统的事务逻辑,理解了如何使用事务、锁机制和存储过程来保障数据一致性,并能够在实际项目中应用这些知识提升系统的稳定性和性能。