PostgreSQL 事务与锁
事务解决的是“多个操作必须一起成功或一起失败”,锁解决的是“多个事务同时操作数据时,谁先执行、谁等待”。
PostgreSQL 的并发控制核心是:
MVCC 负责让读写尽量互不阻塞,锁负责处理真正冲突的数据操作,隔离级别负责决定事务能够看到什么。
事务是一组不可分割的数据库操作。
例如卖出股票需要:
- 扣减持仓
- 写入成交记录
- 更新账户资金
- 写入资金流水
这些操作必须全部成功,否则全部撤销。
BEGIN;
UPDATE trading.position
SET quantity = quantity - 100
WHERE account_id = 1
AND security_id = 1001;
INSERT INTO trading.trade_log (
account_id,
security_id,
trade_type,
quantity
)
VALUES (1, 1001, 'SELL', 100);
UPDATE trading.account
SET available_cash = available_cash + 150000
WHERE account_id = 1;
COMMIT;
如果中间出错:
ROLLBACK;
前面已经执行的修改全部撤销。PostgreSQL 还会把每条未显式包含在 BEGIN 中的 SQL,当作一个隐式事务执行。PostgreSQL 18:事务
事务中的操作:
- 要么全部成功
- 要么全部失败
不能只扣持仓,却没有增加资金。
事务执行前后,数据必须符合约束和业务规则。
例如:
quantity bigint NOT NULL CHECK (quantity >= 0)
一致性并不意味着数据库自动理解全部业务。必须结合:
- 主键
- 唯一约束
- 外键
CHECK- 正确的 SQL
- 锁或串行化隔离级别
两个事务同时运行时,不应看到对方未提交的中间状态。
事务成功提交后,在默认持久化配置下,修改会通过 WAL 持久记录;数据库即使异常重启,也能恢复已提交事务。
BEGIN;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES ('SSE', '600519', '贵州茅台');
COMMIT;
也可以写:
START TRANSACTION;
BEGIN;
UPDATE market.security
SET name = '错误名称'
WHERE code = '600519';
ROLLBACK;
更新不会生效。
BEGIN;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES ('SSE', '600519', '重复证券');
-- 如果违反唯一约束,事务进入 aborted 状态
SELECT * FROM market.security;
随后通常会看到:
ERROR: current transaction is aborted,
commands ignored until end of transaction block
必须执行:
ROLLBACK;
不能只忽略错误继续执行。
PostgreSQL 没有任意层级的真正嵌套事务,但可以使用保存点局部回滚。
BEGIN;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES ('SSE', '600519', '贵州茅台');
SAVEPOINT before_second_row;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES ('SSE', '600519', '重复数据');
-- 第二条失败后,回到保存点
ROLLBACK TO SAVEPOINT before_second_row;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES ('SZSE', '000001', '平安银行');
COMMIT;
结果:
- 第一条保留
- 重复数据撤销
- 第三条正常提交
不再需要保存点时:
RELEASE SAVEPOINT before_second_row;
保存点特别适合批量导入时跳过少量错误数据。不过每行一个保存点也有成本,大批量导入更适合暂存表、COPY 和集合化 SQL。
如果表使用:
security_id bigint GENERATED ALWAYS AS IDENTITY
事务获取了 ID 1001,随后回滚,下一次插入可能直接使用 1002。
因此主键出现空号是正常现象:
999, 1000, 1002, 1003
不要使用自增 ID 判断:
- 表中有多少数据
- 是否删除过记录
- 事务是否全部成功
- 严格的业务先后顺序
PostgreSQL 的序列变更对其他事务立即可见,而且不会因事务回滚而恢复。PostgreSQL 18:事务隔离
PostgreSQL 使用 MVCC 管理并发。
假设一行数据为:
security_id = 1001
quantity = 1000
事务 A 更新:
UPDATE trading.position
SET quantity = 900
WHERE security_id = 1001;
PostgreSQL 通常不是直接覆盖原来的行版本,而是生成新版本:
旧版本:quantity = 1000
新版本:quantity = 900
在事务 A 提交前:
- 事务 A 能看到 900
- 其他普通查询仍看到符合自身快照的旧版本 1000
- 其他事务看不到未提交的 900
所以 PostgreSQL 通常表现为:
普通读不阻塞写,写也不阻塞普通读。
但下面这些操作仍可能互相阻塞:
- 两个事务修改同一行
SELECT ... FOR UPDATE- 显式表锁
- 某些 DDL
TRUNCATEVACUUM FULL
旧版本最终由 VACUUM 清理。长时间不结束的事务可能导致旧版本无法及时回收,产生表膨胀。
PostgreSQL 支持四个标准名称,但内部只有三个不同级别:
| 隔离级别 | PostgreSQL 行为 | 快照范围 |
|---|---|---|
| Read Uncommitted | 等同于 Read Committed | 每条语句 |
| Read Committed | 默认级别 | 每条语句 |
| Repeatable Read | PostgreSQL 快照隔离 | 整个事务 |
| Serializable | 最严格,可串行化 | 整个事务并检测异常 |
| 现象 | 含义 |
|---|---|
| 脏读 | 读取其他事务尚未提交的数据 |
| 不可重复读 | 同一事务两次读取同一行,结果不同 |
| 幻读 | 同一条件两次查询,结果集行数发生变化 |
| 串行化异常 | 并发结果无法对应任何一种串行执行顺序 |
PostgreSQL 中:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 串行化异常 |
|---|---|---|---|---|
| Read Committed | 不会 | 可能 | 可能 | 可能 |
| Repeatable Read | 不会 | 不会 | 不会 | 可能 |
| Serializable | 不会 | 不会 | 不会 | 不会 |
PostgreSQL 的 Repeatable Read 比 SQL 标准的最低要求更强,不允许幻读。PostgreSQL 18:隔离级别
默认级别:
SHOW transaction_isolation;
通常返回:
read committed
它的关键特点是:
每条 SQL 开始时获取一个新快照。
会话 A:
BEGIN;
SELECT quantity
FROM trading.position
WHERE account_id = 1
AND security_id = 1001;
第一次返回:
1000
会话 B:
BEGIN;
UPDATE trading.position
SET quantity = 1200
WHERE account_id = 1
AND security_id = 1001;
COMMIT;
会话 A 再查询:
SELECT quantity
FROM trading.position
WHERE account_id = 1
AND security_id = 1001;
可能返回:
1200
因为第二条 SELECT 获得了新快照。
Read Committed 适合大多数普通业务:
- 单行增删改查
- 基于主键更新
INSERT ... ON CONFLICT- 短事务
BEGIN ISOLATION LEVEL REPEATABLE READ;
在该事务中,多次查询看到一致的数据快照:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT *
FROM market.daily_quote
WHERE trade_date = DATE '2026-09-14';
-- 其他事务即使提交了新数据
-- 当前事务重新查询时仍使用原快照
SELECT *
FROM market.daily_quote
WHERE trade_date = DATE '2026-09-14';
COMMIT;
适合:
- 同一报表中的多个查询必须基于一致数据
- 多步骤统计
- 数据导出
- 需要稳定快照的分析
如果事务尝试修改一个在事务开始后被其他事务修改过的行,可能收到:
ERROR: could not serialize access due to concurrent update
此时需要回滚并重试整个事务。
BEGIN ISOLATION LEVEL SERIALIZABLE;
Serializable 并不是让全部事务真的排队逐个运行。PostgreSQL 仍允许并发执行,但会监控读写依赖;如果结果不可能对应某种串行执行顺序,就中止其中一个事务。
例如账户风险上限是 100 万:
- 当前总持仓为 80 万
- 事务 A 读取总持仓,增加股票 A 15 万
- 事务 B 同时读取总持仓,增加股票 B 15 万
- 两个事务各自认为最终只有 95 万
- 实际提交后可能成为 110 万
Repeatable Read 仍可能出现这种“写偏差”;Serializable 会尝试检测,并让其中一个事务失败:
ERROR: could not serialize access due to
read/write dependencies among transactions
应用必须针对 SQLSTATE 40001 重试整个事务。
适合:
- 跨多行、跨多表的业务不变量
- 复杂额度检查
- 无法通过唯一约束或单条原子 SQL 保证的规则
不应该简单地把所有业务都改成 Serializable,因为它有额外监控成本,并要求应用实现可靠重试。
BEGIN TRANSACTION
ISOLATION LEVEL SERIALIZABLE
READ ONLY
DEFERRABLE;
数据库可能先等待一个安全快照。一旦开始执行,这类事务适合要求高一致性的长时间只读统计。
下面是一种危险的应用逻辑:
SELECT quantity
FROM trading.position
WHERE account_id = 1
AND security_id = 1001;
两个应用同时读到:
quantity = 100
然后:
- 应用 A 计算为 80,执行
SET quantity = 80 - 应用 B 计算为 70,执行
SET quantity = 70
最终 A 的更新可能被 B 覆盖。
如果只是加减,尽量让数据库直接计算:
UPDATE trading.position
SET quantity = quantity - 20
WHERE account_id = 1
AND security_id = 1001;
并发事务修改同一行时,后一个事务会等待,然后基于最新行版本继续更新。
防止持仓变成负数:
UPDATE trading.position
SET quantity = quantity - 100
WHERE account_id = 1
AND security_id = 1001
AND quantity >= 100
RETURNING quantity;
如果返回零行,说明:
- 持仓不足
- 持仓不存在
- 或并发事务已经先卖出了持仓
这种“条件检查和更新写在一条 SQL 中”的方式,通常比先查再改安全。
如果后续计算比较复杂:
BEGIN;
SELECT quantity
FROM trading.position
WHERE account_id = 1
AND security_id = 1001
FOR UPDATE;
-- 应用检查数量、手续费、风控规则
UPDATE trading.position
SET quantity = quantity - 100
WHERE account_id = 1
AND security_id = 1001;
INSERT INTO trading.trade_log (...);
COMMIT;
表中增加版本号:
version integer NOT NULL DEFAULT 0
更新时携带旧版本:
UPDATE trading.position
SET quantity = 900,
version = version + 1
WHERE account_id = 1
AND security_id = 1001
AND version = 5
RETURNING version;
如果返回零行,说明数据已经被其他事务修改,应重新读取并重试。
PostgreSQL 支持四种显式行锁:
| 行锁 | 强度 | 常见用途 |
|---|---|---|
FOR UPDATE |
最强 | 即将修改或删除该行 |
FOR NO KEY UPDATE |
较强 | 修改非关键字段 |
FOR SHARE |
共享 | 允许其他共享读取锁,阻止修改 |
FOR KEY SHARE |
最弱 | 防止删除或修改关键键值 |
行锁不会阻塞普通 SELECT,但会阻塞对同一行的不兼容修改或加锁操作。锁一般一直持有到事务提交或回滚。PostgreSQL 18:显式锁与行锁
BEGIN;
SELECT *
FROM trading.position
WHERE account_id = 1
AND security_id = 1001
FOR UPDATE;
其他事务仍能普通查询这行,但以下操作可能等待:
UPDATE ...
DELETE ...
SELECT ... FOR UPDATE
SELECT ... FOR SHARE
SELECT *
FROM trading.position
WHERE account_id = 1
AND security_id = 1001
FOR NO KEY UPDATE;
它比 FOR UPDATE 弱,适合不会修改主键或可供外键引用的唯一键的场景。普通 UPDATE 非关键字段时,PostgreSQL通常取得这种锁。
SELECT *
FROM market.security
WHERE security_id = 1001
FOR SHARE;
允许其他事务取得共享锁,但阻止对该行进行更新或删除。
SELECT *
FROM market.security
WHERE security_id = 1001
FOR KEY SHARE;
主要用于保护被外键引用的键:
- 阻止删除该行
- 阻止修改关键键值
- 允许修改不影响键的其他字段
不愿意等待锁时:
SELECT *
FROM trading.position
WHERE account_id = 1
AND security_id = 1001
FOR UPDATE NOWAIT;
如果行已经被锁,立即报错,而不是持续等待。
适合:
- 用户立即操作
- 尝试抢占资源
- 应用能够提示“记录正在处理中”
SELECT *
FROM trading.import_job
WHERE status = 'pending'
ORDER BY created_at, job_id
FOR UPDATE SKIP LOCKED
LIMIT 1;
已经被其他工作进程锁住的任务会被跳过。
更完整的任务领取方式:
BEGIN;
WITH picked AS (
SELECT job_id
FROM trading.import_job
WHERE status = 'pending'
ORDER BY created_at, job_id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE trading.import_job AS j
SET status = 'running',
started_at = clock_timestamp()
FROM picked
WHERE j.job_id = picked.job_id
RETURNING j.*;
COMMIT;
SKIP LOCKED 提供的是不一致视图,不适合普通业务查询,主要用于多消费者任务队列。PostgreSQL 18:SELECT 锁定子句
领取任务后应尽快提交,再执行耗时的网页抓取或数据处理,不要为了“占住任务”长时间保持数据库事务。
SELECT
p.account_id,
p.quantity,
s.code,
s.name
FROM trading.position AS p
JOIN market.security AS s
ON s.security_id = p.security_id
WHERE p.account_id = 1
FOR UPDATE OF p;
OF p 表示只锁持仓表对应的行,不锁证券基础信息。
多表查询使用锁定子句时,建议明确指定要锁的表,避免扩大锁范围。
即使操作的是一行,PostgreSQL 通常也会自动取得相应的表级锁。
注意:
ROW SHARE、ROW EXCLUSIVE名字中虽然有 ROW,但它们仍是表级锁。
常见模式:
| 表级锁 | 自动获取它的常见操作 |
|---|---|
ACCESS SHARE |
普通 SELECT |
ROW SHARE |
SELECT ... FOR UPDATE/SHARE |
ROW EXCLUSIVE |
INSERT、UPDATE、DELETE、MERGE |
SHARE UPDATE EXCLUSIVE |
VACUUM、ANALYZE、CREATE INDEX CONCURRENTLY |
SHARE |
普通 CREATE INDEX |
SHARE ROW EXCLUSIVE |
CREATE TRIGGER、部分 ALTER TABLE |
EXCLUSIVE |
REFRESH MATERIALIZED VIEW CONCURRENTLY |
ACCESS EXCLUSIVE |
DROP、TRUNCATE、VACUUM FULL、普通 REINDEX、很多 DDL |
最重要的一点:
只有
ACCESS EXCLUSIVE会阻塞普通SELECT。
因此生产环境执行以下操作要格外谨慎:
TRUNCATE TABLE market.daily_quote;
VACUUM FULL market.daily_quote;
ALTER TABLE market.daily_quote ...;
即使 DDL 本身执行很快,也可能先长时间等待已有事务结束;一旦进入锁等待队列,又可能影响后续会话。
BEGIN;
LOCK TABLE market.security
IN SHARE MODE
NOWAIT;
-- 执行需要表级一致性的操作
COMMIT;
显式表锁会显著降低并发能力,一般只用于:
- 特殊批处理
- 维护操作
- 无法使用行锁或 Serializable 保证的场景
死锁是两个事务互相等待。
会话 A:
BEGIN;
UPDATE trading.position
SET quantity = quantity + 100
WHERE security_id = 1001;
-- 等待一会儿
UPDATE trading.position
SET quantity = quantity + 100
WHERE security_id = 1002;
会话 B:
BEGIN;
UPDATE trading.position
SET quantity = quantity + 100
WHERE security_id = 1002;
-- 等待一会儿
UPDATE trading.position
SET quantity = quantity + 100
WHERE security_id = 1001;
形成:
A 持有 1001,等待 1002
B 持有 1002,等待 1001
PostgreSQL 会自动检测死锁,并中止其中一个事务:
ERROR: deadlock detected
SQLSTATE 为:
40P01
SELECT *
FROM trading.position
WHERE security_id IN (1001, 1002)
ORDER BY security_id
FOR UPDATE;
所有代码统一按 security_id 从小到大锁定。
不要在事务中:
- 等用户输入
- 调用外部 HTTP API
- 执行耗时文件操作
- 长时间休眠
- 进行复杂的非数据库计算
不要先取得较弱锁,再在不同代码路径中以不同顺序升级。
死锁无法在所有场景下完全避免。应用必须能够回滚并重试整个事务,而不是只重试最后一条 SQL。
PostgreSQL 官方同样建议按一致顺序获取多个对象的锁,并让应用能够处理死锁重试。PostgreSQL 18:死锁
默认情况下,如果没有死锁,事务可能持续等待锁。
可以在事务中设置超时:
BEGIN;
SET LOCAL lock_timeout = '3s';
SET LOCAL statement_timeout = '30s';
SET LOCAL transaction_timeout = '60s';
SELECT *
FROM trading.position
WHERE account_id = 1
AND security_id = 1001
FOR UPDATE;
COMMIT;
区别:
| 参数 | 控制范围 |
|---|---|
lock_timeout |
单次获取锁最多等待多久 |
statement_timeout |
一条 SQL 最多执行多久 |
transaction_timeout |
整个事务最多持续多久 |
idle_in_transaction_session_timeout |
事务打开但客户端没有继续发送 SQL 的时间 |
例如:
SET idle_in_transaction_session_timeout = '2min';
生产环境尤其应防范 idle in transaction,因为它可能:
- 长时间持有锁
- 阻止 VACUUM 清理旧行
- 导致表膨胀
- 阻塞 DDL
这些超时值应按接口、批处理和管理会话分别配置,不建议不加区分地设置成全库相同值。PostgreSQL 18:客户端连接与事务超时
咨询锁用于锁定一个由应用定义的“逻辑资源”,而不一定是某一行数据。
例如,同一只股票同一时刻只允许一个行情同步任务运行:
BEGIN;
SELECT pg_advisory_xact_lock(1, 1001);
-- 同步 security_id = 1001 的行情
COMMIT;
这里:
1可以表示“行情同步”命名空间1001表示证券 ID
事务级咨询锁会在 COMMIT 或 ROLLBACK 时自动释放。
不等待版本:
SELECT pg_try_advisory_xact_lock(1, 1001);
返回:
true:取得锁false:其他事务已经持有锁
还有会话级锁:
SELECT pg_advisory_lock(1001);
SELECT pg_advisory_unlock(1001);
会话级锁不会因为普通事务回滚而释放,连接池中使用不当容易残留,因此业务代码通常优先使用:
pg_advisory_xact_lock(...)
咨询锁只是约定,数据库不知道这个数字对应什么业务对象。所有相关应用代码都必须遵守同一套锁规则。PostgreSQL 18:咨询锁函数
SELECT
pid,
usename,
application_name,
state,
xact_start,
now() - xact_start AS transaction_age,
wait_event_type,
wait_event,
pg_blocking_pids(pid) AS blocking_pids,
left(query, 200) AS query
FROM pg_stat_activity
WHERE datname = current_database()
AND (
wait_event_type = 'Lock'
OR state LIKE 'idle in transaction%'
)
ORDER BY xact_start NULLS LAST;
WITH blocked AS (
SELECT
pid AS waiting_pid,
unnest(pg_blocking_pids(pid)) AS blocking_pid
FROM pg_stat_activity
)
SELECT
w.pid AS waiting_pid,
w.usename AS waiting_user,
now() - w.query_start AS waiting_duration,
left(w.query, 150) AS waiting_query,
b.pid AS blocking_pid,
b.usename AS blocking_user,
b.state AS blocking_state,
now() - b.xact_start AS blocking_transaction_age,
left(b.query, 150) AS blocking_query
FROM blocked
JOIN pg_stat_activity AS w
ON w.pid = blocked.waiting_pid
JOIN pg_stat_activity AS b
ON b.pid = blocked.blocking_pid
ORDER BY waiting_duration DESC;
SELECT
l.pid,
l.locktype,
l.mode,
l.granted,
l.waitstart,
c.relname
FROM pg_locks AS l
LEFT JOIN pg_class AS c
ON c.oid = l.relation
WHERE l.granted IS FALSE
ORDER BY l.waitstart;
pg_locks.granted = false 表示正在等待。需要注意,行锁信息主要存储在数据行中,因此行级锁等待经常表现为等待另一个事务 ID,而不是直接显示为某个 tuple 锁。PostgreSQL 18:pg_locks
SELECT
datname,
deadlocks,
stats_reset
FROM pg_stat_database
WHERE datname = current_database();
数据库管理员还可以启用:
log_lock_waits = on
记录超过 deadlock_timeout 的锁等待,用于定位线上阻塞问题。
取消当前 SQL:
SELECT pg_cancel_backend(12345);
但取消 SQL 不一定立即释放事务已经取得的锁。如果连接仍处于未提交或 aborted 事务中,锁可能继续保留。
终止整个连接:
SELECT pg_terminate_backend(12345);
连接终止后,未提交事务会回滚并释放锁。
这属于管理操作,执行前必须确认:
- PID 对应的用户和应用
- 当前 SQL
- 事务持续时间
- 是否为关键批处理
- 回滚可能持续多久
索引不只影响查询性能,也会间接影响锁竞争。
例如:
UPDATE trading.position
SET quantity = quantity + 100
WHERE account_id = 1
AND security_id = 1001;
适合的索引:
PRIMARY KEY (account_id, security_id)
数据库可以迅速找到目标行。
如果没有合适索引:
- 扫描时间更长
- 事务持续时间更长
- 已取得的锁更晚释放
- 与其他事务重叠的概率更高
- 更容易形成锁等待和死锁
但索引不能消除两个事务修改同一行时的冲突。
from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(
bind=engine,
expire_on_commit=False,
)
with SessionLocal.begin() as session:
session.add(trade_log)
position.quantity -= 100
正常退出时自动提交;出现异常时自动回滚并关闭 Session。
try:
with SessionLocal.begin() as session:
...
except Exception:
# 此时事务已经回滚
raise
不要捕获异常后什么都不做,否则事务上下文可能误以为业务正常完成。
SQLAlchemy 2.0 的 Session 采用 autobegin:执行数据库操作时会自动开始事务;flush() 只是把 SQL 发送到数据库,并不等于提交,锁仍会持有到最终 commit() 或 rollback()。SQLAlchemy 2.0:事务管理
from sqlalchemy import select
with SessionLocal.begin() as session:
stmt = (
select(Position)
.where(
Position.account_id == 1,
Position.security_id == 1001,
)
.with_for_update()
)
position = session.execute(stmt).scalar_one()
if position.quantity < 100:
raise ValueError("持仓不足")
position.quantity -= 100
session.add(
TradeLog(
account_id=1,
security_id=1001,
trade_type="SELL",
quantity=100,
)
)
不等待锁:
stmt = stmt.with_for_update(nowait=True)
跳过已锁任务:
stmt = stmt.with_for_update(skip_locked=True)
这些参数会分别生成 PostgreSQL 的 NOWAIT 和 SKIP LOCKED 子句。SQLAlchemy 2.0:with_for_update
可以创建专用 Engine:
serializable_engine = engine.execution_options(
isolation_level="SERIALIZABLE",
)
SerializableSession = sessionmaker(
bind=serializable_engine,
expire_on_commit=False,
)
然后:
with SerializableSession.begin() as session:
...
隔离级别应在事务真正开始前设置,不要在已经执行查询后再修改。
import random
import time
from sqlalchemy.exc import DBAPIError
RETRYABLE_CODES = {
"40001", # serialization_failure
"40P01", # deadlock_detected
}
def run_transaction(operation, max_attempts: int = 3):
for attempt in range(max_attempts):
try:
with SerializableSession.begin() as session:
return operation(session)
except DBAPIError as exc:
code = (
getattr(exc.orig, "sqlstate", None)
or getattr(exc.orig, "pgcode", None)
)
if code not in RETRYABLE_CODES:
raise
if attempt + 1 >= max_attempts:
raise
delay = 0.05 * (2 ** attempt)
delay += random.uniform(0, 0.05)
time.sleep(delay)
必须重试整个事务,而不是只重试失败语句。
被重试的事务还应具有幂等性。发送短信、HTTP 请求、消息通知等外部副作用,最好安排在成功提交之后,或者采用事务 Outbox 模式,防止重试造成重复发送。
| 场景 | 推荐方案 |
|---|---|
| 扣减单行持仓 | 条件原子 UPDATE ... RETURNING |
| 先检查再修改复杂持仓 | SELECT ... FOR UPDATE |
| 用户编辑低冲突资料 | version 乐观锁 |
| 多行额度、风险上限 | Serializable 或显式锁 |
| 多工作进程领取抓取任务 | FOR UPDATE SKIP LOCKED |
| 同一股票禁止重复同步 | 事务级 advisory lock |
| 一致的行情统计报表 | Repeatable Read |
| 高要求只读报表 | Serializable Read Only Deferrable |
| 大批量日行情导入 | 有界批次事务,分批提交 |
TimescaleDB hypertable 仍遵循 PostgreSQL 的事务和锁语义。导入大量行情时不要用一个持续数小时的大事务;它可能长期保留旧快照和锁,并影响 VACUUM、后台策略及其他写入任务。
- 默认先使用 Read Committed。
- 能用单条原子 SQL,就不要先查再改。
- 复杂读后写场景使用
FOR UPDATE。 - 跨行、跨表业务规则考虑 Serializable。
- Serializable 和死锁失败必须重试整个事务。
- 多行加锁始终使用一致顺序。
- 事务尽量短,不在事务中等待用户或外部 API。
- 使用
lock_timeout和statement_timeout限制无限等待。 - 监控
idle in transaction。 - 给定位和连接条件建立正确索引。
- 优先使用事务级咨询锁,谨慎使用会话级咨询锁。
- 把主键空号视为正常现象。
- SQLAlchemy 中优先使用
with Session.begin()。 flush()不等于commit()。- 锁住的不是越多越安全,锁范围越精确越好。