在PostgreSQL里,默认的READ COMMITTED隔离级别挡不住幻读。这不是配置错了,而是这个级别的定义本身决定的:每条查询语句都会读取一个新快照,并发事务提交的新数据,会直接改变你下一次查询看到的行数。

打开网易新闻 查看精彩图片

有人用Python和SQLAlchemy搭了一套自动化复现测试,目标很明确——用两个连接,一个读、一个写,把幻读当场抓出来。

测试的设计逻辑分三层:

  • 动态表生命周期管理:每轮测试都先拆掉目标表,重建,再灌入固定的种子数据;
  • 并发会话控制:通过sqlalchemy.create_engine拿到独立连接,让Conn1和Conn2各自持有明确的事务边界,模拟竞争条件;
  • 结构化结果序列化:验证指标和原始执行轨迹被打包成结构化数据,以格式化JSON输出到标准输出。

复现路径很直接。事务1以READ COMMITTED开启,第一次范围查询返回2行;事务1保持打开,事务2插入一条符合范围条件的新行并提交;事务1再查一次,结果变成3行。幻读就这样出现了,随后事务1回滚。

真正有意思的意外发生在数据库断言之前。测试运行器在目标环境里直接崩了,报错是ModuleNotFoundError: No module named 'orjson'。

根因不复杂:脚本在模块加载阶段就全局import orjson,而这个用Rust/C实现的高性能JSON库,在最小化运行容器里没有预装wheel,执行器也没有任何动态回退机制。并发和事务管理的代码写得再对,诊断工具在启动导入解析这一步就已经死了。

这暴露的是一个架构层面的错配:逻辑正确性和环境现实之间脱节。做基础设施评估和数据库诊断的命令行工具,硬性依赖编译型原生库、又不给标准库兜底,等于把运维流程做脆了。

修复有两条路。一条是严格前置打包,在pyproject.toml或requirements.txt里写死依赖,保证容器构建时先装好:

pip install orjson pydantic sqlalchemy psycopg2-binary

另一条更推荐:把高性能原生库当成可选增强,在临时、不可信或最小化环境里回退到Python内置的json模块。写法是try/except包住import,设一个HAS_ORJSON标志,输出函数里按标志分流——有orjson就走sys.stdout.buffer.write,没有就用json.dumps加缩进写标准输出。

回到隔离级别本身。PostgreSQL切到REPEATABLE READ后用的是快照隔离,快照冻结在事务开始那一刻,普通SELECT语句的经典幻读被挡住了。但并发事务如果在同一范围上做变更、或更新冲突行,序列化异常依然可能发生——除非显式加悲观行锁SELECT ... FOR UPDATE,或者上真正的可串行化:SERIALIZABLE隔离级别配合SSI(Serializable Snapshot Isolation)。

两条经验可以带走。诊断和验证类CLI工具要把零配置可移植性放在性能前面,性能库只当加速项,标准库做默认,才能在不同CI/CD节点和生产跳板机上稳定跑起来。另一条是别把隔离级别的名字当保证,READ COMMITTED每次查询换快照,行数变化是它的正常行为,不是bug。