PostgreSQL 事务与锁 #
事务概述 #
事务是一组数据库操作的逻辑单元,要么全部成功,要么全部失败。
text
┌─────────────────────────────────────────────────────────────┐
│ ACID 特性 │
├─────────────────────────────────────────────────────────────┤
│ │
│ A - Atomicity(原子性) │
│ 事务中的操作要么全部执行,要么全部不执行 │
│ │
│ C - Consistency(一致性) │
│ 事务执行前后数据库状态保持一致 │
│ │
│ I - Isolation(隔离性) │
│ 并发事务之间互不干扰 │
│ │
│ D - Durability(持久性) │
│ 事务提交后数据永久保存 │
│ │
└─────────────────────────────────────────────────────────────┘
事务控制 #
基本事务 #
sql
-- 开始事务
BEGIN;
-- 或
BEGIN TRANSACTION;
-- 或
START TRANSACTION;
-- 提交事务
COMMIT;
-- 或
COMMIT TRANSACTION;
-- 回滚事务
ROLLBACK;
-- 或
ROLLBACK TRANSACTION;
-- 示例
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 如果出错则回滚
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 发现错误
ROLLBACK;
保存点 #
sql
-- 创建保存点
BEGIN;
INSERT INTO orders (customer_id, total) VALUES (1, 100);
SAVEPOINT order_created;
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (currval('orders_id_seq'), 1, 5);
-- 如果订单项插入失败,回滚到保存点
-- ROLLBACK TO order_created;
COMMIT;
-- 释放保存点
RELEASE SAVEPOINT order_created;
事务模式 #
sql
-- 读写事务(默认)
BEGIN READ WRITE;
-- 只读事务
BEGIN READ ONLY;
-- 隔离级别
BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- 组合使用
BEGIN ISOLATION LEVEL READ COMMITTED READ WRITE;
隔离级别 #
隔离级别对比 #
text
┌─────────────────┬─────────────────────────────────────────────┐
│ 隔离级别 │ 可能的问题 │
├─────────────────┼─────────────────────────────────────────────┤
│ READ UNCOMMITTED│ 脏读、不可重复读、幻读 │
│ READ COMMITTED │ 不可重复读、幻读(PostgreSQL 默认) │
│ REPEATABLE READ │ 幻读 │
│ SERIALIZABLE │ 无(最高隔离级别) │
└─────────────────┴─────────────────────────────────────────────┘
PostgreSQL 实现说明:
- READ UNCOMMITTED 实际等同于 READ COMMITTED
- REPEATABLE READ 实际上也防止了幻读
READ COMMITTED(默认) #
sql
-- 会话 1
BEGIN;
SELECT * FROM accounts WHERE id = 1;
-- balance: 1000
-- 会话 2(在会话 1 事务未提交时)
BEGIN;
UPDATE accounts SET balance = 2000 WHERE id = 1;
COMMIT;
-- 会话 1 继续查询
SELECT * FROM accounts WHERE id = 1;
-- balance: 2000(可以看到其他事务已提交的修改)
COMMIT;
REPEATABLE READ #
sql
-- 会话 1
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM accounts WHERE id = 1;
-- balance: 1000
-- 会话 2
BEGIN;
UPDATE accounts SET balance = 2000 WHERE id = 1;
COMMIT;
-- 会话 1 继续查询
SELECT * FROM accounts WHERE id = 1;
-- balance: 1000(看不到其他事务的修改)
COMMIT;
-- 会话 1 新事务
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM accounts WHERE id = 1;
-- balance: 2000(新事务可以看到已提交的修改)
COMMIT;
SERIALIZABLE #
sql
-- 最高隔离级别,完全隔离并发事务
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- 如果检测到序列化冲突,会抛出错误
-- ERROR: could not serialize access due to concurrent update
-- 应用需要处理这个错误并重试
MVCC 多版本并发控制 #
MVCC 原理 #
text
┌─────────────────────────────────────────────────────────────┐
│ MVCC 工作原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 每行数据包含: │
│ ├── xmin - 插入该行的事务 ID │
│ └── xmax - 删除/更新该行的事务 ID │
│ │
│ 事务可见性规则: │
│ 1. xmin 已提交且 xmin < 当前事务 ID │
│ 2. xmax 为空或 xmax 未提交或 xmax >= 当前事务 ID │
│ │
│ 示例: │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ id | name | balance | xmin | xmax │ │
│ ├────┼───────┼─────────┼──────┼────── │ │
│ │ 1 | Alice | 1000 | 100 | │ │
│ │ 1 | Alice | 1500 | 150 | (更新后新版本) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 事务 120 看到:balance = 1000(旧版本) │
│ 事务 160 看到:balance = 1500(新版本) │
│ │
└─────────────────────────────────────────────────────────────┘
查看事务信息 #
sql
-- 查看当前事务 ID
SELECT txid_current();
-- 查看行的版本信息
SELECT
id,
name,
xmin,
xmax,
ctid
FROM employees;
-- ctid 是行的物理位置 (page_number, item_number)
锁机制 #
锁类型 #
text
┌─────────────────────┬─────────────────────────────────────────┐
│ 锁类型 │ 说明 │
├─────────────────────┼─────────────────────────────────────────┤
│ ACCESS SHARE │ SELECT 请求 │
│ ROW SHARE │ SELECT FOR UPDATE/SHARE │
│ ROW EXCLUSIVE │ UPDATE, DELETE, INSERT │
│ SHARE UPDATE EXCLUSIVE│ VACUUM, ALTER TABLE │
│ SHARE │ 创建外键时获取 │
│ SHARE ROW EXCLUSIVE │ 并发更新 │
│ EXCLUSIVE │ 阻塞 ROW SHARE │
│ ACCESS EXCLUSIVE │ ALTER TABLE, DROP TABLE, VACUUM FULL │
└─────────────────────┴─────────────────────────────────────────┘
表级锁 #
sql
-- 显式获取表锁
LOCK TABLE employees IN ACCESS SHARE MODE;
LOCK TABLE employees IN ROW SHARE MODE;
LOCK TABLE employees IN ROW EXCLUSIVE MODE;
LOCK TABLE employees IN SHARE MODE;
LOCK TABLE employees IN EXCLUSIVE MODE;
LOCK TABLE employees IN ACCESS EXCLUSIVE MODE;
-- NOWAIT 选项(如果无法获取锁则立即报错)
LOCK TABLE employees IN ACCESS EXCLUSIVE MODE NOWAIT;
行级锁 #
sql
-- FOR UPDATE:排他行锁
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
-- FOR NO KEY UPDATE:非键排他锁
SELECT * FROM employees WHERE id = 1 FOR NO KEY UPDATE;
-- FOR SHARE:共享行锁
SELECT * FROM employees WHERE id = 1 FOR SHARE;
-- FOR KEY SHARE:键共享锁
SELECT * FROM employees WHERE id = 1 FOR KEY SHARE;
-- NOWAIT 选项
SELECT * FROM employees WHERE id = 1 FOR UPDATE NOWAIT;
-- SKIP LOCKED:跳过已锁定的行
SELECT * FROM employees WHERE status = 'pending'
FOR UPDATE SKIP LOCKED LIMIT 10;
查看锁信息 #
sql
-- 查看当前锁
SELECT
pid,
locktype,
relation::regclass AS table_name,
mode,
granted
FROM pg_locks
WHERE relation IS NOT NULL;
-- 查看阻塞的查询
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.relation = blocked_locks.relation
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;
-- 终止阻塞的查询
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid = <blocking_pid>;
死锁 #
死锁示例 #
sql
-- 会话 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 会话 2(同时执行)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2;
-- 会话 1 继续执行
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 等待...
-- 会话 2 继续执行
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
-- 死锁!其中一个会话会收到错误:
-- ERROR: deadlock detected
-- PostgreSQL 会自动检测并解决死锁
避免死锁 #
sql
-- 1. 按相同顺序访问资源
-- 总是先锁定 id 小的记录
-- 2. 使用 NOWAIT 检测
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- 如果无法获取锁,立即报错
-- 3. 设置锁超时
SET lock_timeout = '5s';
-- 4. 减少事务持有锁的时间
-- 尽量缩短事务时间
并发问题示例 #
丢失更新 #
sql
-- 问题:两个事务同时更新同一行
-- 会话 1
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- balance: 1000
-- 计算新余额...
UPDATE accounts SET balance = 1050 WHERE id = 1;
COMMIT;
-- 会话 2(同时执行)
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- balance: 1000
-- 计算新余额...
UPDATE accounts SET balance = 1100 WHERE id = 1;
COMMIT;
-- 最终 balance = 1100,会话 1 的更新丢失
-- 解决方案:使用 FOR UPDATE
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 加锁
-- 计算新余额...
UPDATE accounts SET balance = 1050 WHERE id = 1;
COMMIT;
队列处理 #
sql
-- 使用 SKIP LOCKED 实现任务队列
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
data JSONB,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 消费者获取任务
BEGIN;
UPDATE tasks
SET status = 'processing'
WHERE id = (
SELECT id FROM tasks
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *;
COMMIT;
-- 多个消费者可以并发处理,不会获取到相同的任务
事务最佳实践 #
事务设计原则 #
text
┌─────────────────────────────────────────────────────────────┐
│ 事务最佳实践 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 保持事务简短 │
│ - 减少持有锁的时间 │
│ - 避免在事务中执行耗时操作 │
│ │
│ 2. 合理选择隔离级别 │
│ - 大多数场景 READ COMMITTED 足够 │
│ - 需要一致性读时使用 REPEATABLE READ │
│ │
│ 3. 避免长事务 │
│ - 长事务会阻塞 VACUUM │
│ - 导致表膨胀 │
│ │
│ 4. 处理序列化失败 │
│ - SERIALIZABLE 隔离级别可能失败 │
│ - 应用需要重试逻辑 │
│ │
│ 5. 使用适当的锁 │
│ - 不要过度使用显式锁 │
│ - 让数据库自动管理锁 │
│ │
└─────────────────────────────────────────────────────────────┘
监控事务 #
sql
-- 查看长时间运行的事务
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes';
-- 查看空闲事务
SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction';
-- 设置空闲事务超时
SET idle_in_transaction_session_timeout = '10min';
学习路径 #
text
高级阶段
├── 索引
├── 视图
├── 存储过程
├── 触发器
└── 事务与锁(本文)
运维阶段
├── 用户权限管理
├── 备份与恢复
└── 性能优化
下一步 #
掌握了事务与锁后,接下来学习 用户权限管理,了解如何管理数据库安全!
最后更新:2026-03-29