未记录事实核验 · 状态:现行内容
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本章适用于测试数据库中的事务、快照、锁冲突及隔离级别练习,需按会话标注安排并发执行,不是可直接投产的转账或库存服务。转账与库存预留函数未完整校验非正数或NULL输入:负数可能造成反向转账或减少预留量,NULL库存预留量可能写入允许为空的预留字段;不得直接接受未校验的业务输入。“先验证,再执行”的DO示例未检查账户是否存在,预检查也不能替代并发下的完整业务校验,其内部BEGIN块不是独立提交的事务。末尾pg_locks查询仅提供同一锁对象上的候选关联,不能单凭blocking_pid字段认定实际阻塞关系;隔离级别比较函数仅返回特性标记,不是并发验证。持久性应结合正文所述配置条件理解,序列化失败需由调用方重试整个事务。本次仅作全文结构审阅,未执行SQL或开展外部事实核验。
🔒 PostgreSQL事务与并发控制
📚 学习目标
通过本章节的学习,你将能够:
- 理解ACID事务特性和PostgreSQL的实现机制
- 掌握MVCC并发控制原理和优势
- 了解各种锁机制和死锁处理
- 理解不同隔离级别的特点和应用场景
- 掌握事务管理的最佳实践
tip 💡 学习建议
事务和并发控制是数据库的核心概念,理解比记忆更重要。就像理解交通规则一样,不仅要知道规则是什么,更要理解为什么需要这些规则。通过实际例子来体验不同的并发场景,这样能更好地理解这些抽象概念。
🎯 ACID事务特性
1. 原子性(Atomicity)
原子性确保事务中的所有操作要么全部成功,要么全部失败。
-- 创建测试表
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
account_number VARCHAR(20) UNIQUE NOT NULL,
balance DECIMAL(10,2) NOT NULL CHECK (balance >= 0),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入测试数据
INSERT INTO accounts (account_number, balance) VALUES
('ACC001', 1000.00),
('ACC002', 500.00),
('ACC003', 750.00);
-- 原子性示例:银行转账
BEGIN;
-- 从账户ACC001转出100元
UPDATE accounts
SET balance = balance - 100
WHERE account_number = 'ACC001';
-- 向账户ACC002转入100元
UPDATE accounts
SET balance = balance + 100
WHERE account_number = 'ACC002';
-- 检查余额是否足够
SELECT balance FROM accounts WHERE account_number = 'ACC001';
COMMIT;
-- 演示回滚:转账失败的情况
BEGIN;
UPDATE accounts
SET balance = balance - 2000 -- 超出余额
WHERE account_number = 'ACC001';
UPDATE accounts
SET balance = balance + 2000
WHERE account_number = 'ACC002';
-- 检查约束违反,触发回滚
ROLLBACK;
-- 查看账户余额(应该没有变化)
SELECT * FROM accounts;
-- 使用SAVEPOINT实现部分回滚
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE account_number = 'ACC001';
SAVEPOINT sp1; -- 创建保存点
UPDATE accounts SET balance = balance + 50 WHERE account_number = 'ACC002';
UPDATE accounts SET balance = balance - 1000 WHERE account_number = 'ACC003'; -- 这会失败
ROLLBACK TO sp1; -- 回滚到保存点
-- 这个操作会成功
UPDATE accounts SET balance = balance + 50 WHERE account_number = 'ACC003';
COMMIT;
SELECT * FROM accounts;
2. 一致性(Consistency)
一致性确保数据库始终处于有效状态,满足所有约束条件。
-- 创建带约束的表
CREATE TABLE inventory (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL,
warehouse_id INTEGER NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity >= 0),
reserved_quantity INTEGER DEFAULT 0 CHECK (reserved_quantity >= 0),
-- 确保预留数量不超过总数量
CHECK (reserved_quantity <= quantity),
UNIQUE(product_id, warehouse_id)
);
INSERT INTO inventory (product_id, warehouse_id, quantity) VALUES
(1, 1, 100),
(1, 2, 50),
(2, 1, 75);
-- 一致性示例:库存预留
CREATE OR REPLACE FUNCTION reserve_inventory(
p_product_id INTEGER,
p_warehouse_id INTEGER,
p_reserve_qty INTEGER
) RETURNS BOOLEAN AS $$
DECLARE
current_available INTEGER;
BEGIN
-- 获取当前可用库存
SELECT (quantity - reserved_quantity) INTO current_available
FROM inventory
WHERE product_id = p_product_id AND warehouse_id = p_warehouse_id
FOR UPDATE; -- 锁定行
-- 检查库存是否足够
IF current_available IS NULL THEN
RAISE EXCEPTION 'Product not found in warehouse';
END IF;
IF current_available < p_reserve_qty THEN
RAISE EXCEPTION 'Insufficient inventory. Available: %, Requested: %',
current_available, p_reserve_qty;
END IF;
-- 更新预留数量
UPDATE inventory
SET reserved_quantity = reserved_quantity + p_reserve_qty
WHERE product_id = p_product_id AND warehouse_id = p_warehouse_id;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
-- 测试一致性
BEGIN;
SELECT reserve_inventory(1, 1, 30); -- 成功
SELECT reserve_inventory(1, 1, 80); -- 失败:库存不足
ROLLBACK;
-- 查看库存状态
SELECT *, (quantity - reserved_quantity) as available
FROM inventory;
3. 隔离性(Isolation)
隔离性确保并发事务之间不会相互干扰。
-- 创建测试表
CREATE TABLE counters (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE,
value INTEGER DEFAULT 0
);
INSERT INTO counters (name, value) VALUES ('page_views', 0);
-- 在两个会话中模拟并发更新
-- 会话1:
BEGIN;
SELECT value FROM counters WHERE name = 'page_views'; -- 假设返回0
-- ... 处理时间 ...
UPDATE counters SET value = value + 1 WHERE name = 'page_views';
COMMIT;
-- 会话2(同时进行):
BEGIN;
SELECT value FROM counters WHERE name = 'page_views'; -- 也返回0
-- ... 处理时间 ...
UPDATE counters SET value = value + 1 WHERE name = 'page_views';
COMMIT;
-- 在默认READ COMMITTED下,两次UPDATE均成功提交且没有其他写入时,最终值为2;
-- 关键是SET value = value + 1在数据库中执行增量更新,不能据此认为所有先读后写流程都不会丢失更新
4. 持久性(Durability)
持久性确保已提交的事务更改永久保存。
-- 持久性通过WAL(写前日志)实现
-- 查看WAL配置
SHOW wal_level;
SHOW fsync;
SHOW synchronous_commit;
SHOW wal_sync_method;
-- 创建测试表
CREATE TABLE important_data (
id SERIAL PRIMARY KEY,
data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入重要数据
BEGIN;
INSERT INTO important_data (data) VALUES ('Critical business data');
-- 在fsync开启且synchronous_commit不为off等相应持久性配置下,提交确认前会完成要求的WAL持久化;并不要求此时数据页本身已写入磁盘
COMMIT;
-- 即使数据库重启,数据也会保留
SELECT * FROM important_data;
🔄 MVCC多版本并发控制
PostgreSQL使用MVCC来实现高并发性能,允许读操作不被写操作阻塞。
1. MVCC基本原理
-- 创建测试表
CREATE TABLE mvcc_demo (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
value INTEGER,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO mvcc_demo (name, value) VALUES ('test', 100);
-- 查看当前事务ID
SELECT txid_current();
-- 查看表的系统列(通常不显示)
SELECT
ctid, -- 行标识符
xmin, -- 创建该行版本的事务ID
xmax, -- 删除该行版本的事务ID(0表示未删除)
name,
value
FROM mvcc_demo;
-- 模拟MVCC行为
-- 会话1:使用可重复读观察事务级快照
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT
txid_current() as current_txid,
name,
value,
xmin,
xmax
FROM mvcc_demo WHERE id = 1;
-- 不要提交,保持事务打开
-- COMMIT;
-- 会话2:更新数据
BEGIN;
UPDATE mvcc_demo
SET value = 200, updated_at = CURRENT_TIMESTAMP
WHERE id = 1;
COMMIT;
-- 再次查看系统列
SELECT
ctid,
xmin,
xmax,
name,
value,
updated_at
FROM mvcc_demo;
-- 会话1仍然能看到旧版本的数据(快照隔离)
-- 会话2能看到新版本的数据
2. 事务快照
-- 查看当前事务快照信息
SELECT
txid_current() as current_txid,
txid_current_snapshot() as snapshot;
-- 创建并发测试场景
CREATE TABLE snapshot_test (
id SERIAL PRIMARY KEY,
data TEXT,
modified_by INTEGER,
modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO snapshot_test (data, modified_by) VALUES
('Initial data', txid_current());
-- 开始事务A:使用可重复读保持事务级快照
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT txid_current() as txid_a;
SELECT * FROM snapshot_test; -- 记录快照
-- 在另一个会话中修改数据
-- 这个事务看不到其他事务的修改
-- 在另一个会话中:
-- BEGIN;
-- UPDATE snapshot_test SET data = 'Modified data', modified_by = txid_current();
-- COMMIT;
-- 事务A仍然看到原始数据
SELECT * FROM snapshot_test;
COMMIT;
-- 现在能看到修改后的数据
SELECT * FROM snapshot_test;
3. 死元组和VACUUM
-- 查看表的统计信息
SELECT
schemaname,
tablename,
n_live_tup, -- 活跃元组数
n_dead_tup, -- 死亡元组数
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
WHERE tablename = 'mvcc_demo';
-- 执行多次更新产生死元组
UPDATE mvcc_demo SET value = value + 1;
UPDATE mvcc_demo SET value = value + 1;
UPDATE mvcc_demo SET value = value + 1;
-- 再次查看统计信息
SELECT
schemaname,
tablename,
n_live_tup,
n_dead_tup,
n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0) as dead_ratio
FROM pg_stat_user_tables
WHERE tablename = 'mvcc_demo';
-- 手动执行VACUUM清理死元组
VACUUM mvcc_demo;
-- 查看清理后的统计信息
SELECT
schemaname,
tablename,
n_live_tup,
n_dead_tup
FROM pg_stat_user_tables
WHERE tablename = 'mvcc_demo';
🔐 锁机制
PostgreSQL使用多种锁机制来控制并发访问。
1. 表级锁
-- 查看当前的锁信息
SELECT
l.locktype,
l.database,
l.relation::regclass,
l.mode,
l.granted,
a.query
FROM pg_locks l
LEFT JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.relation IS NOT NULL;
-- 显式表级锁
BEGIN;
LOCK TABLE accounts IN ACCESS EXCLUSIVE MODE;
-- 这会阻止其他事务对表的任何访问
SELECT * FROM accounts;
COMMIT;
-- 不同的锁模式
BEGIN;
LOCK TABLE accounts IN SHARE MODE; -- 允许读,阻止写
SELECT * FROM accounts;
-- 其他会话可以读取,但不能修改
COMMIT;
-- 查看锁的兼容性
CREATE TABLE lock_demo (id INTEGER, data TEXT);
-- 会话1:
BEGIN;
LOCK TABLE lock_demo IN SHARE MODE;
SELECT pg_sleep(30); -- 保持锁30秒
COMMIT;
-- 会话2(同时运行):
BEGIN;
LOCK TABLE lock_demo IN SHARE MODE; -- 成功(兼容)
-- LOCK TABLE lock_demo IN EXCLUSIVE MODE; -- 不兼容,等待会话1释放锁;使用NOWAIT才会立即报错
COMMIT;
2. 行级锁
-- 行级锁示例
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
price DECIMAL(10,2),
status VARCHAR(20) DEFAULT 'pending'
);
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
(1, 101, 2, 25.50),
(1, 102, 1, 45.00),
(2, 101, 3, 25.50);
-- FOR UPDATE:排他行锁
BEGIN;
SELECT * FROM order_items
WHERE order_id = 1
FOR UPDATE; -- 锁定选中的行
-- 其他事务无法修改这些行
UPDATE order_items
SET status = 'processing'
WHERE order_id = 1;
COMMIT;
-- FOR SHARE:共享行锁
BEGIN;
SELECT * FROM order_items
WHERE order_id = 1
FOR SHARE; -- 共享锁,允许其他读取,阻止修改
-- 其他事务可以读取,但不能修改
COMMIT;
-- FOR UPDATE SKIP LOCKED:跳过已锁定的行
BEGIN;
-- 仅更新本次实际选中并锁定的订单项
WITH selected_item AS (
SELECT id FROM order_items
WHERE status = 'pending'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE order_items AS oi
SET status = 'processing'
FROM selected_item AS selected
WHERE oi.id = selected.id
RETURNING oi.*;
COMMIT;
-- FOR UPDATE NOWAIT:立即返回而不等待
BEGIN;
SELECT * FROM order_items
WHERE order_id = 1
FOR UPDATE NOWAIT; -- 如果有锁冲突,立即失败
COMMIT;
3. 死锁检测和处理
-- 创建可能导致死锁的场景
CREATE TABLE resource_a (id INTEGER PRIMARY KEY, data TEXT);
CREATE TABLE resource_b (id INTEGER PRIMARY KEY, data TEXT);
INSERT INTO resource_a VALUES (1, 'Data A');
INSERT INTO resource_b VALUES (1, 'Data B');
-- 模拟死锁场景
-- 会话1:
BEGIN;
UPDATE resource_a SET data = 'Modified by Session 1' WHERE id = 1;
-- 等待一下,然后执行下一条语句
SELECT pg_sleep(5);
UPDATE resource_b SET data = 'Modified by Session 1' WHERE id = 1;
COMMIT;
-- 会话2(同时进行):
BEGIN;
UPDATE resource_b SET data = 'Modified by Session 2' WHERE id = 1;
-- 等待一下,然后执行下一条语句
SELECT pg_sleep(5);
UPDATE resource_a SET data = 'Modified by Session 2' WHERE id = 1; -- 死锁!
COMMIT;
-- PostgreSQL会自动检测死锁并回滚其中一个事务
-- 查看死锁检测配置
SHOW deadlock_timeout; -- 死锁检测超时时间
-- 监控死锁
SELECT
application_name,
state,
query,
wait_event_type,
wait_event,
query_start,
state_change
FROM pg_stat_activity
WHERE state != 'idle';
📊 事务隔离级别
PostgreSQL支持四种标准的事务隔离级别。
1. READ UNCOMMITTED
-- 读未提交(实际上PostgreSQL将其实现为READ COMMITTED)
-- 创建测试表
CREATE TABLE isolation_test (
id INTEGER PRIMARY KEY,
value INTEGER,
description TEXT
);
INSERT INTO isolation_test VALUES (1, 100, 'Initial value');
-- 会话1:READ UNCOMMITTED
BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM isolation_test WHERE id = 1;
-- 在会话2中修改数据(但不提交)
SELECT pg_sleep(10);
SELECT * FROM isolation_test WHERE id = 1; -- 不会看到未提交的更改
COMMIT;
2. READ COMMITTED(默认级别)
-- 读已提交
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT value FROM isolation_test WHERE id = 1; -- 第一次读取
-- 在另一个会话中修改并提交数据
SELECT pg_sleep(10);
SELECT value FROM isolation_test WHERE id = 1; -- 第二次读取(不可重复读)
COMMIT;
-- 演示幻读现象
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT COUNT(*) FROM isolation_test WHERE value > 50; -- 第一次计数
-- 在另一个会话中插入新记录
SELECT pg_sleep(10);
SELECT COUNT(*) FROM isolation_test WHERE value > 50; -- 第二次计数(幻读)
COMMIT;
3. REPEATABLE READ
-- 可重复读
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT value FROM isolation_test WHERE id = 1; -- 第一次读取
-- 在另一个会话中修改并提交数据
SELECT pg_sleep(10);
SELECT value FROM isolation_test WHERE id = 1; -- 第二次读取(相同结果)
-- 尝试修改可能导致序列化失败
UPDATE isolation_test SET value = 200 WHERE id = 1;
COMMIT;
-- 演示序列化失败
-- 会话1:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT value FROM isolation_test WHERE id = 1;
-- 基于读取的值进行计算
UPDATE isolation_test SET value = value + 50 WHERE id = 1;
COMMIT;
-- 会话2(同时进行):
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT value FROM isolation_test WHERE id = 1;
-- 基于相同的值进行计算
UPDATE isolation_test SET value = value + 30 WHERE id = 1; -- 可能失败
COMMIT;
4. SERIALIZABLE
-- 可序列化
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT SUM(value) FROM isolation_test;
-- 基于汇总结果进行操作
INSERT INTO isolation_test VALUES (2, 150, 'Calculated value');
COMMIT;
-- 并发SERIALIZABLE事务发生不可序列化的读写依赖时,可能出现序列化失败;
-- 修改相同数据并不必然触发该错误,修改不同数据也可能触发;调用方应重试整个事务
5. 隔离级别对比
-- 创建隔离级别比较函数
CREATE OR REPLACE FUNCTION test_isolation_level(isolation_level TEXT)
RETURNS TABLE(
read_count INTEGER,
phantom_possible BOOLEAN,
dirty_read_possible BOOLEAN
) AS $$
BEGIN
-- 此函数仅返回隔离级别特性标记,不改变调用事务的隔离级别。
-- 实际并发实验应在独立会话中使用BEGIN TRANSACTION ISOLATION LEVEL。
isolation_level := upper(trim(isolation_level));
IF isolation_level IS NULL OR isolation_level NOT IN
('READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE') THEN
RAISE EXCEPTION 'Unsupported isolation level: %', isolation_level;
END IF;
-- 执行测试查询
SELECT COUNT(*) INTO read_count FROM isolation_test;
-- 根据隔离级别设置可能性
CASE isolation_level
WHEN 'READ UNCOMMITTED' THEN
phantom_possible := TRUE;
dirty_read_possible := FALSE; -- PostgreSQL按READ COMMITTED实现
WHEN 'READ COMMITTED' THEN
phantom_possible := TRUE;
dirty_read_possible := FALSE;
WHEN 'REPEATABLE READ' THEN
phantom_possible := FALSE;
dirty_read_possible := FALSE;
WHEN 'SERIALIZABLE' THEN
phantom_possible := FALSE;
dirty_read_possible := FALSE;
END CASE;
-- 函数内不提交事务;事务边界由调用方控制。
RETURN QUERY SELECT read_count, phantom_possible, dirty_read_possible;
END;
$$ LANGUAGE plpgsql;
-- 测试不同隔离级别
SELECT 'READ COMMITTED' as level, * FROM test_isolation_level('READ committed');
SELECT 'REPEATABLE READ' as level, * FROM test_isolation_level('repeatable read');
SELECT 'SERIALIZABLE' as level, * FROM test_isolation_level('serializable');
🎯 事务管理最佳实践
1. 事务设计原则
-- 1. 保持事务简短
-- 不好的做法:长事务
-- BEGIN;
-- SELECT pg_sleep(300); -- 长时间操作
-- UPDATE accounts SET balance = balance + 1;
-- COMMIT;
-- 好的做法:将长操作移到事务外
SELECT pg_sleep(1); -- 准备工作在事务外完成
BEGIN;
UPDATE accounts SET balance = balance + 1; -- 快速事务
COMMIT;
-- 2. 避免在事务中进行用户交互
-- 不好的做法:
-- BEGIN;
-- UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- -- 等待用户确认...
-- UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- COMMIT;
-- 好的做法:先验证,再执行
DO $$
DECLARE
source_balance DECIMAL(10,2);
BEGIN
-- 预检查
SELECT balance INTO source_balance FROM accounts WHERE id = 1;
IF source_balance < 100 THEN
RAISE EXCEPTION 'Insufficient funds';
END IF;
-- 快速事务
BEGIN
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
EXCEPTION WHEN OTHERS THEN
RAISE;
END;
END;
$$;
2. 错误处理
-- 事务中的错误处理
CREATE OR REPLACE FUNCTION safe_transfer(
from_account TEXT,
to_account TEXT,
amount DECIMAL(10,2)
) RETURNS BOOLEAN AS $$
DECLARE
from_balance DECIMAL(10,2);
BEGIN
-- 开始事务
BEGIN
-- 检查源账户余额
SELECT balance INTO from_balance
FROM accounts
WHERE account_number = from_account
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Source account not found: %', from_account;
END IF;
IF from_balance < amount THEN
RAISE EXCEPTION 'Insufficient funds. Available: %, Requested: %',
from_balance, amount;
END IF;
-- 执行转账
UPDATE accounts
SET balance = balance - amount
WHERE account_number = from_account;
UPDATE accounts
SET balance = balance + amount
WHERE account_number = to_account;
IF NOT FOUND THEN
RAISE EXCEPTION 'Target account not found: %', to_account;
END IF;
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
-- 记录错误日志
RAISE NOTICE 'Transfer failed: %', SQLERRM;
RAISE;
END;
END;
$$ LANGUAGE plpgsql;
-- 测试错误处理
SELECT safe_transfer('ACC001', 'ACC002', 50.00); -- 成功
SELECT safe_transfer('ACC001', 'INVALID', 50.00); -- 失败:账户不存在
SELECT safe_transfer('ACC001', 'ACC002', 5000.00); -- 失败:余额不足
3. 并发控制策略
-- 创建库存管理示例
CREATE TABLE product_inventory (
product_id INTEGER PRIMARY KEY,
quantity INTEGER NOT NULL CHECK (quantity >= 0),
reserved INTEGER DEFAULT 0 CHECK (reserved >= 0),
version INTEGER DEFAULT 1,
CHECK (reserved <= quantity)
);
INSERT INTO product_inventory (product_id, quantity) VALUES
(1, 100), (2, 50), (3, 75);
-- 乐观锁策略
CREATE OR REPLACE FUNCTION reserve_product_optimistic(
p_product_id INTEGER,
p_quantity INTEGER
) RETURNS BOOLEAN AS $$
DECLARE
current_version INTEGER;
available_qty INTEGER;
BEGIN
-- 读取当前版本和可用数量
SELECT version, (quantity - reserved)
INTO current_version, available_qty
FROM product_inventory
WHERE product_id = p_product_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Product not found: %', p_product_id;
END IF;
IF available_qty < p_quantity THEN
RAISE EXCEPTION 'Insufficient inventory';
END IF;
-- 尝试更新(检查版本)
UPDATE product_inventory
SET
reserved = reserved + p_quantity,
version = version + 1
WHERE product_id = p_product_id
AND version = current_version;
IF NOT FOUND THEN
RAISE EXCEPTION 'Concurrent modification detected';
END IF;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
-- 悲观锁策略
CREATE OR REPLACE FUNCTION reserve_product_pessimistic(
p_product_id INTEGER,
p_quantity INTEGER
) RETURNS BOOLEAN AS $$
DECLARE
available_qty INTEGER;
BEGIN
-- 立即锁定行
SELECT (quantity - reserved) INTO available_qty
FROM product_inventory
WHERE product_id = p_product_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Product not found: %', p_product_id;
END IF;
IF available_qty < p_quantity THEN
RAISE EXCEPTION 'Insufficient inventory';
END IF;
-- 更新库存
UPDATE product_inventory
SET reserved = reserved + p_quantity
WHERE product_id = p_product_id;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
-- 测试并发控制
BEGIN;
SELECT reserve_product_pessimistic(1, 10);
COMMIT;
SELECT * FROM product_inventory;
4. 事务监控
-- 创建事务监控视图
CREATE VIEW v_transaction_monitor AS
SELECT
pid,
usename,
application_name,
client_addr,
state,
query_start,
state_change,
EXTRACT(EPOCH FROM (NOW() - query_start)) as duration_seconds,
EXTRACT(EPOCH FROM (NOW() - xact_start)) as transaction_duration,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE state != 'idle'
AND pid != pg_backend_pid();
-- 查看长时间运行的事务
SELECT * FROM v_transaction_monitor
WHERE transaction_duration > 10
ORDER BY transaction_duration DESC;
-- 查看阻塞信息
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity
ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;
🎯 小结
通过本章节的学习,我们掌握了:
- ACID特性:原子性、一致性、隔离性、持久性的概念和实现
- MVCC机制:多版本并发控制的工作原理和优势
- 锁机制:表级锁、行级锁、死锁检测和处理
- 隔离级别:四种标准隔离级别的特点和应用
- 最佳实践:事务设计、错误处理、并发控制、监控
abstract 📚 下一步学习
现在你已经理解了PostgreSQL的事务和并发控制机制,接下来我们将学习08-PostgreSQL备份与恢复|PostgreSQL备份与恢复,了解如何保护你的数据安全。
🔗 相关资源
- PostgreSQL索引与性能优化
- PostgreSQL
- SQL - 通用事务概念
💡 记住:理解事务和并发控制是数据库编程的核心技能。在设计应用时,始终考虑并发场景,选择合适的隔离级别和锁策略。保持事务简短,避免长时间持有锁。当遇到并发问题时,先理解问题的本质,再选择合适的解决方案。实践是掌握这些概念的最好方法!