创建时间: 2026-09-01最后更新: 2026-09-01

1. 并发

FastAPI 可以同时处理多个请求。并发请求可能通过连接池中的不同连接执行事务,即使我们没有在业务代码中手动创建线程,数据库仍然会同时面对多个读写操作。

假设两个请求同时提高任务优先级:

lost-update.txt
1
请求 A 读取 priority = 1
2
请求 B 读取 priority = 1
3
请求 A 写入 priority = 2
4
请求 B 写入 priority = 2

两个请求都报告成功,但最终只增加了一次,这叫作丢失更新。每个请求单独看都没有语法错误,问题来自它们交错执行。

为了让后面的示例不依赖固定 ID,先在当前 psql 会话中选择一条未归档任务:

select-concurrency-task.psql
1
SELECT id AS task_id
2
FROM tasks
3
WHERE archived_at IS NULL
4
ORDER BY id DESC
5
LIMIT 1
6
\gset

\gset 会把查询结果保存为 task_id 变量。真实 FastAPI 接口应该直接使用路径参数或请求数据中的任务 ID,并通过数据库驱动绑定参数。

如果操作可以写成数据库内的原子更新,优先让一条 SQL 完成:

atomic-update.psql
1
UPDATE tasks
2
SET
3
priority = LEAST(priority + 1, 3),
4
updated_at = now()
5
WHERE id = :task_id
6
AND priority < 3
7
RETURNING id, priority;

PostgreSQL 会为被更新的行进行必要的并发控制。两个请求同时执行时,其中一个会先更新,另一个随后基于该行的新版本继续计算,不会都使用应用之前读到的旧值。这里增加 priority < 3,到达上限后语句会影响零行,不会只修改 updated_at 却仍被接口当成提升成功。

原子 SQL 通常是处理简单并发写入的第一选择。它减少了应用与数据库之间的往返,也缩小了竞态出现的空间。

2. 锁

如果业务必须先读取多列、执行判断,再决定怎样写入,可以使用行锁:

lock-task.psql
01
BEGIN;
02
03
SELECT id, status, priority
04
FROM tasks
05
WHERE id = :task_id
06
FOR UPDATE;
07
08
UPDATE tasks
09
SET status = 'doing', updated_at = now()
10
WHERE id = :task_id
11
AND status = 'todo'
12
RETURNING id, status;
13
14
COMMIT;

FOR UPDATE 只锁定实际查询到的行,锁通常一直持有到当前事务提交或回滚。另一个事务如果想更新、删除或取得冲突的行锁,默认会等待;如果查询没有找到任务,也就没有任何任务行被锁定。若锁是在保存点之后取得的,回滚到该保存点时也会提前释放这部分锁。

行锁不会把整张表禁止访问。普通 SELECT 在 PostgreSQL 的多版本并发控制下通常仍能读取一个已经提交的版本,不会被 FOR UPDATE 阻塞,具体看到哪个版本取决于隔离级别。

取得锁和后续更新必须使用同一连接、处于同一事务。如果 SELECT ... FOR UPDATE 后立即提交,再从另一条连接执行 UPDATE,前面的行锁已经释放,无法保护后续操作。

行锁应该在事务中尽快完成使用。锁定后执行慢速网络请求,会让其他事务长时间等待,最终造成接口延迟和连接池堆积。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容