sql: Transaction & Concurrency Patterns using postgresql 18

前言

在珠宝电商业务中,订单、库存、资金等核心数据的一致性与并发控制是系统稳定运行的关键。本文基于 PostgreSQL 18,以珠宝行业为业务背景,通过完整的 SQL 脚本实战演示事务隔离、锁机制、死锁预防以及 Saga 分布式事务模式,帮助读者掌握数据库事务与并发控制的核心技能。

一、事务基础与隔离级别

1.1 事务的 ACID 特性

理解原子性、一致性、隔离性和持久性四大特性,以及它们在珠宝订单、库存扣减等场景中的具体体现。

1.2 隔离级别与并发问题

对比读已提交、可重复读、可串行化等隔离级别,分析脏读、不可重复读、幻读在珠宝库存查询中的实际影响。

1.3 事务控制语句实战

通过 BEGIN、COMMIT、ROLLBACK、SAVEPOINT 等语句,演示珠宝订单创建与库存扣减的完整事务流程。

二、锁机制与并发控制

2.1 PostgreSQL 锁类型详解

介绍表级锁、行级锁、咨询锁等类型,结合珠宝库存表说明各类锁的适用场景与兼容性矩阵。

2.2 乐观锁与悲观锁实践

通过版本号字段实现乐观锁,对比 SELECT FOR UPDATE 悲观锁,在珠宝商品价格更新场景中演示两种策略的取舍。

2.3 死锁检测与预防

分析珠宝订单与资金账户交叉更新导致的死锁案例,演示死锁检测机制与预防策略。

三、Saga 分布式事务模式

3.1 Saga 模式原理与设计

介绍 Saga 模式的编排式与协同式实现,结合珠宝订单、库存、资金三个服务说明补偿事务的设计思路。

3.2 订单创建 Saga 实战

通过 saga_log 表追踪订单创建、库存预占、资金冻结、支付确认四个步骤,演示失败时的反向补偿流程。

3.3 订单取消与一致性验证

实现订单取消的 Saga 补偿流程,并通过库存、订单金额、账户余额等一致性校验 SQL 验证最终一致性。

总结与参考资料

本文从事务基础、锁机制到 Saga 分布式事务,系统梳理了 PostgreSQL 18 在珠宝行业高并发场景下的实践要点。建议读者结合完整脚本逐步执行,深入理解每个环节的设计意图。

参考资料:PostgreSQL 官方文档、PostgreSQL 18 Release Notes、分布式事务设计模式相关书籍与社区实践。

sql 复制代码
-- ============================================================
-- PostgreSQL 18 珠宝行业 - 事务与并发模式实战(完整版)
-- Geovin Du,geovindu
-- 合并版:所有模块合并为一个脚本,可一次性执行
-- ============================================================
 
 
-- ============================================================
-- 模块: pg18_schema.sql
-- ============================================================
 
-- ============================================================
-- PostgreSQL 18 珠宝行业 - 建库建表
-- 事务与并发模式实战 - 模式定义
-- ============================================================
 
-- 创建数据库(如已存在则跳过)
SELECT 'CREATE DATABASE jewelry_db'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'jewelry_db')\gexec
 
-- 连接到新数据库
\c jewelry_db
 
-- 启用扩展
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE EXTENSION IF NOT EXISTS tablefunc;
 
-- 创建枚举类型
CREATE TYPE product_category AS ENUM (
    '戒指', '项链', '手链', '耳环', '腕表',
    '胸针', '头饰', '饰品配件', '定制珠宝', '古董珠宝'
);
 
CREATE TYPE order_status AS ENUM (
    '待支付', '已支付', '生产中', '质检中', '已发货',
    '已收货', '已取消', '已退款'
);
 
CREATE TYPE payment_method AS ENUM ('信用卡', '支付宝', '微信支付', '银行转账', '分期付款');
 
CREATE TYPE currency AS ENUM ('CNY', 'USD', 'EUR', 'JPY', 'HKD');
 
-- ============================================================
-- 1. 产品分类表
-- ============================================================
CREATE TABLE category (
    id            SERIAL PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    parent_id     INT REFERENCES category(id) ON DELETE SET NULL,
    description   TEXT,
    sort_order    INT DEFAULT 0,
    is_active     BOOLEAN DEFAULT TRUE,
    created_at    TIMESTAMPTZ DEFAULT NOW(),
    updated_at    TIMESTAMPTZ DEFAULT NOW()
);
 
COMMENT ON TABLE category IS '珠宝产品分类表';
COMMENT ON COLUMN category.parent_id IS '上级分类ID,NULL表示顶级分类';
 
-- ============================================================
-- 2. 产品表
-- ============================================================
CREATE TABLE product (
    id              SERIAL PRIMARY KEY,
    name            VARCHAR(200) NOT NULL,
    category_id     INT NOT NULL REFERENCES category(id),
    sku             VARCHAR(50) NOT NULL,
    material        VARCHAR(100),
    gemstone        VARCHAR(100),
    carat_weight    DECIMAL(8,2),
    color_grade     VARCHAR(20),
    clarity_grade   VARCHAR(20),
    cut_grade       VARCHAR(20),
    certification   VARCHAR(100),
    price           DECIMAL(12,2) NOT NULL,
    cost            DECIMAL(12,2),
    stock_quantity  INT NOT NULL DEFAULT 0,
    min_stock       INT DEFAULT 5,
    description     TEXT,
    image_url       VARCHAR(500),
    is_active       BOOLEAN DEFAULT TRUE,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    version         INT DEFAULT 0,
    CONSTRAINT chk_price_positive CHECK (price > 0),
    CONSTRAINT chk_stock_nonneg CHECK (stock_quantity >= 0),
    CONSTRAINT chk_cost_positive CHECK (cost > 0 OR cost IS NULL),
    CONSTRAINT uq_sku UNIQUE (sku)
);
 
COMMENT ON TABLE product IS '珠宝产品表';
COMMENT ON COLUMN product.version IS '乐观锁版本号,每次更新+1';
 
-- ============================================================
-- 3. 客户表
-- ============================================================
CREATE TABLE customer (
    id              SERIAL PRIMARY KEY,
    customer_no     VARCHAR(50) NOT NULL,
    name            VARCHAR(100) NOT NULL,
    phone           VARCHAR(20),
    email           VARCHAR(100),
    membership_level VARCHAR(20) DEFAULT '普通',
    total_spent     DECIMAL(14,2) DEFAULT 0,
    address         TEXT,
    notes           TEXT,
    is_active       BOOLEAN DEFAULT TRUE,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    CONSTRAINT uq_customer_no UNIQUE (customer_no),
    CONSTRAINT chk_total_spent CHECK (total_spent >= 0)
);
 
COMMENT ON TABLE customer IS '客户表';
 
-- ============================================================
-- 4. 订单表
-- ============================================================
CREATE TABLE "order" (
    id              SERIAL PRIMARY KEY,
    order_no        VARCHAR(50) NOT NULL,
    customer_id     INT NOT NULL REFERENCES customer(id),
    status          order_status DEFAULT '待支付',
    payment_method  payment_method,
    total_amount    DECIMAL(14,2) NOT NULL,
    discount_amount DECIMAL(14,2) DEFAULT 0,
    actual_amount   DECIMAL(14,2) NOT NULL,
    currency        currency DEFAULT 'CNY',
    shipping_addr   TEXT,
    shipping_track  VARCHAR(100),
    payment_time    TIMESTAMPTZ,
    shipped_time    TIMESTAMPTZ,
    received_time   TIMESTAMPTZ,
    cancelled_time  TIMESTAMPTZ,
    cancel_reason   VARCHAR(500),
    notes           TEXT,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    version         INT DEFAULT 0,
    CONSTRAINT uq_order_no UNIQUE (order_no),
    CONSTRAINT chk_amount_nonneg CHECK (total_amount >= 0),
    CONSTRAINT chk_discount CHECK (discount_amount >= 0),
    CONSTRAINT chk_actual CHECK (actual_amount >= 0)
);
 
COMMENT ON TABLE "order" IS '订单表';
COMMENT ON COLUMN "order".version IS '乐观锁版本号';
 
-- ============================================================
-- 5. 订单明细表
-- ============================================================
CREATE TABLE order_item (
    id              SERIAL PRIMARY KEY,
    order_id        INT NOT NULL REFERENCES "order"(id) ON DELETE CASCADE,
    product_id      INT NOT NULL REFERENCES product(id),
    product_name    VARCHAR(200) NOT NULL,
    product_sku     VARCHAR(50) NOT NULL,
    quantity        INT NOT NULL DEFAULT 1,
    unit_price      DECIMAL(12,2) NOT NULL,
    subtotal        DECIMAL(14,2) NOT NULL,
    CONSTRAINT chk_qty_positive CHECK (quantity > 0),
    CONSTRAINT chk_price_positive CHECK (unit_price >= 0),
    CONSTRAINT chk_subtotal CHECK (subtotal = quantity * unit_price)
);
 
COMMENT ON TABLE order_item IS '订单明细表';
 
-- ============================================================
-- 6. 库存表
-- ============================================================
CREATE TABLE inventory (
    id              SERIAL PRIMARY KEY,
    product_id      INT NOT NULL REFERENCES product(id) ON DELETE CASCADE,
    warehouse       VARCHAR(50) DEFAULT '主仓库',
    quantity        INT NOT NULL DEFAULT 0,
    reserved_qty    INT NOT NULL DEFAULT 0,
    available_qty   INT GENERATED ALWAYS AS (quantity - reserved_qty) STORED,
    last_sync_time  TIMESTAMPTZ DEFAULT NOW(),
    CONSTRAINT uq_product_warehouse UNIQUE (product_id, warehouse),
    CONSTRAINT chk_qty_nonneg CHECK (quantity >= 0),
    CONSTRAINT chk_reserved_nonneg CHECK (reserved_qty >= 0)
);
 
COMMENT ON TABLE inventory IS '库存表';
COMMENT ON COLUMN inventory.available_qty IS '可用库存 = 总库存 - 预占数量';
 
-- ============================================================
-- 7. 账户/资金表
-- ============================================================
CREATE TABLE account (
    id              SERIAL PRIMARY KEY,
    customer_id     INT NOT NULL REFERENCES customer(id),
    balance         DECIMAL(14,2) DEFAULT 0,
    frozen_amount   DECIMAL(14,2) DEFAULT 0,
    total_recharge  DECIMAL(14,2) DEFAULT 0,
    total_consume   DECIMAL(14,2) DEFAULT 0,
    version         INT DEFAULT 0,
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    CONSTRAINT uq_customer_account UNIQUE (customer_id),
    CONSTRAINT chk_balance_nonneg CHECK (balance >= 0),
    CONSTRAINT chk_frozen_nonneg CHECK (frozen_amount >= 0),
    CONSTRAINT chk_total_recharge CHECK (total_recharge >= 0),
    CONSTRAINT chk_total_consume CHECK (total_consume >= 0)
);
 
COMMENT ON TABLE account IS '客户账户资金表';
COMMENT ON COLUMN account.frozen_amount IS '冻结金额(下单时冻结,支付/取消时解冻)';
 
-- ============================================================
-- 8. 库存日志表(审计追踪)
-- ============================================================
CREATE TABLE inventory_log (
    id              SERIAL PRIMARY KEY,
    product_id      INT NOT NULL REFERENCES product(id),
    warehouse       VARCHAR(50),
    change_type     VARCHAR(20) NOT NULL,
    change_qty      INT NOT NULL,
    before_qty      INT NOT NULL,
    after_qty       INT NOT NULL,
    related_order_id INT,
    related_order_no VARCHAR(50),
    operator        VARCHAR(50) DEFAULT 'SYSTEM',
    created_at      TIMESTAMPTZ DEFAULT NOW()
);
 
COMMENT ON TABLE inventory_log IS '库存变动日志表';
COMMENT ON COLUMN inventory_log.change_type IS '变动类型:stock_in/stock_out/reserve/release/adjust';
 
-- ============================================================
-- 9. Saga事务日志表(最终一致性追踪)
-- ============================================================
CREATE TABLE saga_log (
    id              SERIAL PRIMARY KEY,
    saga_id         UUID NOT NULL,
    step_name       VARCHAR(100) NOT NULL,
    step_order      INT NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    request_payload JSONB,
    response_payload JSONB,
    error_message   TEXT,
    retry_count     INT DEFAULT 0,
    max_retries     INT DEFAULT 3,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    completed_at    TIMESTAMPTZ,
    CONSTRAINT chk_step_order CHECK (step_order >= 0),
    CONSTRAINT chk_retry CHECK (retry_count >= 0 AND max_retries >= 0)
);
 
COMMENT ON TABLE saga_log IS 'Saga事务日志表,用于追踪分布式事务执行状态';
COMMENT ON COLUMN saga_log.saga_id IS 'Saga事务唯一标识';
COMMENT ON COLUMN saga_log.step_name IS '步骤名称:create_order/reserve_stock/deduct_balance/confirm_payment';
COMMENT ON COLUMN saga_log.status IS '步骤状态:PENDING/EXECUTING/SUCCEEDED/FAILED/COMPENSATING/COMPENSATED/TIMEOUT';
 
-- ============================================================
-- 10. 死锁演示表
-- ============================================================
CREATE TABLE deadlock_demo (
    id              SERIAL PRIMARY KEY,
    account_a       INT,
    account_b       INT,
    amount          DECIMAL(12,2) NOT NULL,
    description     TEXT,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);
 
COMMENT ON TABLE deadlock_demo IS '死锁演示专用表';
 
-- ============================================================
-- 创建索引
-- ============================================================
CREATE INDEX idx_product_category ON product(category_id);
CREATE INDEX idx_product_sku ON product(sku);
CREATE INDEX idx_product_active ON product(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_order_customer ON "order"(customer_id);
CREATE INDEX idx_order_status ON "order"(status);
CREATE INDEX idx_order_created ON "order"(created_at DESC);
CREATE INDEX idx_order_item_order ON order_item(order_id);
CREATE INDEX idx_order_item_product ON order_item(product_id);
CREATE INDEX idx_inventory_product ON inventory(product_id);
CREATE INDEX idx_inventory_available ON inventory(available_qty) WHERE available_qty > 0;
CREATE INDEX idx_customer_no ON customer(customer_no);
CREATE INDEX idx_account_customer ON account(customer_id);
CREATE INDEX idx_inventory_log_product ON inventory_log(product_id);
CREATE INDEX idx_inventory_log_created ON inventory_log(created_at DESC);
CREATE INDEX idx_saga_log_saga ON saga_log(saga_id);
CREATE INDEX idx_saga_log_status ON saga_log(status) WHERE status IN ('PENDING', 'EXECUTING');
 
-- ============================================================
-- 创建视图
-- ============================================================
CREATE VIEW v_product_stock AS
SELECT
    p.id, p.sku, p.name, p.category_id, c.name AS category_name,
    p.stock_quantity, p.price,
    COALESCE(i.available_qty, 0) AS available_stock,
    i.warehouse
FROM product p
LEFT JOIN inventory i ON p.id = i.product_id
LEFT JOIN category c ON p.category_id = c.id;
 
COMMENT ON VIEW v_product_stock IS '产品库存视图';
 
CREATE VIEW v_order_summary AS
SELECT
    o.id, o.order_no, o.status, o.actual_amount,
    c.name AS customer_name, c.membership_level,
    COUNT(oi.id) AS item_count,
    o.created_at
FROM "order" o
JOIN customer c ON o.customer_id = c.id
LEFT JOIN order_item oi ON o.id = oi.order_id
GROUP BY o.id, o.order_no, o.status, o.actual_amount, c.name, c.membership_level, o.created_at;
 
COMMENT ON VIEW v_order_summary IS '订单汇总视图';
 
-- ============================================================
-- 创建触发器函数(自动更新时间戳)
-- ============================================================
CREATE OR REPLACE FUNCTION fn_update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
 
CREATE TRIGGER trg_product_updated
    BEFORE UPDATE ON product
    FOR EACH ROW EXECUTE FUNCTION fn_update_timestamp();
 
CREATE TRIGGER trg_order_updated
    BEFORE UPDATE ON "order"
    FOR EACH ROW EXECUTE FUNCTION fn_update_timestamp();
 
CREATE TRIGGER trg_customer_updated
    BEFORE UPDATE ON customer
    FOR EACH ROW EXECUTE FUNCTION fn_update_timestamp();
 
CREATE TRIGGER trg_account_updated
    BEFORE UPDATE ON account
    FOR EACH ROW EXECUTE FUNCTION fn_update_timestamp();
 
-- ============================================================
-- 创建序列
-- ============================================================
CREATE SEQUENCE IF NOT EXISTS seq_saga_id START WITH 1 INCREMENT BY 1;
 
-- ============================================================
-- 初始化分类数据
-- ============================================================
INSERT INTO category (name, parent_id, description, sort_order) VALUES
('钻戒', NULL, '钻石戒指系列', 1),
('彩宝', NULL, '彩色宝石系列', 2),
('黄金', NULL, '黄金制品', 3),
('翡翠', NULL, '翡翠玉石系列', 4),
('珍珠', NULL, '珍珠系列', 5),
('腕表', NULL, '高端腕表', 6),
('定制', NULL, '定制珠宝', 7),
('配饰', NULL, '时尚配饰', 8),
('礼品', NULL, '珠宝礼品', 9),
('古董', NULL, '古董/vintage珠宝', 10);
 
-- ============================================================
-- 验证
-- ============================================================
SELECT table_name, table_type
FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
ORDER BY table_name;
 
SELECT table_name, table_type
FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'VIEW'
ORDER BY table_name;
 
SELECT enumlabel FROM pg_enum
JOIN pg_type ON pg_enum.enumtypid = pg_type.oid
WHERE pg_type.typname IN ('product_category', 'order_status', 'payment_method', 'currency')
ORDER BY pg_type.typname, enumordinal;
 
SELECT indexname, tablename
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;
 
SELECT trigger_name, event_object_table
FROM information_schema.triggers
WHERE trigger_schema = 'public';
 
\echo 'Schema creation completed successfully!'
 
 
-- ============================================================
-- 模块: pg18_data.sql
-- ============================================================
 
-- ============================================================
-- PostgreSQL 18 珠宝行业 - 批量测试数据
-- 事务与并发模式实战
-- ============================================================
 
\c jewelry_db
 
-- ============================================================
-- 1. 产品分类数据
-- ============================================================
INSERT INTO category (name, parent_id, description, sort_order) VALUES
('钻戒', NULL, '钻石戒指系列', 1),
('彩宝', NULL, '彩色宝石系列', 2),
('黄金', NULL, '黄金制品', 3),
('翡翠', NULL, '翡翠玉石系列', 4),
('珍珠', NULL, '珍珠系列', 5),
('腕表', NULL, '高端腕表', 6),
('定制', NULL, '定制珠宝', 7),
('配饰', NULL, '时尚配饰', 8),
('礼品', NULL, '珠宝礼品', 9),
('古董', NULL, '古董/vintage珠宝', 10),
-- 子分类
('求婚钻戒', 1, '求婚专用钻戒', 1),
('日常钻戒', 1, '日常佩戴钻戒', 2),
('红宝石', 2, '红宝石系列', 1),
('蓝宝石', 2, '蓝宝石系列', 2),
('祖母绿', 2, '祖母绿系列', 3),
('投资金条', 3, '投资用金条', 1),
('黄金首饰', 3, '黄金首饰', 2),
('缅甸翡翠', 4, '缅甸产翡翠', 1),
('翡翠手镯', 4, '翡翠手镯', 2),
('南洋珍珠', 5, '南洋珍珠', 1),
('Akoya珍珠', 5, 'Akoya珍珠', 2);
 
-- ============================================================
-- 2. 珠宝产品数据(50款)
-- ============================================================
INSERT INTO product (name, category_id, sku, material, gemstone, carat_weight, color_grade, clarity_grade, cut_grade, certification, price, cost, stock_quantity, min_stock, description) VALUES
('经典六爪钻戒-1克拉', 11, 'RG-DIA-001', 'PT950铂金', '钻石', 1.00, 'D', 'VVS1', 'EX', 'GIA', 58800.00, 28000.00, 15, 5, '经典六爪镶嵌,火彩璀璨'),
(' Halo光环钻戒-0.8克拉', 11, 'RG-DIA-002', '18K白金', '钻石', 0.80, 'E', 'VS1', 'EX', 'GIA', 35600.00, 17000.00, 20, 5, 'Halo光环设计,显钻效果佳'),
('三石钻戒-1.2克拉', 11, 'RG-DIA-003', '18K玫瑰金', '钻石', 1.20, 'F', 'VS1', 'VG', 'GIA', 72000.00, 34000.00, 8, 3, '过去现在未来三石设计'),
('素圈戒指-3mm', 12, 'RG-SU-001', 'PT950铂金', NULL, NULL, NULL, NULL, NULL, NULL, 8800.00, 4200.00, 50, 10, '经典素圈,简约百搭'),
('扭臂钻戒-0.5克拉', 12, 'RG-DIA-004', '18K白金', '钻石', 0.50, 'G', 'VS2', 'VG', 'NGTC', 18500.00, 8800.00, 25, 8, '扭臂设计独特优雅'),
('红宝戒指-1.5克拉', 13, 'RB-RB-001', '18K黄金', '红宝石', 1.50, '鸽血红', NULL, NULL, 'GRS', 68000.00, 32000.00, 5, 2, '缅甸鸽血红红宝石'),
('红宝耳环-0.8克拉', 13, 'RB-RB-002', '18K玫瑰金', '红宝石', 0.80, '真红', NULL, NULL, 'GIA', 25600.00, 12000.00, 12, 5, '红宝石耳钉'),
('蓝宝石戒指-2克拉', 14, 'RB-SA-001', '18K白金', '蓝宝石', 2.00, '皇家蓝', NULL, NULL, 'GRS', 85000.00, 40000.00, 3, 2, '斯里兰卡皇家蓝蓝宝石'),
('蓝宝石吊坠-1.2克拉', 14, 'RB-SA-002', 'PT950铂金', '蓝宝石', 1.20, '矢车菊', NULL, NULL, 'GIA', 42000.00, 20000.00, 10, 5, '矢车菊蓝蓝宝石吊坠'),
('祖母绿戒指-1克拉', 15, 'RB-EM-001', '18K黄金', '祖母绿', 1.00, '沃顿绿', NULL, NULL, 'GIA', 55000.00, 26000.00, 6, 3, '哥伦比亚祖母绿'),
('祖母绿项链-0.8克拉', 15, 'RB-EM-002', '18K白金', '祖母绿', 0.80, '森林绿', NULL, NULL, 'GRS', 32000.00, 15000.00, 8, 5, '祖母绿钻石项链'),
('投资金条-100克', 16, 'GL-BAR-001', 'Au9999', NULL, NULL, NULL, NULL, NULL, 'SGP', 48500.00, 45000.00, 30, 10, '上海金交所标准金条'),
('投资金条-50克', 16, 'GL-BAR-002', 'Au9999', NULL, NULL, NULL, NULL, NULL, 'SGP', 24200.00, 22500.00, 40, 10, '上海金交所标准金条'),
('黄金手镯-古法金', 17, 'GL-HZ-001', 'Au999', 'NULL', NULL, NULL, NULL, NULL, 'NGTC', 32000.00, 18000.00, 15, 5, '古法工艺黄金手镯'),
('黄金项链-肖邦链', 17, 'GL-NL-001', 'Au999', NULL, NULL, NULL, NULL, NULL, 'NGTC', 15800.00, 8500.00, 20, 8, '经典肖邦链黄金项链'),
('缅甸翡翠手镯', 18, 'JG-HZ-001', NULL, '翡翠', NULL, '阳绿', 'A货', NULL, 'NGTC', 128000.00, 60000.00, 2, 1, '缅甸天然A货翡翠手镯'),
('翡翠观音吊坠', 18, 'JG-GY-001', NULL, '翡翠', NULL, '冰种绿', 'A货', NULL, 'NGTC', 68000.00, 32000.00, 5, 2, '冰种翡翠观音吊坠'),
('翡翠手镯-紫罗兰', 19, 'JG-HZ-002', NULL, '翡翠', NULL, '紫罗兰', 'A货', NULL, 'NGTC', 95000.00, 45000.00, 3, 2, '紫罗兰翡翠手镯'),
('南洋珍珠项链', 20, 'PRL-NL-001', '18K黄金', '南洋珍珠', NULL, '白色', NULL, NULL, 'GIA', 28000.00, 13000.00, 10, 5, '南洋白珠项链18mm'),
('Akoya珍珠耳环', 21, 'PRL-AK-001', '18K白金', 'Akoya珍珠', NULL, '粉色', NULL, NULL, 'GIA', 12800.00, 6000.00, 15, 5, 'Akoya粉珍珠耳钉'),
('百达翡丽鹦鹉螺', 6, 'WP-PATEK-001', '18K玫瑰金', NULL, NULL, NULL, NULL, NULL, NULL, 285000.00, 220000.00, 1, 1, '百达翡丽鹦鹉螺5711'),
('劳力士迪通拿', 6, 'WP-ROLEX-001', '904L钢', NULL, NULL, NULL, NULL, NULL, NULL, 168000.00, 130000.00, 2, 1, '劳力士宇宙计型迪通拿'),
('卡地亚坦克', 6, 'WP-CARTIER-001', '18K黄金', NULL, NULL, NULL, NULL, NULL, NULL, 98000.00, 75000.00, 3, 2, '卡地亚坦克系列腕表'),
('定制钻戒-异形钻', 7, 'CT-DIA-001', 'PT950铂金', '钻石', 1.50, 'D', 'IF', 'EX', 'GIA', 128000.00, 58000.00, 2, 1, '定制异形钻戒指'),
('定制项链-生辰石', 7, 'CT-NL-001', '18K黄金', '红宝石', 1.00, '鸽血红', NULL, NULL, 'GRS', 45000.00, 21000.00, 5, 2, '定制生辰石项链'),
('时尚耳环-锆石', 8, 'ACC-ZR-001', 'S925银', '立方氧化锆', NULL, '彩色', NULL, NULL, NULL, 1280.00, 380.00, 100, 20, '时尚锆石耳环'),
('手链-珍珠串珠', 8, 'ACC-PRL-001', '18K黄金', '珍珠', NULL, '白色', NULL, NULL, NULL, 3500.00, 1500.00, 30, 10, '天然珍珠串珠手链'),
('胸针-花卉造型', 8, 'ACC-XZ-001', '18K白金', '钻石', 0.30, NULL, NULL, NULL, NULL, 8800.00, 4200.00, 10, 5, '花卉造型钻石胸针'),
('礼品套装-婚庆', 9, 'GFT-WED-001', '18K白金', '钻石', 0.50, 'E', 'VS1', 'EX', 'GIA', 35000.00, 16500.00, 10, 5, '婚庆珠宝礼品套装'),
('礼品礼盒-生日', 9, 'GFT-BDY-001', '18K黄金', '珍珠', NULL, '粉色', NULL, NULL, NULL, 8800.00, 4000.00, 15, 5, '生日珠宝礼盒'),
('古董胸针-维多利亚', 10, 'VTG-XZ-001', '铂金', '钻石', 0.80, NULL, NULL, NULL, NULL, 55000.00, 25000.00, 1, 1, '维多利亚时期古董胸针'),
('古董戒指- Art Deco', 10, 'VTG-RG-001', '铂金', '钻石', 1.00, NULL, NULL, NULL, NULL, 78000.00, 35000.00, 1, 1, 'Art Deco时期钻石戒指'),
('翡翠吊坠-佛公', 18, 'JG-FG-001', NULL, '翡翠', NULL, '糯种绿', 'A货', NULL, 'NGTC', 28000.00, 13000.00, 8, 3, '糯种翡翠佛公吊坠'),
('黄金吊坠-转运珠', 17, 'GL-DZ-001', 'Au999', NULL, NULL, NULL, NULL, NULL, 'NGTC', 5600.00, 3000.00, 40, 10, '古法金转运珠吊坠'),
('蓝宝石手链', 14, 'RB-SA-003', '18K白金', '蓝宝石', 3.50, '皇家蓝', NULL, NULL, 'GRS', 158000.00, 75000.00, 2, 1, '蓝宝石钻石手链'),
('红宝项链-花型', 13, 'RB-RB-003', '18K玫瑰金', '红宝石', 2.00, '鸽血红', NULL, NULL, 'GRS', 95000.00, 45000.00, 3, 2, '红宝石花朵项链'),
('钻石手链-永恒款', 12, 'RG-DIA-005', 'PT950铂金', '钻石', 2.00, 'E', 'VS1', 'EX', 'GIA', 88000.00, 42000.00, 5, 3, '永恒款钻石手链'),
('珍珠手链-Akoya', 21, 'PRL-AK-002', '18K黄金', 'Akoya珍珠', NULL, '白色', NULL, NULL, 'GIA', 6800.00, 3200.00, 20, 8, 'Akoya珍珠手链'),
('翡翠戒指-蛋面', 18, 'JG-DM-001', '18K黄金', '翡翠', NULL, '阳绿', 'A货', NULL, 'NGTC', 38000.00, 18000.00, 5, 2, '冰种阳绿翡翠蛋面戒指'),
('黄金耳环-古法', 17, 'GL-EH-001', 'Au999', NULL, NULL, NULL, NULL, NULL, 'NGTC', 8800.00, 4500.00, 25, 8, '古法黄金耳环'),
('定制手镯-刻字', 7, 'CT-HZ-001', '18K玫瑰金', '钻石', 0.50, 'F', 'VS2', 'VG', 'NGTC', 25000.00, 12000.00, 8, 3, '定制刻字钻石手镯'),
('祖母绿耳环', 15, 'RB-EM-003', '18K白金', '祖母绿', 1.50, '沃顿绿', NULL, NULL, 'GIA', 48000.00, 23000.00, 4, 2, '祖母绿钻石耳环'),
('腕表-欧米茄海马', 6, 'WP-OMEGA-001', '钢', NULL, NULL, NULL, NULL, NULL, NULL, 45000.00, 32000.00, 5, 2, '欧米茄海马系列'),
('腕表-万国葡萄牙', 6, 'WP-IW-001', '18K玫瑰金', NULL, NULL, NULL, NULL, NULL, NULL, 88000.00, 65000.00, 2, 1, '万国葡萄牙系列'),
('配饰-发饰-钻石', 8, 'ACC-FS-001', '18K白金', '钻石', 0.20, NULL, NULL, NULL, NULL, 5600.00, 2600.00, 10, 5, '钻石发饰'),
('礼品-周年纪念', 9, 'GFT-ANN-001', '18K黄金', '珍珠', NULL, '粉色', NULL, NULL, NULL, 15800.00, 7500.00, 10, 5, '周年纪念珠宝礼盒'),
('古董项链-爱德华', 10, 'VTG-NL-001', '铂金', '钻石', 1.50, NULL, NULL, NULL, NULL, 125000.00, 55000.00, 1, 1, '爱德华时期钻石项链'),
('投资金币-熊猫', 16, 'GL-COIN-001', 'Au999', NULL, NULL, NULL, NULL, NULL, 'SGP', 8800.00, 8200.00, 50, 15, '中国熊猫金币30克'),
('翡翠挂件-如意', 18, 'JG-RY-001', NULL, '翡翠', NULL, '飘花', 'A货', NULL, 'NGTC', 18000.00, 8500.00, 10, 5, '飘花翡翠如意挂件'),
('黄金手链-五福绳', 17, 'GL-SH-001', 'Au999', NULL, NULL, NULL, NULL, NULL, 'NGTC', 6500.00, 3500.00, 30, 10, '古法金五福手链'),
('定制耳环-彩宝', 7, 'CT-EH-001', '18K白金', '蓝宝石', 1.00, '皇家蓝', NULL, NULL, 'GRS', 35000.00, 16500.00, 5, 2, '定制蓝宝石耳环');
 
-- ============================================================
-- 3. 客户数据(30人)
-- ============================================================
INSERT INTO customer (customer_no, name, phone, email, membership_level, total_spent, address) VALUES
('CUST001', '张伟', '13800138001', 'zhangwei@email.com', '钻石会员', 158000.00, '深圳市南山区科技园'),
('CUST002', '李娜', '13800138002', 'lina@email.com', '白金会员', 89500.00, '深圳市福田区CBD'),
('CUST003', '王磊', '13800138003', 'wanglei@email.com', '黄金会员', 45000.00, '广州市天河区'),
('CUST004', '赵敏', '13800138004', 'zhaomin@email.com', '钻石会员', 210000.00, '上海市静安区'),
('CUST005', '陈静', '13800138005', 'chenjing@email.com', '白金会员', 67800.00, '北京市朝阳区'),
('CUST006', '刘洋', '13800138006', 'liuyang@email.com', '黄金会员', 32000.00, '杭州市西湖区'),
('CUST007', '杨帆', '13800138007', 'yangfan@email.com', '普通', 8500.00, '成都市锦江区'),
('CUST008', '黄丽', '13800138008', 'huangli@email.com', '白金会员', 95000.00, '南京市鼓楼区'),
('CUST009', '周强', '13800138009', 'zhouqiang@email.com', '黄金会员', 52000.00, '武汉市武昌区'),
('CUST010', '吴婷', '13800138010', 'wuting@email.com', '钻石会员', 178000.00, '深圳市罗湖区'),
('CUST011', '郑华', '13800138011', 'zhenghua@email.com', '白金会员', 76000.00, '苏州市姑苏区'),
('CUST012', '孙鹏', '13800138012', 'sunpeng@email.com', '普通', 5600.00, '重庆市渝中区'),
('CUST013', '马莉', '13800138013', 'mali@email.com', '黄金会员', 41000.00, '西安市雁塔区'),
('CUST014', '朱军', '13800138014', 'zhujun@email.com', '白金会员', 88000.00, '长沙市天心区'),
('CUST015', '胡敏', '13800138015', 'humin@email.com', '钻石会员', 195000.00, '深圳市福田区'),
('CUST016', '林涛', '13800138016', 'lintao@email.com', '黄金会员', 38000.00, '厦门市思明区'),
('CUST017', '何雪', '13800138017', 'hexue@email.com', '白金会员', 72000.00, '成都市高新区'),
('CUST018', '罗明', '13800138018', 'luoming@email.com', '普通', 3200.00, '重庆市沙坪坝区'),
('CUST019', '梁芳', '13800138019', 'liangfang@email.com', '黄金会员', 48000.00, '昆明市五华区'),
('CUST020', '宋刚', '13800138020', 'songgang@email.com', '白金会员', 85000.00, '济南市历下区'),
('CUST021', '谢红', '13800138021', 'xiehong@email.com', '钻石会员', 168000.00, '深圳市南山区'),
('CUST022', '唐磊', '13800138022', 'tanglei@email.com', '普通', 6800.00, '合肥市包河区'),
('CUST023', '韩雪', '13800138023', 'hanxue@email.com', '白金会员', 92000.00, '南京市秦淮区'),
('CUST024', '冯伟', '13800138024', 'fengwei@email.com', '黄金会员', 35000.00, '武汉市洪山区'),
('CUST025', '蒋莉', '13800138025', 'jiangli@email.com', '钻石会员', 225000.00, '上海市徐汇区'),
('CUST026', '沈涛', '13800138026', 'shentao@email.com', '普通', 4500.00, '天津市和平区'),
('CUST027', '姚芳', '13800138027', 'yaofang@email.com', '黄金会员', 55000.00, '杭州市滨江区'),
('CUST028', '叶军', '13800138028', 'yejun@email.com', '白金会员', 78000.00, '广州市越秀区'),
('CUST029', '秦秀', '13800138029', 'qinxiu@email.com', '钻石会员', 185000.00, '北京市海淀区'),
('CUST030', '戴明', '13800138030', 'daiming@email.com', '黄金会员', 42000.00, '成都市武侯区');
 
-- ============================================================
-- 4. 账户数据
-- ============================================================
INSERT INTO account (customer_id, balance, frozen_amount, total_recharge, total_consume) VALUES
(1, 58000.00, 0, 250000.00, 158000.00),
(2, 32000.00, 8800.00, 150000.00, 89500.00),
(3, 15000.00, 0, 80000.00, 45000.00),
(4, 85000.00, 0, 350000.00, 210000.00),
(5, 28000.00, 15000.00, 120000.00, 67800.00),
(6, 12000.00, 0, 60000.00, 32000.00),
(7, 3000.00, 0, 15000.00, 8500.00),
(8, 45000.00, 0, 180000.00, 95000.00),
(9, 18000.00, 5000.00, 90000.00, 52000.00),
(10, 72000.00, 0, 300000.00, 178000.00),
(11, 35000.00, 0, 140000.00, 76000.00),
(12, 2000.00, 0, 10000.00, 5600.00),
(13, 16000.00, 0, 75000.00, 41000.00),
(14, 42000.00, 12000.00, 160000.00, 88000.00),
(15, 68000.00, 0, 280000.00, 195000.00),
(16, 14000.00, 0, 65000.00, 38000.00),
(17, 38000.00, 0, 150000.00, 72000.00),
(18, 1500.00, 0, 8000.00, 3200.00),
(19, 22000.00, 0, 95000.00, 48000.00),
(20, 48000.00, 0, 170000.00, 85000.00),
(21, 65000.00, 0, 260000.00, 168000.00),
(22, 2500.00, 0, 12000.00, 6800.00),
(23, 52000.00, 0, 190000.00, 92000.00),
(24, 16000.00, 3000.00, 70000.00, 35000.00),
(25, 95000.00, 0, 380000.00, 225000.00),
(26, 1800.00, 0, 9000.00, 4500.00),
(27, 28000.00, 0, 110000.00, 55000.00),
(28, 45000.00, 0, 165000.00, 78000.00),
(29, 78000.00, 0, 310000.00, 185000.00),
(30, 19000.00, 0, 85000.00, 42000.00);
 
-- ============================================================
-- 5. 库存数据
-- ============================================================
INSERT INTO inventory (product_id, warehouse, quantity, reserved_qty) VALUES
(1, '主仓库', 15, 3), (2, '主仓库', 20, 5), (3, '主仓库', 8, 2),
(4, '主仓库', 50, 10), (5, '主仓库', 25, 5), (6, '主仓库', 5, 2),
(7, '主仓库', 12, 3), (8, '主仓库', 3, 1), (9, '主仓库', 10, 2),
(10, '主仓库', 6, 2), (11, '主仓库', 8, 1), (12, '主仓库', 30, 5),
(13, '主仓库', 40, 8), (14, '主仓库', 15, 3), (15, '主仓库', 2, 1),
(16, '主仓库', 5, 1), (17, '主仓库', 8, 2), (18, '主仓库', 3, 1),
(19, '主仓库', 10, 3), (20, '主仓库', 1, 0), (21, '主仓库', 2, 1),
(22, '主仓库', 3, 1), (23, '主仓库', 5, 2), (24, '主仓库', 2, 1),
(25, '主仓库', 8, 3), (26, '主仓库', 100, 20), (27, '主仓库', 30, 5),
(28, '主仓库', 10, 3), (29, '主仓库', 15, 5), (30, '主仓库', 10, 3),
(31, '主仓库', 5, 2), (32, '主仓库', 50, 10), (33, '主仓库', 25, 5),
(34, '主仓库', 20, 5), (35, '主仓库', 8, 2), (36, '主仓库', 5, 1),
(37, '主仓库', 3, 1), (38, '主仓库', 10, 3), (39, '主仓库', 15, 5),
(40, '主仓库', 20, 5), (41, '主仓库', 5, 2), (42, '主仓库', 10, 3),
(43, '主仓库', 2, 1), (44, '主仓库', 50, 10), (45, '主仓库', 10, 3),
(46, '主仓库', 15, 5), (47, '主仓库', 10, 3), (48, '主仓库', 5, 2),
(49, '主仓库', 30, 8), (50, '主仓库', 8, 2);
 
-- ============================================================
-- 6. 订单数据(30单)
-- ============================================================
INSERT INTO "order" (order_no, customer_id, status, payment_method, total_amount, discount_amount, actual_amount, shipping_addr, payment_time, shipped_time, received_time) VALUES
('ORD20260901001', 1, '已收货', '信用卡', 58800.00, 2000.00, 56800.00, '深圳市南山区科技园1号', '2026-09-01 10:30:00+08', '2026-09-02 14:00:00+08', '2026-09-05 09:00:00+08'),
('ORD20260901002', 2, '已发货', '支付宝', 35600.00, 1000.00, 34600.00, '深圳市福田区CBD大厦', '2026-09-01 14:20:00+08', '2026-09-02 16:00:00+08', NULL),
('ORD20260902001', 3, '已支付', '微信支付', 18500.00, 0, 18500.00, '广州市天河区体育西路', '2026-09-02 09:15:00+08', NULL, NULL),
('ORD20260902002', 4, '已收货', '银行转账', 85000.00, 5000.00, 80000.00, '上海市静安区南京西路', '2026-09-02 11:00:00+08', '2026-09-03 10:00:00+08', '2026-09-06 15:00:00+08'),
('ORD20260903001', 5, '已收货', '信用卡', 28000.00, 800.00, 27200.00, '北京市朝阳区建国路', '2026-09-03 15:30:00+08', '2026-09-04 09:00:00+08', '2026-09-07 11:00:00+08'),
('ORD20260903002', 6, '生产中', '支付宝', 12800.00, 0, 12800.00, '杭州市西湖区文三路', '2026-09-03 16:00:00+08', NULL, NULL),
('ORD20260904001', 7, '已取消', '微信支付', 8800.00, 0, 8800.00, '成都市锦江区春熙路', '2026-09-04 10:00:00+08', NULL, NULL),
('ORD20260904002', 8, '已收货', '信用卡', 68000.00, 3000.00, 65000.00, '南京市鼓楼区中山路', '2026-09-04 14:30:00+08', '2026-09-05 11:00:00+08', '2026-09-08 10:00:00+08'),
('ORD20260905001', 9, '已发货', '银行转账', 42000.00, 2000.00, 40000.00, '武汉市武昌区中南路', '2026-09-05 09:00:00+08', '2026-09-06 14:00:00+08', NULL),
('ORD20260905002', 10, '已收货', '信用卡', 128000.00, 8000.00, 120000.00, '深圳市罗湖区深南东路', '2026-09-05 11:30:00+08', '2026-09-06 09:00:00+08', '2026-09-09 16:00:00+08'),
('ORD20260906001', 11, '已支付', '支付宝', 15800.00, 0, 15800.00, '苏州市姑苏区观前街', '2026-09-06 10:00:00+08', NULL, NULL),
('ORD20260906002', 12, '待支付', '微信支付', 5600.00, 0, 5600.00, '重庆市渝中区解放碑', '2026-09-06 15:00:00+08', NULL, NULL),
('ORD20260907001', 13, '已收货', '信用卡', 32000.00, 1500.00, 30500.00, '西安市雁塔区高新路', '2026-09-07 09:30:00+08', '2026-09-08 10:00:00+08', '2026-09-11 14:00:00+08'),
('ORD20260907002', 14, '已发货', '支付宝', 45000.00, 2000.00, 43000.00, '长沙市天心区黄兴路', '2026-09-07 14:00:00+08', '2026-09-08 16:00:00+08', NULL),
('ORD20260908001', 15, '已收货', '银行转账', 95000.00, 5000.00, 90000.00, '深圳市福田区中心区', '2026-09-08 10:00:00+08', '2026-09-09 09:00:00+08', '2026-09-12 11:00:00+08'),
('ORD20260908002', 16, '已支付', '信用卡', 8800.00, 0, 8800.00, '厦门市思明区中山路', '2026-09-08 15:30:00+08', NULL, NULL),
('ORD20260909001', 17, '已收货', '支付宝', 55000.00, 3000.00, 52000.00, '成都市高新区天府大道', '2026-09-09 09:00:00+08', '2026-09-10 10:00:00+08', '2026-09-13 15:00:00+08'),
('ORD20260909002', 18, '待支付', '微信支付', 3200.00, 0, 3200.00, '重庆市沙坪坝区三峡广场', '2026-09-09 16:00:00+08', NULL, NULL),
('ORD20260910001', 19, '已发货', '信用卡', 28000.00, 1000.00, 27000.00, '昆明市五华区翠湖路', '2026-09-10 10:30:00+08', '2026-09-11 14:00:00+08', NULL),
('ORD20260910002', 20, '已收货', '银行转账', 48000.00, 2500.00, 45500.00, '济南市历下区泉城路', '2026-09-10 14:00:00+08', '2026-09-11 09:00:00+08', '2026-09-14 10:00:00+08'),
('ORD20260911001', 21, '已收货', '信用卡', 72000.00, 4000.00, 68000.00, '深圳市南山区科技园2号', '2026-09-11 09:00:00+08', '2026-09-12 10:00:00+08', '2026-09-15 14:00:00+08'),
('ORD20260911002', 22, '已取消', '支付宝', 6800.00, 0, 6800.00, '合肥市包河区徽州大道', '2026-09-11 15:00:00+08', NULL, NULL),
('ORD20260912001', 23, '已收货', '信用卡', 65000.00, 3500.00, 61500.00, '南京市秦淮区夫子庙', '2026-09-12 10:00:00+08', '2026-09-13 09:00:00+08', '2026-09-16 11:00:00+08'),
('ORD20260912002', 24, '生产中', '微信支付', 18000.00, 0, 18000.00, '武汉市洪山区光谷广场', '2026-09-12 14:30:00+08', NULL, NULL),
('ORD20260913001', 25, '已收货', '银行转账', 158000.00, 10000.00, 148000.00, '上海市徐汇区淮海路', '2026-09-13 09:00:00+08', '2026-09-14 10:00:00+08', '2026-09-17 15:00:00+08'),
('ORD20260913002', 26, '待支付', '支付宝', 4500.00, 0, 4500.00, '天津市和平区南京路', '2026-09-13 16:00:00+08', NULL, NULL),
('ORD20260914001', 27, '已发货', '信用卡', 38000.00, 2000.00, 36000.00, '杭州市滨江区江南大道', '2026-09-14 10:00:00+08', '2026-09-15 14:00:00+08', NULL),
('ORD20260914002', 28, '已收货', '支付宝', 52000.00, 2500.00, 49500.00, '广州市越秀区北京路', '2026-09-14 14:00:00+08', '2026-09-15 09:00:00+08', '2026-09-18 10:00:00+08'),
('ORD20260915001', 29, '已收货', '信用卡', 88000.00, 5000.00, 83000.00, '北京市海淀区中关村', '2026-09-15 09:30:00+08', '2026-09-16 10:00:00+08', '2026-09-19 11:00:00+08'),
('ORD20260915002', 30, '已支付', '微信支付', 15000.00, 0, 15000.00, '成都市武侯区科华北路', '2026-09-15 15:00:00+08', NULL, NULL);
 
-- ============================================================
-- 7. 订单明细数据
-- ============================================================
INSERT INTO order_item (order_id, product_id, product_name, product_sku, quantity, unit_price, subtotal) VALUES
(1, 1, '经典六爪钻戒-1克拉', 'RG-DIA-001', 1, 58800.00, 58800.00),
(2, 2, 'Halo光环钻戒-0.8克拉', 'RG-DIA-002', 1, 35600.00, 35600.00),
(3, 5, '扭臂钻戒-0.5克拉', 'RG-DIA-004', 1, 18500.00, 18500.00),
(4, 8, '蓝宝石戒指-2克拉', 'RB-SA-001', 1, 85000.00, 85000.00),
(5, 19, '南洋珍珠项链', 'PRL-NL-001', 1, 28000.00, 28000.00),
(6, 26, '时尚耳环-锆石', 'ACC-ZR-001', 1, 1280.00, 1280.00),
(7, 4, '素圈戒指-3mm', 'RG-SU-001', 1, 8800.00, 8800.00),
(8, 15, '缅甸翡翠手镯', 'JG-HZ-001', 1, 128000.00, 128000.00),
(9, 9, '蓝宝石吊坠-1.2克拉', 'RB-SA-002', 1, 42000.00, 42000.00),
(10, 22, '百达翡丽鹦鹉螺', 'WP-PATEK-001', 1, 285000.00, 285000.00),
(11, 32, '黄金项链-肖邦链', 'GL-NL-001', 1, 15800.00, 15800.00),
(12, 18, 'Akoya珍珠耳环', 'PRL-AK-001', 1, 12800.00, 12800.00),
(13, 16, '缅甸翡翠手镯', 'JG-HZ-001', 1, 128000.00, 128000.00),
(14, 10, '祖母绿戒指-1克拉', 'RB-EM-001', 1, 55000.00, 55000.00),
(15, 23, '劳力士迪通拿', 'WP-ROLEX-001', 1, 168000.00, 168000.00),
(16, 27, '黄金耳环-古法', 'GL-EH-001', 1, 8800.00, 8800.00),
(17, 14, '黄金手镯-古法金', 'GL-HZ-001', 1, 32000.00, 32000.00),
(18, 1, '经典六爪钻戒-1克拉', 'RG-DIA-001', 1, 58800.00, 58800.00),
(19, 33, '蓝宝石手链', 'RB-SA-003', 1, 158000.00, 158000.00),
(20, 11, '祖母绿项链-0.8克拉', 'RB-EM-002', 1, 32000.00, 32000.00),
(21, 6, '红宝戒指-1.5克拉', 'RB-RB-001', 1, 68000.00, 68000.00),
(22, 3, '三石钻戒-1.2克拉', 'RG-DIA-003', 1, 72000.00, 72000.00),
(23, 24, '卡地亚坦克', 'WP-CARTIER-001', 1, 98000.00, 98000.00),
(24, 35, '钻石手链-永恒款', 'RG-DIA-005', 1, 88000.00, 88000.00),
(25, 20, '百达翡丽鹦鹉螺', 'WP-PATEK-001', 1, 285000.00, 285000.00),
(26, 44, '投资金币-熊猫', 'GL-COIN-001', 1, 8800.00, 8800.00),
(27, 38, '翡翠戒指-蛋面', 'JG-DM-001', 1, 38000.00, 38000.00),
(28, 7, '红宝耳环-0.8克拉', 'RB-RB-002', 1, 25600.00, 25600.00),
(29, 25, '定制钻戒-异形钻', 'CT-DIA-001', 1, 128000.00, 128000.00),
(30, 46, '翡翠挂件-如意', 'JG-RY-001', 1, 18000.00, 18000.00);
 
-- ============================================================
-- 8. 库存日志数据
-- ============================================================
INSERT INTO inventory_log (product_id, warehouse, change_type, change_qty, before_qty, after_qty, related_order_id, related_order_no, operator) VALUES
(1, '主仓库', 'stock_out', 1, 16, 15, 1, 'ORD20260901001', 'SYSTEM'),
(2, '主仓库', 'stock_out', 1, 21, 20, 2, 'ORD20260901002', 'SYSTEM'),
(5, '主仓库', 'stock_out', 1, 26, 25, 3, 'ORD20260902001', 'SYSTEM'),
(8, '主仓库', 'stock_out', 1, 4, 3, 4, 'ORD20260902002', 'SYSTEM'),
(19, '主仓库', 'stock_out', 1, 11, 10, 5, 'ORD20260903001', 'SYSTEM'),
(26, '主仓库', 'stock_out', 1, 101, 100, 6, 'ORD20260903002', 'SYSTEM'),
(4, '主仓库', 'release', 1, 9, 10, 7, 'ORD20260904001', 'SYSTEM'),
(15, '主仓库', 'stock_out', 1, 3, 2, 8, 'ORD20260904002', 'SYSTEM'),
(9, '主仓库', 'stock_out', 1, 11, 10, 9, 'ORD20260905001', 'SYSTEM'),
(22, '主仓库', 'stock_out', 1, 2, 1, 10, 'ORD20260905002', 'SYSTEM'),
(32, '主仓库', 'stock_out', 1, 51, 50, 11, 'ORD20260906001', 'SYSTEM'),
(18, '主仓库', 'reserve', 1, 11, 10, 12, 'ORD20260906002', 'SYSTEM'),
(16, '主仓库', 'stock_out', 1, 3, 2, 13, 'ORD20260907001', 'SYSTEM'),
(10, '主仓库', 'stock_out', 1, 7, 6, 14, 'ORD20260907002', 'SYSTEM'),
(23, '主仓库', 'stock_out', 1, 3, 2, 15, 'ORD20260908001', 'SYSTEM'),
(27, '主仓库', 'stock_out', 1, 21, 20, 16, 'ORD20260908002', 'SYSTEM'),
(14, '主仓库', 'stock_out', 1, 16, 15, 17, 'ORD20260909001', 'SYSTEM'),
(1, '主仓库', 'reserve', 1, 15, 14, 18, 'ORD20260909002', 'SYSTEM'),
(33, '主仓库', 'stock_out', 1, 4, 3, 19, 'ORD20260910001', 'SYSTEM'),
(11, '主仓库', 'stock_out', 1, 9, 8, 20, 'ORD20260910002', 'SYSTEM'),
(6, '主仓库', 'stock_out', 1, 6, 5, 21, 'ORD20260911001', 'SYSTEM'),
(3, '主仓库', 'stock_out', 1, 3, 2, 22, 'ORD20260911002', 'SYSTEM'),
(24, '主仓库', 'stock_out', 1, 6, 5, 23, 'ORD20260912001', 'SYSTEM'),
(35, '主仓库', 'stock_out', 1, 9, 8, 24, 'ORD20260912002', 'SYSTEM'),
(20, '主仓库', 'stock_out', 1, 2, 1, 25, 'ORD20260913001', 'SYSTEM'),
(44, '主仓库', 'stock_out', 1, 51, 50, 26, 'ORD20260913002', 'SYSTEM'),
(38, '主仓库', 'stock_out', 1, 9, 8, 27, 'ORD20260914001', 'SYSTEM'),
(7, '主仓库', 'stock_out', 1, 13, 12, 28, 'ORD20260914002', 'SYSTEM'),
(25, '主仓库', 'stock_out', 1, 3, 2, 29, 'ORD20260915001', 'SYSTEM'),
(46, '主仓库', 'stock_out', 1, 16, 15, 30, 'ORD20260915002', 'SYSTEM');
 
-- ============================================================
-- 9. Saga事务日志示例数据
-- ============================================================
INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload, retry_count, max_retries) VALUES
('a0000000-0000-0000-0000-000000000001'::uuid, 'create_order', 1, 'SUCCEEDED',
 '{"order_no":"ORD-SAGA-001","customer_id":1,"total_amount":58800}',
 '{"order_id":101,"order_no":"ORD-SAGA-001"}', 0, 3),
('a0000000-0000-0000-0000-000000000001'::uuid, 'reserve_stock', 2, 'SUCCEEDED',
 '{"product_id":1,"quantity":1,"warehouse":"主仓库"}',
 '{"reserved":true,"product_id":1,"reserved_qty":1}', 0, 3),
('a0000000-0000-0000-0000-000000000001'::uuid, 'deduct_balance', 3, 'SUCCEEDED',
 '{"customer_id":1,"amount":58800}',
 '{"balance":58000,"customer_id":1}', 0, 3),
('a0000000-0000-0000-0000-000000000001'::uuid, 'confirm_payment', 4, 'SUCCEEDED',
 '{"order_id":101,"amount":58800,"payment_method":"信用卡"}',
 '{"payment_id":"PAY-001","status":"confirmed"}', 0, 3),
('a0000000-0000-0000-0000-000000000002'::uuid, 'create_order', 1, 'SUCCEEDED',
 '{"order_no":"ORD-SAGA-002","customer_id":2,"total_amount":35600}',
 '{"order_id":102,"order_no":"ORD-SAGA-002"}', 0, 3),
('a0000000-0000-0000-0000-000000000002'::uuid, 'reserve_stock', 2, 'FAILED',
 '{"product_id":2,"quantity":1,"warehouse":"主仓库"}',
 '{"error":"库存不足"}', 1, 3),
('a0000000-0000-0000-0000-000000000002'::uuid, 'cancel_order', 3, 'COMPENSATING',
 '{"order_id":102,"reason":"库存不足"}',
 '{"cancelled":true}', 0, 3);
 
-- ============================================================
-- 10. 死锁演示数据
-- ============================================================
INSERT INTO deadlock_demo (account_a, account_b, amount, description) VALUES
(1, 2, 5000.00, '演示死锁:事务A锁账户1->账户2,事务B锁账户2->账户1'),
(3, 4, 3000.00, '演示死锁:转账方向相反导致循环等待'),
(5, 6, 8000.00, '演示死锁:多行锁顺序不一致'),
(7, 8, 2000.00, '演示死锁:库存预占顺序不一致'),
(9, 10, 15000.00, '演示死锁:订单与库存交叉锁定');
 
-- ============================================================
-- 验证数据完整性
-- ============================================================
SELECT 'category' AS table_name, COUNT(*) AS row_count FROM category
UNION ALL SELECT 'product', COUNT(*) FROM product
UNION ALL SELECT 'customer', COUNT(*) FROM customer
UNION ALL SELECT 'order', COUNT(*) FROM "order"
UNION ALL SELECT 'order_item', COUNT(*) FROM order_item
UNION ALL SELECT 'inventory', COUNT(*) FROM inventory
UNION ALL SELECT 'account', COUNT(*) FROM account
UNION ALL SELECT 'inventory_log', COUNT(*) FROM inventory_log
UNION ALL SELECT 'saga_log', COUNT(*) FROM saga_log
UNION ALL SELECT 'deadlock_demo', COUNT(*) FROM deadlock_demo;
 
-- 验证库存一致性
SELECT p.name, p.stock_quantity, COALESCE(i.quantity, 0) AS inv_quantity
FROM product p
LEFT JOIN inventory i ON p.id = i.product_id
WHERE p.stock_quantity <> COALESCE(i.quantity, 0);
 
-- 验证订单总额
SELECT o.order_no, o.actual_amount, COALESCE(SUM(oi.subtotal), 0) AS items_total
FROM "order" o
LEFT JOIN order_item oi ON o.id = oi.order_id
GROUP BY o.id, o.order_no, o.actual_amount
HAVING ABS(o.actual_amount - COALESCE(SUM(oi.subtotal), 0)) > 0.01;
 
\echo 'Data insertion completed successfully!'
 
 
-- ============================================================
-- 模块: pg18_acid_isolation.sql
-- ============================================================
 
-- ============================================================
-- PostgreSQL 18 珠宝行业 - ACID事务控制与隔离级别演示
-- 事务与并发模式实战
-- ============================================================
 
\c jewelry_db
 
-- ============================================================
-- 1. ACID 事务控制基础
-- ============================================================
 
-- 1.1 基本事务:珠宝转账(从一个客户账户转到另一个客户账户)
-- 演示原子性(A):要么全部成功,要么全部回滚
BEGIN;
 
-- 转出方扣款
UPDATE account
SET balance = balance - 5000.00,
    total_consume = total_consume + 5000.00,
    version = version + 1,
    updated_at = NOW()
WHERE customer_id = 1
  AND balance >= 5000.00;  -- 防止超扣
 
-- 转入方加款
UPDATE account
SET balance = balance + 5000.00,
    version = version + 1,
    updated_at = NOW()
WHERE customer_id = 2;
 
-- 记录交易日志
INSERT INTO inventory_log (product_id, warehouse, change_type, change_qty, before_qty, after_qty, related_order_id, related_order_no, operator)
VALUES (0, '资金', 'transfer_out', 5000, 0, 0, NULL, 'TXF-001', 'SYSTEM');
 
-- 验证转账结果
SELECT customer_id, balance FROM account WHERE customer_id IN (1, 2);
 
-- 如果上面验证正确,提交;否则回滚
COMMIT;  -- 或 ROLLBACK;
 
-- 1.2 带保存点的事务嵌套
BEGIN;
 
-- 第一步:创建订单
INSERT INTO "order" (order_no, customer_id, status, payment_method, total_amount, discount_amount, actual_amount, shipping_addr)
VALUES ('ORD-ACID-001', 1, '待支付', '信用卡', 58800.00, 0, 58800.00, '深圳市南山区科技园');
 
SAVEPOINT sp_order_created;
 
-- 第二步:扣减库存
UPDATE inventory
SET reserved_qty = reserved_qty + 1,
    last_sync_time = NOW()
WHERE product_id = 1
  AND available_qty >= 1;
 
-- 第三步:冻结资金
UPDATE account
SET frozen_amount = frozen_amount + 58800.00,
    version = version + 1,
    updated_at = NOW()
WHERE customer_id = 1
  AND balance >= 58800.00;
 
-- 模拟业务异常,回滚到保存点
-- ROLLBACK TO SAVEPOINT sp_order_created;
-- 然后可以选择继续或回滚整个事务
 
-- 完成所有步骤后提交
COMMIT;
 
-- 1.3 异常处理:使用DO块模拟存储过程式事务
DO $$
DECLARE
    v_order_id INT;
    v_stock INT;
    v_balance DECIMAL(14,2);
BEGIN
    -- 开始事务(DO块自动在事务中执行)
     
    -- 检查库存
    SELECT stock_quantity INTO v_stock FROM product WHERE id = 1 FOR UPDATE;
     
    -- 检查余额
    SELECT balance INTO v_balance FROM account WHERE customer_id = 1 FOR UPDATE;
     
    -- 业务规则校验
    IF v_stock < 1 THEN
        RAISE EXCEPTION '库存不足,无法完成订单';
    END IF;
     
    IF v_balance < 58800.00 THEN
        RAISE EXCEPTION '账户余额不足,当前余额: %', v_balance;
    END IF;
     
    -- 执行订单创建
    INSERT INTO "order" (order_no, customer_id, status, payment_method, total_amount, actual_amount, shipping_addr)
    VALUES ('ORD-ACID-002', 1, '已支付', '信用卡', 58800.00, 58800.00, '深圳市南山区科技园')
    RETURNING id INTO v_order_id;
     
    -- 扣减库存
    UPDATE inventory SET quantity = quantity - 1, last_sync_time = NOW() WHERE product_id = 1;
     
    -- 扣除余额
    UPDATE account SET balance = balance - 58800.00, total_consume = total_consume + 58800.00, version = version + 1, updated_at = NOW() WHERE customer_id = 1;
     
    -- 记录日志
    INSERT INTO inventory_log (product_id, warehouse, change_type, change_qty, before_qty, after_qty, related_order_id, related_order_no, operator)
    VALUES (1, '主仓库', 'stock_out', 1, v_stock, v_stock - 1, v_order_id, 'ORD-ACID-002', 'SYSTEM');
     
    -- 提交(DO块结束时自动提交)
    RAISE NOTICE '订单创建成功: ORD-ACID-002';
     
EXCEPTION
    WHEN OTHERS THEN
        -- 异常时自动回滚(DO块结束时自动回滚未提交的事务)
        RAISE NOTICE '事务回滚,错误: %', SQLERRM;
        RAISE NOTICE '错误码: %', SQLSTATE;
END $$;
 
-- ============================================================
-- 2. PostgreSQL 四种隔离级别演示
-- ============================================================
 
-- 2.1 READ UNCOMMITTED(PostgreSQL中等同于READ COMMITTED)
-- PostgreSQL不支持真正的READ UNCOMMITTED,会自动升级为READ COMMITTED
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- 在PostgreSQL中,这实际上等同于READ COMMITTED
-- 可以看到其他事务已提交的数据
SELECT * FROM product WHERE id = 1;
COMMIT;
 
-- 2.2 READ COMMITTED(PostgreSQL默认隔离级别)
-- 每次查询都能看到最新已提交的数据(快照读)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- 第一次查询:看到旧值
SELECT name, stock_quantity FROM product WHERE id = 1;
-- 此时另一个事务可能更新了库存...
-- 第二次查询:看到新值(PostgreSQL的READ COMMITTED每次语句重新获取快照)
SELECT name, stock_quantity FROM product WHERE id = 1;
COMMIT;
 
-- 2.3 REPEATABLE READ(PostgreSQL中的快照隔离SI)
-- 整个事务期间看到一致的数据快照
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- 第一次查询
SELECT name, stock_quantity FROM product WHERE id = 1;
-- 此时另一个事务可能已提交更新...
-- 第二次查询:仍然看到事务开始时的快照(不会看到未提交的数据,也不会看到已提交的更新)
SELECT name, stock_quantity FROM product WHERE id = 1;
-- PostgreSQL的REPEATABLE READ使用快照隔离,不会出现不可重复读
-- 但可能遇到写偏斜(write skew),PostgreSQL会通过序列化快照隔离(SSSI)检测
 
-- 检查是否有序列化冲突
SELECT * FROM pg_stat_activity WHERE state = 'active' AND pid <> pg_backend_pid();
COMMIT;
 
-- 2.4 SERIALIZABLE(串行化隔离级别)
-- 完全串行执行,最高隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 查询库存
SELECT stock_quantity FROM product WHERE id = 1 FOR UPDATE;
-- 更新库存
UPDATE product SET stock_quantity = stock_quantity - 1 WHERE id = 1;
-- 如果此时另一个SERIALIZABLE事务也修改了同一行,会抛出序列化失败异常
COMMIT;
 
-- ============================================================
-- 3. MVCC 快照读 vs 当前读对比演示
-- ============================================================
 
-- 3.1 快照读(Snapshot Read)- 普通SELECT
-- 在REPEATABLE READ或READ COMMITTED下,SELECT不会加锁,读取的是快照数据
BEGIN;
-- 快照读:看到事务开始时的数据版本
SELECT id, name, stock_quantity, version FROM product WHERE id = 1;
-- 即使另一个事务更新了这行并提交,本事务的SELECT仍看到旧版本
-- 这是MVCC的核心机制:多版本并发控制
COMMIT;
 
-- 3.2 当前读(Current Read)- 带锁的SELECT
-- FOR UPDATE / FOR SHARE 会读取最新已提交数据并加锁
BEGIN;
-- 当前读:读取最新已提交版本并加排他锁
SELECT id, name, stock_quantity, version FROM product WHERE id = 1 FOR UPDATE;
-- 此时其他事务无法更新这行,直到本事务提交/回滚
-- 更新操作
UPDATE product SET stock_quantity = stock_quantity - 1 WHERE id = 1;
COMMIT;
 
-- 3.3 对比演示:使用pg_snapshot查看快照
BEGIN;
-- 获取当前快照
SELECT pg_export_snapshot();
-- 在另一个会话中使用此快照可以重现当前读到的数据
-- pg_export_snapshot() 返回类似 '0000:00000003-0000000A' 的快照字符串
COMMIT;
 
-- 3.4 查看活跃事务和MVCC相关信息
SELECT
    pid,
    state,
    query,
    now() - query_start AS query_duration,
    txid_current() AS current_txid
FROM pg_stat_activity
WHERE state != 'idle'
  AND pid != pg_backend_pid()
ORDER BY query_start;
 
-- 3.5 两阶段提交(2PC)演示
-- 第一阶段:准备提交
BEGIN;
-- 执行业务操作
UPDATE account SET balance = balance - 1000.00, version = version + 1 WHERE customer_id = 1;
UPDATE account SET balance = balance + 1000.00, version = version + 1 WHERE customer_id = 2;
-- 准备提交(事务进入prepared状态,释放行锁但保留事务信息)
PREPARE TRANSACTION 'tx_jewelry_transfer_001';
 
-- 第二阶段:提交或回滚prepared事务
-- 提交:
-- COMMIT PREPARED 'tx_jewelry_transfer_001';
-- 回滚:
-- ROLLBACK PREPARED 'tx_jewelry_transfer_001';
 
-- 查看prepared事务
SELECT transaction, prepared, request_user, database FROM pg_prepared_xacts;
 
-- ============================================================
-- 4. 行版本控制(乐观锁)演示
-- ============================================================
 
-- 4.1 使用version列实现乐观锁
-- 场景:两个管理员同时编辑同一个产品信息
BEGIN;
-- 读取产品和版本号
SELECT id, name, price, stock_quantity, version FROM product WHERE id = 1;
-- 假设读到 version=5, stock_quantity=15
 
-- 更新时检查版本号
UPDATE product
SET name = '经典六爪钻戒-1克拉(升级版)',
    price = 62800.00,
    version = version + 1,
    updated_at = NOW()
WHERE id = 1 AND version = 5;  -- 乐观锁条件
 
-- 检查受影响行数
-- 如果affected_rows = 1,说明更新成功
-- 如果affected_rows = 0,说明版本号已被其他事务修改,需要重试
 
-- 4.2 使用xmin系统列检测并发修改
-- xmin存储创建该行的事务ID
-- 可以通过xmin判断行是否被其他事务修改过
BEGIN;
SELECT id, xmin, name, stock_quantity FROM product WHERE id = 1;
-- 记录xmin值
-- 提交后检查xmin是否变化
COMMIT;
 
-- 4.3 CAS(Compare-And-Swap)原子扣减库存
-- 使用UPDATE的WHERE条件实现原子操作
-- 这种方式不需要显式锁,利用数据库行锁保证原子性
BEGIN;
-- 原子扣减:只有当库存>=1时才扣减,否则不更新
UPDATE inventory
SET quantity = quantity - 1,
    reserved_qty = reserved_qty - 1,
    last_sync_time = NOW()
WHERE product_id = 1
  AND quantity - reserved_qty >= 1;
 
-- 检查是否成功扣减
SELECT affected_rows = COUNT(*) FROM inventory WHERE product_id = 1 AND quantity - reserved_qty >= 0;
 
-- 如果affected_rows=0,说明库存不足,需要回滚或提示用户
COMMIT;
 
-- ============================================================
-- 5. 事务属性验证
-- ============================================================
 
-- 5.1 原子性验证:查看未提交事务
SELECT pid, state, query, txid_current() - txid_start() AS tx_duration_ms
FROM pg_stat_activity
WHERE state = 'active' AND pid != pg_backend_pid();
 
-- 5.2 一致性验证:检查数据完整性约束
-- 检查订单总额与明细总额是否一致
SELECT o.order_no, o.actual_amount, COALESCE(SUM(oi.subtotal), 0) AS items_total,
       o.actual_amount - COALESCE(SUM(oi.subtotal), 0) AS diff
FROM "order" o
LEFT JOIN order_item oi ON o.id = oi.order_id
GROUP BY o.id, o.order_no, o.actual_amount
HAVING ABS(o.actual_amount - COALESCE(SUM(oi.subtotal), 0)) > 0.01;
 
-- 检查库存一致性
SELECT p.sku, p.stock_quantity, COALESCE(i.quantity, 0) AS inv_qty,
       p.stock_quantity - COALESCE(i.quantity, 0) AS diff
FROM product p
LEFT JOIN inventory i ON p.id = i.product_id
WHERE p.stock_quantity <> COALESCE(i.quantity, 0);
 
-- 检查账户余额一致性
SELECT c.customer_no, c.name, a.balance, a.frozen_amount,
       a.balance + a.frozen_amount AS total_locked,
       a.total_recharge - a.total_consume AS should_be
FROM customer c
JOIN account a ON c.id = a.customer_id
WHERE ABS((a.balance + a.frozen_amount) - a.total_recharge + a.total_consume) > 0.01;
 
-- 5.3 隔离性验证:查看锁等待情况
SELECT
    blocked.pid AS blocked_pid,
    blocked.query AS blocked_query,
    blocking.pid AS blocking_pid,
    blocking.query AS blocking_query,
    age(now(), blocked.query_start) AS blocked_duration
FROM pg_locks blocked_lock
JOIN pg_stat_activity blocked ON blocked.pid = blocked_lock.pid
JOIN pg_locks blocking_lock ON blocking_lock.locktype = blocked_lock.locktype
    AND blocking_lock.relation = blocked_lock.relation
    AND blocking_lock.page = blocked_lock.page
    AND blocking_lock.tuple = blocked_lock.tuple
    AND blocking_lock.virtualxid = blocked_lock.virtualxid
    AND blocking_lock.database = blocked_lock.database
    AND blocking_lock.pid != blocked_lock.pid
JOIN pg_stat_activity blocking ON blocking.pid = blocking_lock.pid
WHERE NOT blocked_lock.granted
  AND blocking_lock.granted;
 
-- 5.4 持久性验证:检查WAL日志
SELECT pg_walfile_name(pg_current_wal_lsn()) AS current_wal_file;
SELECT pg_size_pretty(pg_wal_lsn_bytes(pg_current_wal_lsn())) AS current_wal_size;
 
-- ============================================================
-- 6. 事务超时与自动回滚配置
-- ============================================================
 
-- 设置事务超时(超时后自动回滚)
SET statement_timeout = '30s';  -- 语句级超时
SET lock_timeout = '10s';       -- 锁等待超时
SET idle_in_transaction_session_timeout = '60s';  -- 空闲事务超时
 
-- 查看当前会话超时设置
SHOW statement_timeout;
SHOW lock_timeout;
SHOW idle_in_transaction_session_timeout;
 
-- ============================================================
-- 完成
-- ============================================================
\echo 'ACID and isolation levels demo completed!'
 
 
-- ============================================================
-- 模块: pg18_locking.sql
-- ============================================================
 
-- ============================================================
-- PostgreSQL 18 珠宝行业 - 锁机制演示
-- 悲观锁、乐观锁、死锁检测与预防、锁监控
-- 事务与并发模式实战
-- ============================================================
 
\c jewelry_db
 
-- ============================================================
-- 1. PostgreSQL 锁模式总览
-- ============================================================
-- PostgreSQL锁模式(从弱到强):
-- 1. AccessShareLock      - 只读查询(SELECT)自动获取
-- 2. RowShareLock         - SELECT FOR UPDATE/SHARE
-- 3. RowExclusiveLock     - INSERT/UPDATE/DELETE自动获取
-- 4. ExclusiveLock        - 单行排他锁
-- 5. AccessExclusiveLock  - ALTER TABLE/DROP TABLE等DDL语句
 
-- ============================================================
-- 2. 悲观锁演示 - FOR UPDATE 系列
-- ============================================================
 
-- 2.1 SELECT FOR UPDATE(行排他锁)
-- 场景:客服查看订单详情并准备修改状态
BEGIN;
-- 获取订单行排他锁,其他事务无法修改该行
SELECT id, order_no, status, actual_amount
FROM "order"
WHERE id = 1
FOR UPDATE;
 
-- 此时另一个会话如果执行 UPDATE "order" SET status='已发货' WHERE id=1 将被阻塞
-- 直到本事务提交或回滚
 
-- 修改订单状态
UPDATE "order"
SET status = '已发货',
    shipped_time = NOW(),
    version = version + 1
WHERE id = 1;
 
COMMIT;
 
-- 2.2 SELECT FOR UPDATE NOWAIT(不等待,立即返回错误)
-- 场景:高并发下单场景,如果锁不住就立即失败而非等待
BEGIN;
-- 尝试获取库存行锁,如果已被锁定则立即报错
SELECT id, quantity, reserved_qty, available_qty
FROM inventory
WHERE product_id = 1
FOR UPDATE NOWAIT;
 
-- 如果获取成功,执行扣减
UPDATE inventory
SET reserved_qty = reserved_qty + 1
WHERE product_id = 1 AND available_qty >= 1;
 
COMMIT;
-- 如果NOWAIT导致错误,应用层可以重试或返回"库存紧张"提示
 
-- 2.3 SELECT FOR UPDATE WAIT n(等待指定时间)
-- 场景:允许短暂等待锁释放
BEGIN;
-- 等待最多5秒获取行锁
SET lock_timeout = '5s';
SELECT id, quantity, reserved_qty
FROM inventory
WHERE product_id = 1
FOR UPDATE;
 
-- 如果在5秒内获取到锁,继续执行
UPDATE inventory SET reserved_qty = reserved_qty + 1 WHERE product_id = 1;
 
COMMIT;
 
-- 2.4 SELECT FOR SHARE(共享锁)
-- 场景:库存盘点时读取库存但不允许他人修改
BEGIN;
-- 获取共享锁,其他事务可以读但不能修改
SELECT id, quantity, reserved_qty, available_qty
FROM inventory
WHERE product_id = 1
FOR SHARE;
 
-- 可以读取数据
SELECT id, name, stock_quantity FROM product WHERE id = 1;
 
-- 但其他事务的UPDATE会被阻塞
COMMIT;
 
-- 2.5 SELECT FOR UPDATE SKIP LOCKED(跳过已被锁定的行)
-- 场景:多消费者队列模式,多个worker处理订单
-- worker1:
BEGIN;
SELECT id, order_no, status
FROM "order"
WHERE status = '待支付'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- 如果返回了行,则处理;如果返回空,说明没有其他待处理订单
 
-- worker2同时执行也会跳过worker1锁定的行
COMMIT;
 
-- ============================================================
-- 3. 表级锁演示
-- ============================================================
 
-- 3.1 ACCESS EXCLUSIVE锁(DDL语句自动获取)
-- 以下语句会自动获取ACCESS EXCLUSIVE锁,阻塞所有其他操作
-- ALTER TABLE product ADD COLUMN new_column VARCHAR(100);
-- DROP TABLE order_item;
-- TRUNCATE TABLE inventory;
 
-- 3.2 显式表锁
BEGIN;
-- 获取表级排他锁(阻塞所有其他读写)
LOCK TABLE "order" IN ACCESS EXCLUSIVE MODE;
-- 此时其他会话无法对order表做任何操作
-- 通常不推荐在生产环境使用
 
-- 获取表级共享锁(允许并发读,阻止写)
-- LOCK TABLE product IN SHARE MODE;
 
-- 获取行共享锁(允许并发读和共享锁,阻止排他锁)
-- LOCK TABLE inventory IN ROW SHARE MODE;
 
COMMIT;
 
-- 3.3 表级锁对业务的影响演示
-- 在库存批量更新时使用表锁(谨慎使用)
BEGIN;
LOCK TABLE inventory IN EXCLUSIVE MODE;
-- 批量更新库存
UPDATE inventory SET last_sync_time = NOW();
-- 释放锁后其他事务才能访问
COMMIT;
 
-- ============================================================
-- 4. 乐观锁实现方式
-- ============================================================
 
-- 4.1 基于version列的乐观锁(最常用)
-- 场景:珠宝产品定价调整
BEGIN;
-- 第一步:读取产品和当前版本号
SELECT id, name, price, version FROM product WHERE id = 1;
-- 假设读到 version=5
 
-- 第二步:应用层计算新价格
-- 新价格 = 原价 * 1.1 = 58800 * 1.1 = 64680
 
-- 第三步:更新时检查版本号
UPDATE product
SET price = 64680.00,
    version = version + 1,
    updated_at = NOW()
WHERE id = 1 AND version = 5;
 
-- 检查受影响行数:
-- affected_rows = 1 => 更新成功
-- affected_rows = 0 => 版本已冲突,需要重试
 
COMMIT;
 
-- 4.2 基于时间戳的乐观锁
-- 场景:订单状态变更追踪
BEGIN;
-- 读取订单和更新时间戳
SELECT id, status, updated_at FROM "order" WHERE id = 1;
-- 假设读到 updated_at = '2026-09-19 10:00:00'
 
-- 业务处理...
 
-- 更新时检查时间戳
UPDATE "order"
SET status = '已发货',
    shipped_time = NOW(),
    version = version + 1
WHERE id = 1 AND updated_at = '2026-09-19 10:00:00';
 
COMMIT;
 
-- 4.3 CAS(Compare-And-Swap)原子操作
-- 场景:库存预占(下单时预占,支付时扣减,超时释放)
-- 方法1:使用WHERE条件实现原子检查
BEGIN;
-- 原子预占库存:检查可用库存>=1并预占
UPDATE inventory
SET reserved_qty = reserved_qty + 1,
    last_sync_time = NOW()
WHERE product_id = 1
  AND (quantity - reserved_qty) >= 1;
 
-- 检查是否预占成功
-- GET DIAGNOSTICS integer_variable = ROW_COUNT;
 
-- 方法2:使用RETURNING返回更新后的值
UPDATE inventory
SET reserved_qty = reserved_qty + 1,
    last_sync_time = NOW()
WHERE product_id = 1
  AND (quantity - reserved_qty) >= 1
RETURNING quantity, reserved_qty, (quantity - reserved_qty) AS available_qty;
 
COMMIT;
 
-- 4.4 乐观锁重试机制(应用层实现)
-- PostgreSQL中可以使用DO块实现带重试的乐观锁
DO $$
DECLARE
    v_version INT;
    v_max_retries INT := 3;
    v_retry_count INT := 0;
    v_success BOOLEAN := FALSE;
BEGIN
    WHILE v_retry_count < v_max_retries AND NOT v_success LOOP
        -- 读取当前版本
        SELECT version INTO v_version FROM product WHERE id = 1 FOR UPDATE;
         
        -- 执行更新(带版本检查)
        UPDATE product
        SET price = 64680.00,
            version = version + 1,
            updated_at = NOW()
        WHERE id = 1 AND version = v_version;
         
        -- 检查是否更新成功
        IF FOUND THEN
            v_success := TRUE;
        ELSE
            v_retry_count := v_retry_count + 1;
            RAISE NOTICE '版本冲突,重试第 % 次', v_retry_count;
            -- 短暂等待后重试(避免忙等)
            PERFORM pg_sleep(0.1 * v_retry_count);
        END IF;
    END LOOP;
     
    IF NOT v_success THEN
        RAISE EXCEPTION '乐观锁重试失败,无法更新产品';
    END IF;
END $$;
 
-- ============================================================
-- 5. 死锁检测与预防
-- ============================================================
 
-- 5.1 死锁产生演示(需要两个会话同时执行)
-- 会话A:
-- BEGIN;
-- UPDATE account SET balance = balance - 1000 WHERE customer_id = 1;
-- (切换到会话B执行下面语句)
-- 会话B:
-- BEGIN;
-- UPDATE account SET balance = balance + 1000 WHERE customer_id = 2;
-- (切换回会话A)
-- 会话A:
-- UPDATE account SET balance = balance + 1000 WHERE customer_id = 2;
-- (切换回会话B)
-- 会话B:
-- UPDATE account SET balance = balance - 1000 WHERE customer_id = 1;
-- -> 死锁!PostgreSQL会自动检测并回滚其中一个事务
 
-- 5.2 死锁预防策略
 
-- 策略1:统一加锁顺序
-- 所有事务按相同的顺序访问资源(如始终先锁customer_id小的)
-- 示例:转账时总是先锁余额小的账户
DO $$
DECLARE
    v_from INT := 1;
    v_to INT := 2;
    v_temp INT;
BEGIN
    -- 统一按ID升序加锁
    IF v_from > v_to THEN
        v_temp := v_from;
        v_from := v_to;
        v_to := v_temp;
    END IF;
     
    -- 先锁from再锁to
    PERFORM * FROM account WHERE customer_id = v_from FOR UPDATE;
    PERFORM * FROM account WHERE customer_id = v_to FOR UPDATE;
     
    -- 执行转账
    UPDATE account SET balance = balance - 1000 WHERE customer_id = v_from;
    UPDATE account SET balance = balance + 1000 WHERE customer_id = v_to;
END $$;
 
-- 策略2:设置锁超时,避免无限等待
SET lock_timeout = '5s';  -- 5秒内获取不到锁则报错
-- 应用层捕获错误后重试
 
-- 策略3:减少锁持有时间
-- 不要在事务中做耗时操作(如网络调用、复杂计算)
-- 尽量将业务逻辑放在数据库层面处理
 
-- 策略4:使用NOWAIT/SKIP LOCKED避免死锁
-- 如前面2.2和2.5所示
 
-- 5.3 死锁监控视图
-- 查看当前锁等待情况
SELECT
    blocked.pid AS blocked_pid,
    blocked.usename AS blocked_user,
    blocked.query AS blocked_query,
    age(now(), blocked.query_start) AS blocked_duration,
    blocking.pid AS blocking_pid,
    blocking.usename AS blocking_user,
    blocking.query AS blocking_query
FROM pg_locks blocked_lock
JOIN pg_stat_activity blocked ON blocked.pid = blocked_lock.pid
JOIN pg_locks blocking_lock ON blocking_lock.locktype = blocked_lock.locktype
    AND blocking_lock.relation = blocked_lock.relation
    AND blocking_lock.database = blocked_lock.database
    AND blocking_lock.pid != blocked_lock.pid
    AND blocking_lock.granted
JOIN pg_stat_activity blocking ON blocking.pid = blocking_lock.pid
WHERE NOT blocked_lock.granted
  AND blocked.pid != blocking.pid;
 
-- 查看当前所有锁信息
SELECT
    pid,
    locktype,
    relation::regclass AS table_name,
    mode,
    granted,
    age(now(), query_start) AS duration
FROM pg_locks
JOIN pg_stat_activity ON pg_locks.pid = pg_stat_activity.pid
WHERE pid != pg_backend_pid()
ORDER BY query_start;
 
-- 查看死锁历史(需要启用pg_stat_statements)
SELECT query, calls, mean_exec_time
FROM pg_stat_statements
WHERE query LIKE '%deadlock%'
ORDER BY calls DESC;
 
-- 查看系统级死锁统计
SELECT deadlocks FROM pg_stat_database WHERE datname = 'jewelry_db';
 
-- ============================================================
-- 6. 应用程序级锁(Advisory Lock)
-- ============================================================
 
-- PostgreSQL提供应用级锁,用于协调应用层面的并发控制
-- 适用于跨表/跨事务的协调锁
 
-- 6.1 获取应用级排他锁
SELECT pg_try_advisory_lock(hashtext('inventory_product_1'));
-- 返回true表示获取成功,false表示已被其他会话持有
 
-- 6.2 释放应用级锁
SELECT pg_advisory_unlock(hashtext('inventory_product_1'));
 
-- 6.3 使用场景:库存预占的分布式锁
-- 多个应用实例需要协调时,使用应用级锁避免重复处理
DO $$
BEGIN
    -- 尝试获取应用级锁(超时1秒)
    IF NOT pg_try_advisory_lock(hashtext('order_processing')) THEN
        RAISE NOTICE '其他实例正在处理订单,跳过本次执行';
        RETURN;
    END IF;
     
    -- 执行业务逻辑
    -- ...
     
    -- 释放锁
    PERFORM pg_advisory_unlock(hashtext('order_processing'));
END $$;
 
-- 6.4 查看应用级锁持有情况
SELECT
    pid,
    pg_get_userbyid(usesysid) AS user_name,
    locktype,
    objid AS lock_id
FROM pg_locks
WHERE locktype = 'advisory';
 
-- ============================================================
-- 7. 行级锁与间隙锁演示
-- ============================================================
 
-- 7.1 行级锁(Row Lock)
-- FOR UPDATE 只对选中的行加锁,不影响其他行
BEGIN;
-- 只锁定product_id=1的行
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE;
-- 其他行可以自由读写
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 2;  -- 不受影响
COMMIT;
 
-- 7.2 间隙锁(Gap Lock)- PostgreSQL通过唯一索引避免
-- PostgreSQL的REPEATABLE READ和SERIALIZABLE隔离级别下
-- 会使用谓词锁(Predicate Lock)来防止幻读
BEGIN;
-- 在REPEATABLE READ下,插入满足WHERE条件的行会被阻止
INSERT INTO product (name, category_id, sku, material, price)
VALUES ('测试产品', 1, 'TEST-001', '测试', 100);
-- 如果有其他事务在相同范围上有查询,可能触发序列化失败
 
COMMIT;
 
-- 7.3 使用SERIALIZABLE隔离级别防止幻读
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 查询某个分类下的产品数量
SELECT COUNT(*) FROM product WHERE category_id = 1;
-- 此时其他事务插入category_id=1的新产品会被检测
-- 如果检测到冲突,会抛出序列化失败异常
INSERT INTO product (name, category_id, sku, material, price)
VALUES ('新产品', 1, 'NEW-001', '测试', 100);
COMMIT;
-- 如果报错serialization_failure,应用层应重试整个事务
 
-- ============================================================
-- 8. 锁诊断工具
-- ============================================================
 
-- 8.1 查看当前活跃查询及锁信息
SELECT
    a.pid,
    a.usename,
    a.state,
    a.query,
    age(now(), a.query_start) AS query_age,
    a.wait_event_type,
    a.wait_event
FROM pg_stat_activity a
WHERE a.state != 'idle'
ORDER BY a.query_start;
 
-- 8.2 查看锁等待图
WITH lock_info AS (
    SELECT
        l.pid,
        l.locktype,
        l.relation::regclass AS table_name,
        l.mode,
        l.granted,
        a.query,
        a.usename
    FROM pg_locks l
    JOIN pg_stat_activity a ON l.pid = a.pid
    WHERE l.pid != pg_backend_pid()
)
SELECT
    blocked.pid AS blocked_pid,
    blocked.usename AS blocked_user,
    blocked.table_name AS locked_table,
    blocked.mode AS locked_mode,
    blocked.query AS blocked_query,
    blocker.pid AS blocker_pid,
    blocker.usename AS blocker_user,
    blocker.table_name AS blocker_table,
    blocker.mode AS blocker_mode
FROM lock_info blocked
JOIN lock_info blocker ON blocker.locktype = blocked.locktype
    AND blocker.relation = blocked.relation
    AND blocker.granted = true
    AND NOT blocked.granted
    AND blocker.pid != blocked.pid;
 
-- 8.3 强制终止长时间运行的查询
-- SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'active' AND age(now(), query_start) > interval '30 minutes';
 
-- ============================================================
-- 完成
-- ============================================================
\echo 'Locking mechanisms demo completed!'
 
 
-- ============================================================
-- 模块: pg18_deadlock_saga.sql
-- ============================================================
 
-- ============================================================
-- PostgreSQL 18 珠宝行业 - 死锁预防与Saga模式实现
-- 事务与并发模式实战
-- ============================================================
 
\c jewelry_db
 
-- ============================================================
-- 1. 死锁产生与预防策略
-- ============================================================
 
-- 1.1 死锁场景模拟(需要两个并发会话)
-- 场景:两个客服同时处理两笔转账,加锁顺序相反导致死锁
 
-- 会话A执行:
-- BEGIN;
-- UPDATE account SET balance = balance - 1000 WHERE customer_id = 1;
-- (切换到会话B)
 
-- 会话B执行:
-- BEGIN;
-- UPDATE account SET balance = balance + 1000 WHERE customer_id = 2;
-- (切换回会话A)
 
-- 会话A执行:
-- UPDATE account SET balance = balance + 1000 WHERE customer_id = 2;
-- (切换回会话B)
 
-- 会话B执行:
-- UPDATE account SET balance = balance - 1000 WHERE customer_id = 1;
-- -> 死锁检测!PostgreSQL自动回滚其中一个事务
 
-- 1.2 死锁预防:统一加锁顺序
-- 核心原则:所有事务按相同的顺序访问共享资源
CREATE OR REPLACE FUNCTION fn_safe_transfer(
    p_from_customer INT,
    p_to_customer INT,
    p_amount DECIMAL(14,2)
) RETURNS TABLE(result TEXT) AS $$
DECLARE
    v_from_id INT;
    v_to_id INT;
    v_temp INT;
BEGIN
    -- 策略1:统一按customer_id升序加锁
    IF p_from_customer > p_to_customer THEN
        v_temp := p_from_customer;
        p_from_customer := p_to_customer;
        p_to_customer := v_temp;
    END IF;
     
    -- 先锁from再锁to(固定顺序)
    SELECT id INTO v_from_id FROM account WHERE customer_id = p_from_customer FOR UPDATE;
    SELECT id INTO v_to_id FROM account WHERE customer_id = p_to_customer FOR UPDATE;
     
    -- 执行业务逻辑
    UPDATE account SET balance = balance - p_amount, version = version + 1, updated_at = NOW()
    WHERE customer_id = p_from_customer AND balance >= p_amount;
     
    UPDATE account SET balance = balance + p_amount, version = version + 1, updated_at = NOW()
    WHERE customer_id = p_to_customer;
     
    result := '转账成功';
    RETURN NEXT;
EXCEPTION
    WHEN OTHERS THEN
        result := '转账失败: ' || SQLERRM;
        RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- 测试转账函数
SELECT * FROM fn_safe_transfer(1, 2, 5000.00);
 
-- 1.3 死锁预防:使用锁超时
-- 设置合理的锁超时时间,避免事务无限等待
SET lock_timeout = '5s';
SET statement_timeout = '30s';
 
-- 1.4 死锁预防:减少锁持有时间
-- 避免在事务中执行耗时操作(HTTP调用、文件IO等)
-- 将业务逻辑尽量放在数据库存储过程中执行
 
-- 1.5 死锁监控与告警
-- 监控死锁发生频率
SELECT
    datname,
    xact_commit,
    xact_rollback,
    deadlocks,
    round(deadlocks::numeric / GREATEST(xact_commit + xact_rollback, 1) * 100, 2) AS deadlock_rate
FROM pg_stat_database
WHERE datname = 'jewelry_db';
 
-- 实时监控锁等待
SELECT
    blocked.pid AS blocked_pid,
    substring(blocked.query, 1, 80) AS blocked_query,
    blocking.pid AS blocking_pid,
    substring(blocking.query, 1, 80) AS blocking_query,
    age(now(), blocked.query_start) AS wait_duration
FROM pg_locks bl
JOIN pg_stat_activity blocked ON bl.pid = blocked.pid
JOIN pg_locks bl2 ON bl2.locktype = bl.locktype
    AND bl2.relation = bl.relation
    AND bl2.pid != bl.pid
    AND bl2.granted
JOIN pg_stat_activity blocking ON bl2.pid = blocking.pid
WHERE NOT bl.granted
  AND bl.pid != bl2.pid;
 
-- ============================================================
-- 2. Saga模式实现 - 订单创建流程
-- ============================================================
 
-- 2.1 Saga步骤定义(订单创建流程)
-- Step 1: create_order        - 创建订单(成功/失败)
-- Step 2: reserve_stock       - 预占库存(成功/失败->补偿:释放库存)
-- Step 3: deduct_balance      - 冻结资金(成功/失败->补偿:取消订单)
-- Step 4: confirm_payment     - 确认支付(成功/失败->补偿:释放资金+释放库存)
 
-- 2.2 各步骤实现
 
-- Step 1: 创建订单
CREATE OR REPLACE FUNCTION fn_saga_step_create_order(
    p_saga_id UUID,
    p_customer_id INT,
    p_product_id INT,
    p_quantity INT,
    p_total_amount DECIMAL(14,2)
) RETURNS TABLE(step_status VARCHAR(20), message TEXT) AS $$
DECLARE
    v_order_id INT;
    v_order_no VARCHAR(50);
BEGIN
    v_order_no := 'SAGA-' || to_char(NOW(), 'YYYYMMDD') || '-' || LPAD(nextval('seq_saga_id')::TEXT, 6, '0');
     
    -- 创建订单
    INSERT INTO "order" (order_no, customer_id, status, payment_method, total_amount, actual_amount, shipping_addr)
    VALUES (v_order_no, p_customer_id, '待支付', NULL, p_total_amount, p_total_amount, '默认地址')
    RETURNING id INTO v_order_id;
     
    -- 创建订单明细
    INSERT INTO order_item (order_id, product_id, product_name, product_sku, quantity, unit_price, subtotal)
    SELECT v_order_id, p_product_id, name, sku, p_quantity, price, price * p_quantity
    FROM product WHERE id = p_product_id;
     
    -- 记录Saga步骤
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload)
    VALUES (p_saga_id, 'create_order', 1, 'SUCCEEDED',
        jsonb_build_object('customer_id', p_customer_id, 'product_id', p_product_id, 'quantity', p_quantity),
        jsonb_build_object('order_id', v_order_id, 'order_no', v_order_no));
     
    step_status := 'SUCCEEDED';
    message := '订单创建成功: ' || v_order_no;
    RETURN NEXT;
EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, error_message)
        VALUES (p_saga_id, 'create_order', 1, 'FAILED',
            jsonb_build_object('customer_id', p_customer_id, 'product_id', p_product_id),
            SQLERRM);
        step_status := 'FAILED';
        message := '创建订单失败: ' || SQLERRM;
        RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- Step 2: 预占库存
CREATE OR REPLACE FUNCTION fn_saga_step_reserve_stock(
    p_saga_id UUID,
    p_product_id INT,
    p_quantity INT
) RETURNS TABLE(step_status VARCHAR(20), message TEXT) AS $$
DECLARE
    v_available INT;
BEGIN
    -- 检查并预占库存(原子操作)
    UPDATE inventory
    SET reserved_qty = reserved_qty + p_quantity,
        last_sync_time = NOW()
    WHERE product_id = p_product_id
      AND (quantity - reserved_qty) >= p_quantity
    RETURNING (quantity - reserved_qty) INTO v_available;
     
    IF NOT FOUND THEN
        INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, error_message)
        VALUES (p_saga_id, 'reserve_stock', 2, 'FAILED',
            jsonb_build_object('product_id', p_product_id, 'quantity', p_quantity),
            '库存不足');
        step_status := 'FAILED';
        message := '库存不足,无法预占';
        RETURN NEXT;
    END IF;
     
    -- 记录Saga步骤
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload)
    VALUES (p_saga_id, 'reserve_stock', 2, 'SUCCEEDED',
        jsonb_build_object('product_id', p_product_id, 'quantity', p_quantity),
        jsonb_build_object('reserved', true));
     
    step_status := 'SUCCEEDED';
    message := '库存预占成功';
    RETURN NEXT;
EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, error_message)
        VALUES (p_saga_id, 'reserve_stock', 2, 'FAILED',
            jsonb_build_object('product_id', p_product_id, 'quantity', p_quantity),
            SQLERRM);
        step_status := 'FAILED';
        message := '预占库存失败: ' || SQLERRM;
        RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- Step 3: 冻结资金
CREATE OR REPLACE FUNCTION fn_saga_step_deduct_balance(
    p_saga_id UUID,
    p_customer_id INT,
    p_amount DECIMAL(14,2)
) RETURNS TABLE(step_status VARCHAR(20), message TEXT) AS $$
BEGIN
    -- 冻结资金
    UPDATE account
    SET frozen_amount = frozen_amount + p_amount,
        version = version + 1,
        updated_at = NOW()
    WHERE customer_id = p_customer_id
      AND balance >= p_amount;
     
    IF NOT FOUND THEN
        INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, error_message)
        VALUES (p_saga_id, 'deduct_balance', 3, 'FAILED',
            jsonb_build_object('customer_id', p_customer_id, 'amount', p_amount),
            '账户余额不足');
        step_status := 'FAILED';
        message := '账户余额不足';
        RETURN NEXT;
    END IF;
     
    -- 记录Saga步骤
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload)
    VALUES (p_saga_id, 'deduct_balance', 3, 'SUCCEEDED',
        jsonb_build_object('customer_id', p_customer_id, 'amount', p_amount),
        jsonb_build_object('frozen', true));
     
    step_status := 'SUCCEEDED';
    message := '资金冻结成功';
    RETURN NEXT;
EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, error_message)
        VALUES (p_saga_id, 'deduct_balance', 3, 'FAILED',
            jsonb_build_object('customer_id', p_customer_id, 'amount', p_amount),
            SQLERRM);
        step_status := 'FAILED';
        message := '冻结资金失败: ' || SQLERRM;
        RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- Step 4: 确认支付
CREATE OR REPLACE FUNCTION fn_saga_step_confirm_payment(
    p_saga_id UUID,
    p_order_id INT,
    p_amount DECIMAL(14,2)
) RETURNS TABLE(step_status VARCHAR(20), message TEXT) AS $$
DECLARE
    v_order_no VARCHAR(50);
BEGIN
    -- 更新订单状态
    UPDATE "order"
    SET status = '已支付',
        payment_method = '账户余额',
        payment_time = NOW(),
        version = version + 1
    WHERE id = p_order_id AND status = '待支付';
     
    IF NOT FOUND THEN
        INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, error_message)
        VALUES (p_saga_id, 'confirm_payment', 4, 'FAILED',
            jsonb_build_object('order_id', p_order_id, 'amount', p_amount),
            '订单状态异常');
        step_status := 'FAILED';
        message := '订单状态异常';
        RETURN NEXT;
    END IF;
     
    -- 扣减余额
    UPDATE account
    SET balance = balance - p_amount,
        frozen_amount = frozen_amount - p_amount,
        total_consume = total_consume + p_amount,
        version = version + 1,
        updated_at = NOW()
    WHERE customer_id = (SELECT customer_id FROM "order" WHERE id = p_order_id);
     
    -- 记录Saga步骤
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload)
    VALUES (p_saga_id, 'confirm_payment', 4, 'SUCCEEDED',
        jsonb_build_object('order_id', p_order_id, 'amount', p_amount),
        jsonb_build_object('paid', true));
     
    step_status := 'SUCCEEDED';
    message := '支付确认成功';
    RETURN NEXT;
EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, error_message)
        VALUES (p_saga_id, 'confirm_payment', 4, 'FAILED',
            jsonb_build_object('order_id', p_order_id, 'amount', p_amount),
            SQLERRM);
        step_status := 'FAILED';
        message := '确认支付失败: ' || SQLERRM;
        RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- 2.3 补偿操作(取消订单时的逆向操作)
 
-- 补偿Step4: 取消支付
CREATE OR REPLACE FUNCTION fn_saga_compensating_cancel_payment(
    p_saga_id UUID,
    p_order_id INT
) RETURNS VOID AS $$
BEGIN
    -- 恢复订单状态
    UPDATE "order" SET status = '已取消', cancelled_time = NOW() WHERE id = p_order_id;
     
    -- 释放资金
    UPDATE account
    SET frozen_amount = frozen_amount - (SELECT total_amount FROM "order" WHERE id = p_order_id),
        version = version + 1,
        updated_at = NOW()
    WHERE customer_id = (SELECT customer_id FROM "order" WHERE id = p_order_id);
     
    -- 更新日志
    UPDATE saga_log SET status = 'COMPENSATED' WHERE saga_id = p_saga_id AND step_name = 'confirm_payment';
END;
$$ LANGUAGE plpgsql;
 
-- 补偿Step3: 释放库存
CREATE OR REPLACE FUNCTION fn_saga_compensating_release_stock(
    p_saga_id UUID,
    p_product_id INT,
    p_quantity INT
) RETURNS VOID AS $$
BEGIN
    -- 释放预占库存
    UPDATE inventory
    SET reserved_qty = reserved_qty - p_quantity,
        last_sync_time = NOW()
    WHERE product_id = p_product_id AND reserved_qty >= p_quantity;
     
    -- 更新日志
    UPDATE saga_log SET status = 'COMPENSATED' WHERE saga_id = p_saga_id AND step_name = 'reserve_stock';
END;
$$ LANGUAGE plpgsql;
 
-- 补偿Step2: 删除订单
CREATE OR REPLACE FUNCTION fn_saga_compensating_delete_order(
    p_saga_id UUID
) RETURNS VOID AS $$
DECLARE
    v_order_id INT;
BEGIN
    SELECT id INTO v_order_id FROM saga_log s
    JOIN "order" o ON o.order_no = (s.request_payload->>'order_no')::TEXT
    WHERE s.saga_id = p_saga_id AND s.step_name = 'create_order' AND s.status = 'SUCCEEDED'
    LIMIT 1;
     
    IF v_order_id IS NOT NULL THEN
        DELETE FROM "order" WHERE id = v_order_id;
    END IF;
     
    -- 更新日志
    UPDATE saga_log SET status = 'COMPENSATED' WHERE saga_id = p_saga_id AND step_name = 'create_order';
END;
$$ LANGUAGE plpgsql;
 
-- 2.4 Saga编排器( orchestrator )
-- 核心编排器:按顺序执行步骤,失败时反向补偿
CREATE OR REPLACE FUNCTION fn_saga_orchestrator(
    p_customer_id INT,
    p_product_id INT,
    p_quantity INT,
    p_total_amount DECIMAL(14,2)
) RETURNS TABLE(saga_id UUID, final_status VARCHAR(20), message TEXT) AS $$
DECLARE
    v_saga_id UUID := gen_random_uuid();
    v_order_id INT;
    v_step_results TEXT[] := ARRAY[]::TEXT[];
    v_step_idx INT := 0;
BEGIN
    -- 初始化Saga日志
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload)
    VALUES (v_saga_id, 'saga_start', 0, 'EXECUTING',
        jsonb_build_object('customer_id', p_customer_id, 'product_id', p_product_id, 'quantity', p_quantity));
     
    -- Step 1: 创建订单
    v_step_idx := 1;
    FOR step_result IN SELECT * FROM fn_saga_step_create_order(v_saga_id, p_customer_id, p_product_id, p_quantity, p_total_amount) LOOP
        IF step_result.step_status = 'FAILED' THEN
            -- 补偿:不需要回滚,因为订单还没创建
            UPDATE saga_log SET status = 'COMPENSATED' WHERE saga_id = v_saga_id AND step_order = 0;
            saga_id := v_saga_id;
            final_status := 'FAILED';
            message := '订单创建失败: ' || step_result.message;
            RETURN NEXT;
            RETURN;
        END IF;
    END LOOP;
     
    -- 获取订单ID
    SELECT id INTO v_order_id FROM "order" WHERE customer_id = p_customer_id AND status = '待支付' ORDER BY id DESC LIMIT 1;
     
    -- Step 2: 预占库存
    v_step_idx := 2;
    FOR step_result IN SELECT * FROM fn_saga_step_reserve_stock(v_saga_id, p_product_id, p_quantity) LOOP
        IF step_result.step_status = 'FAILED' THEN
            -- 补偿Step2失败:删除已创建的订单
            PERFORM fn_saga_compensating_delete_order(v_saga_id);
            saga_id := v_saga_id;
            final_status := 'FAILED';
            message := '库存预占失败,已取消订单: ' || step_result.message;
            RETURN NEXT;
            RETURN;
        END IF;
    END LOOP;
     
    -- Step 3: 冻结资金
    v_step_idx := 3;
    FOR step_result IN SELECT * FROM fn_saga_step_deduct_balance(v_saga_id, p_customer_id, p_total_amount) LOOP
        IF step_result.step_status = 'FAILED' THEN
            -- 补偿Step3失败:释放库存 + 删除订单
            PERFORM fn_saga_compensating_release_stock(v_saga_id, p_product_id, p_quantity);
            PERFORM fn_saga_compensating_delete_order(v_saga_id);
            saga_id := v_saga_id;
            final_status := 'FAILED';
            message := '资金冻结失败,已补偿: ' || step_result.message;
            RETURN NEXT;
            RETURN;
        END IF;
    END LOOP;
     
    -- Step 4: 确认支付
    v_step_idx := 4;
    FOR step_result IN SELECT * FROM fn_saga_step_confirm_payment(v_saga_id, v_order_id, p_total_amount) LOOP
        IF step_result.step_status = 'FAILED' THEN
            -- 补偿Step4失败:释放资金 + 释放库存 + 删除订单
            PERFORM fn_saga_compensating_cancel_payment(v_saga_id, v_order_id);
            PERFORM fn_saga_compensating_release_stock(v_saga_id, p_product_id, p_quantity);
            PERFORM fn_saga_compensating_delete_order(v_saga_id);
            saga_id := v_saga_id;
            final_status := 'FAILED';
            message := '支付确认失败,已全部补偿: ' || step_result.message;
            RETURN NEXT;
            RETURN;
        END IF;
    END LOOP;
     
    -- 所有步骤成功
    UPDATE saga_log SET status = 'SUCCEEDED', completed_at = NOW() WHERE saga_id = v_saga_id AND step_order = 0;
     
    saga_id := v_saga_id;
    final_status := 'SUCCEEDED';
    message := '订单处理成功,Saga ID: ' || v_saga_id::TEXT;
    RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- 测试Saga编排器
SELECT * FROM fn_saga_orchestrator(1, 1, 1, 58800.00);
 
-- ============================================================
-- 3. Saga模式 - 订单取消流程
-- ============================================================
 
CREATE OR REPLACE FUNCTION fn_saga_cancel_order(
    p_order_id INT,
    p_reason VARCHAR(500)
) RETURNS TABLE(saga_id UUID, final_status VARCHAR(20), message TEXT) AS $$
DECLARE
    v_saga_id UUID := gen_random_uuid();
    v_customer_id INT;
    v_total_amount DECIMAL(14,2);
    v_product_id INT;
    v_quantity INT;
BEGIN
    -- 获取订单信息
    SELECT customer_id, total_amount INTO v_customer_id, v_total_amount
    FROM "order" WHERE id = p_order_id AND status = '已支付';
     
    IF NOT FOUND THEN
        saga_id := v_saga_id;
        final_status := 'FAILED';
        message := '订单不存在或状态异常';
        RETURN NEXT;
        RETURN;
    END IF;
     
    SELECT product_id, quantity INTO v_product_id, v_quantity
    FROM order_item WHERE order_id = p_order_id LIMIT 1;
     
    -- 初始化Saga日志
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload)
    VALUES (v_saga_id, 'cancel_saga_start', 0, 'EXECUTING',
        jsonb_build_object('order_id', p_order_id, 'reason', p_reason));
     
    -- Step 1: 释放资金(解冻)
    UPDATE account
    SET frozen_amount = frozen_amount - v_total_amount,
        version = version + 1,
        updated_at = NOW()
    WHERE customer_id = v_customer_id;
     
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload)
    VALUES (v_saga_id, 'release_funds', 1, 'SUCCEEDED',
        jsonb_build_object('customer_id', v_customer_id, 'amount', v_total_amount),
        jsonb_build_object('released', true));
     
    -- Step 2: 释放库存
    UPDATE inventory
    SET reserved_qty = reserved_qty - v_quantity,
        last_sync_time = NOW()
    WHERE product_id = v_product_id AND reserved_qty >= v_quantity;
     
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload)
    VALUES (v_saga_id, 'release_stock', 2, 'SUCCEEDED',
        jsonb_build_object('product_id', v_product_id, 'quantity', v_quantity),
        jsonb_build_object('released', true));
     
    -- Step 3: 更新订单状态
    UPDATE "order"
    SET status = '已取消',
        cancelled_time = NOW(),
        cancel_reason = p_reason,
        version = version + 1
    WHERE id = p_order_id;
     
    INSERT INTO saga_log (saga_id, step_name, step_order, status, request_payload, response_payload)
    VALUES (v_saga_id, 'cancel_order', 3, 'SUCCEEDED',
        jsonb_build_object('order_id', p_order_id),
        jsonb_build_object('cancelled', true));
     
    -- 所有步骤成功
    UPDATE saga_log SET status = 'SUCCEEDED', completed_at = NOW() WHERE saga_id = v_saga_id AND step_order = 0;
     
    saga_id := v_saga_id;
    final_status := 'SUCCEEDED';
    message := '订单取消成功,Saga ID: ' || v_saga_id::TEXT;
    RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- 测试取消订单
-- SELECT * FROM fn_saga_cancel_order(订单ID, '用户原因取消');
 
-- ============================================================
-- 4. Saga事务日志查询与监控
-- ============================================================
 
-- 4.1 查看Saga执行状态
SELECT
    saga_id,
    step_name,
    step_order,
    status,
    request_payload,
    response_payload,
    error_message,
    retry_count,
    created_at,
    completed_at
FROM saga_log
WHERE saga_id = 'a0000000-0000-0000-0000-000000000001'::uuid
ORDER BY step_order;
 
-- 4.2 查看失败的Saga
SELECT
    DISTINCT ON (saga_id) saga_id,
    step_name,
    status,
    error_message,
    created_at
FROM saga_log
WHERE status IN ('FAILED', 'COMPENSATING')
ORDER BY saga_id, step_order DESC;
 
-- 4.3 查看Saga执行耗时
SELECT
    saga_id,
    step_name,
    step_order,
    status,
    created_at,
    completed_at,
    completed_at - created_at AS duration
FROM saga_log
WHERE completed_at IS NOT NULL
ORDER BY created_at DESC
LIMIT 20;
 
-- 4.4 重试失败的步骤
CREATE OR REPLACE FUNCTION fn_retry_failed_saga_step(
    p_saga_id UUID,
    p_step_name VARCHAR(100),
    p_max_retries INT DEFAULT 3
) RETURNS TABLE(retry_status VARCHAR(20), message TEXT) AS $$
DECLARE
    v_retry_count INT;
    v_request_payload JSONB;
BEGIN
    -- 获取原始请求参数
    SELECT retry_count, request_payload INTO v_retry_count, v_request_payload
    FROM saga_log
    WHERE saga_id = p_saga_id AND step_name = p_step_name AND status = 'FAILED'
    LIMIT 1;
     
    IF v_retry_count IS NULL THEN
        retry_status := 'NOT_FOUND';
        message := '未找到失败的步骤';
        RETURN NEXT;
        RETURN;
    END IF;
     
    IF v_retry_count >= p_max_retries THEN
        retry_status := 'MAX_RETRIES';
        message := '已达最大重试次数';
        RETURN NEXT;
        RETURN;
    END IF;
     
    -- 更新重试次数
    UPDATE saga_log SET retry_count = retry_count + 1 WHERE saga_id = p_saga_id AND step_name = p_step_name;
     
    -- 重新执行步骤(这里需要根据step_name调用对应函数)
    -- 实际生产中可以使用消息队列实现异步重试
     
    retry_status := 'RETRIED';
    message := '已触发重试,当前重试次数: ' || (v_retry_count + 1)::TEXT;
    RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
 
-- ============================================================
-- 5. 最终一致性验证
-- ============================================================
 
-- 5.1 验证库存一致性
SELECT
    p.name,
    p.stock_quantity AS product_stock,
    COALESCE(i.quantity, 0) AS inventory_stock,
    CASE WHEN p.stock_quantity = COALESCE(i.quantity, 0) THEN '一致' ELSE '不一致' END AS consistency_check
FROM product p
LEFT JOIN inventory i ON p.id = i.product_id
WHERE p.is_active = TRUE;
 
-- 5.2 验证订单总额一致性
SELECT
    o.order_no,
    o.actual_amount,
    COALESCE(SUM(oi.subtotal), 0) AS items_total,
    CASE WHEN ABS(o.actual_amount - COALESCE(SUM(oi.subtotal), 0)) < 0.01 THEN '一致' ELSE '不一致' END AS check_result
FROM "order" o
LEFT JOIN order_item oi ON o.id = oi.order_id
GROUP BY o.id, o.order_no, o.actual_amount
HAVING ABS(o.actual_amount - COALESCE(SUM(oi.subtotal), 0)) >= 0.01;
 
-- 5.3 验证账户余额一致性
SELECT
    c.customer_no,
    c.name,
    a.balance,
    a.frozen_amount,
    a.total_recharge,
    a.total_consume,
    a.balance + a.frozen_amount AS total_available,
    a.total_recharge - a.total_consume AS should_be_balance,
    CASE WHEN ABS((a.balance + a.frozen_amount) - (a.total_recharge - a.total_consume)) < 0.01 THEN '一致' ELSE '不一致' END AS check_result
FROM customer c
JOIN account a ON c.id = a.customer_id
WHERE ABS((a.balance + a.frozen_amount) - (a.total_recharge - a.total_consume)) >= 0.01;
 
-- 5.4 检查未完成的Saga
SELECT
    saga_id,
    step_name,
    step_order,
    status,
    created_at,
    age(now(), created_at) AS elapsed
FROM saga_log
WHERE status IN ('PENDING', 'EXECUTING')
ORDER BY created_at;
 
-- ============================================================
-- 6. DBMS_SCHEDULER等效 - pg_cron定时任务
-- ============================================================
 
-- 启用pg_cron扩展(需要postgresql.conf中设置 shared_preload_libraries = 'pg_cron')
-- CREATE EXTENSION IF NOT EXISTS pg_cron;
 
-- 示例:每天凌晨2点清理30天前的Saga日志
-- SELECT cron.schedule('clean-saga-logs', '0 2 * * *', 'DELETE FROM saga_log WHERE created_at < now() - interval ''30 days''');
 
-- 示例:每小时检查超时的Saga事务
-- SELECT cron.schedule('check-stale-sagas', '0 * * * *', '
--     UPDATE saga_log SET status = ''TIMEOUT''
--     WHERE status = ''EXECUTING''
--     AND created_at < now() - interval ''5 minutes''
-- ');
 
-- 查看定时任务
-- SELECT * FROM cron.job;
 
-- ============================================================
-- 完成
-- ============================================================
\echo 'Deadlock prevention and Saga patterns demo completed!'
相关推荐
李白客7 小时前
数据融合平台怎么选:批流一体、实时同步与数据质量的 7 个维度
数据库·数据库架构
thinking_talk8 小时前
WorkBuddy应用生成背后的数据层:腾讯云PG
postgresql·腾讯云·云数据库
知识的搬运工旺仔1 天前
唯一索引与 NULL 值:PostgreSQL 主键约束与 NULLS NOT DISTINCT
数据库·后端·sql·postgresql
geovindu1 天前
sql: Transaction & Concurrency Patterns using Oracel 21c
oracle·数据库开发·数据库架构
jnrjian1 天前
EDB postgresql TDE 加密的文件
postgresql
IvorySQL1 天前
PostgreSQL 日报|逻辑解码竞态条件修复(9 月 20 日)
数据库·人工智能·postgresql
geovindu1 天前
sql: Transaction & Concurrency Patterns using mysql 9.0
mysql·数据库开发·数据库架构
XMYX-01 天前
Rocky_Linux_9.8_安装_PostgreSQL_16
postgresql
foolishlee1 天前
SCRAM-SHA-256
数据库·算法·postgresql