概述
副本是可能落后几毫秒的只读拷贝,适合目录页、报表和搜索建议,不适合结算金额、授权判断或用户刚写完就要读到的数据。路由层必须把能力写清楚:readQuery 默认走副本,writeQuery 和事务始终走主库,pinToPrimary 表示当前请求已经跨过写边界。复制延迟保护每五秒采样一次主库的 pg_stat_replication,同时检查 replay_lag 和 WAL 字节差;副本断流或落后时回退主库。
步骤
先确认拓扑:
psql "$DATABASE_URL_PRIMARY" -Atc "SELECT pg_is_in_recovery();"
psql "$DATABASE_URL_REPLICA" -Atc "SELECT pg_is_in_recovery();"
psql "$DATABASE_URL_PRIMARY" -c "SELECT application_name, state, replay_lag, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind FROM pg_stat_replication;"
主库应返回 f,副本返回 t,复制行处于 streaming。创建两个连接池,副本连接设置 application_name: 'app-replica-1',并用 AsyncLocalStorage 保存请求状态。withRequestRouting(next) 在请求入口建立状态,pinToPrimary() 在写操作成功后置位。readQuery 若发现已固定或延迟保护判定副本不新鲜,就使用主库。
延迟查询使用:
SELECT EXTRACT(EPOCH FROM replay_lag) * 1000 AS lag_ms,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind
FROM pg_stat_replication
WHERE application_name = $1 AND state = 'streaming';
把结果缓存五秒;没有复制行、查询失败、延迟超过两秒或落后超过 16 MB 时都回退主库。写事务通过 primary.connect() 获取客户端,绝不复用副本连接。
验证
在 app.profiles 中插入一个测试用户,向其发送 PATCH 更新 display_name,并在同一请求内立即读回。响应必须已经是新值,因为 pinToPrimary() 强制后续读取走主库。再直接查询副本,复制追上后也应显示新值。若副本持续落后,先检查网络吞吐和长查询,不要直接调低阈值。
注意事项
迁移、序列分配和 advisory lock 必须留在主库。不要向副本发送 SET 后再假设后续查询会落到同一连接。为副本池单独设置上限,避免分析流量耗尽普通页面读取。请求级固定无法保证跨服务、跨设备的写后读一致性;需要时使用主库读取或传递版本令牌。副本不是备份,也不是自动故障转移方案。