bigywater.tech
Flask · 95

悲观锁

悲观锁怎么写?拿不到锁时有哪几种处理?用它做任务队列安全吗?死锁是怎么发生的、怎么避免?这篇在真实的 PostgreSQL 上用多线程把每种情况都跑一遍。

160 / 160
8 线程各加 20 次,FOR UPDATE 下的最终值
1 ms
nowait 与 skip_locked 在别人持锁时的返回时间
约 1 秒
相反加锁顺序时,PostgreSQL 检出死锁的时间

with_for_update 的 SQL

实测 · 本机执行输出
   SELECT counter.id, counter.n FROM counter WHERE counter.id = ... FOR UPDATE
  read=True → FOR SHARE
  nowait=True → FOR UPDATE NOWAIT
  skip_locked=True → FOR UPDATE SKIP LOCKED

上一篇:乐观锁;下一篇:SQLAlchemy 查询日志。

读改写

实测 · 本机执行输出
  期望 160,最终 n = 160,耗时 359 ms(没有重试,但线程在排队)

拿不到锁时

实测 · 本机执行输出
  nowait=True            → LockNotAvailable,用时 1 ms
  skip_locked=True       → 返回空(被跳过),用时 1 ms
  lock_timeout='500ms'   → LockNotAvailable,用时 505 ms
  默认(阻塞等待)         → 拿到了,用时 1011 ms(等持有者 1 秒后 commit)

任务队列

实测 · 本机执行输出
  每个 worker 拿到的任务: {0: [2, 5, 8], 1: [3, 6, 9], 2: [1, 4, 7]} | 全部 [1, 2, 3, 4, 5, 6, 7, 8, 9] | 有没有重复: False | 184 ms

死锁

实测 · 本机执行输出
  相反顺序: {'B': ('DeadlockDetected', '1004 ms'), 'A': ('成功', '1005 ms')}
  相同顺序(都先 1 后 2): {'B': '成功', 'A': '成功'}

两个线程各自的 Session,中间 sleep 0.3 秒保证两边都拿到了第一把锁。

分步演示

悲观锁
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、psycopg 3.3.6、自己起的 PostgreSQL(非默认端口)
    • 没有验证:MySQL(InnoDB 间隙锁)与 SQLite(不支持 FOR UPDATE)的行为、with_for_update(of=...) 多表连接时锁哪张表、advisory lock、SERIALIZABLE 隔离级别、锁等待在高并发下的吞吐
    京ICP备2026067502号-1京公网安备11010502063487号