Laravel 8 PostgreSQL读写分离如何确认主从复制完成再执行后续操作
Laravel 8 对接PostgreSQL主从时复制延迟问题解决方案
PostgreSQL 主从异步复制架构下,写操作提交后从库存在同步延迟,此时请求路由到从库读刚写入的数据就会触发404、数据不存在等错误。要实现写操作后确认复制完成再执行后续逻辑,可根据业务场景选择以下方案:
方案1:基于PG原生WAL位点校验(和预期逻辑完全匹配)
PostgreSQL 每个写事务提交后都会生成唯一的WAL日志序列号(LSN),主库写入后拿到当前LSN,轮询从库的WAL回放进度,当从库回放LSN大于等于主库写入LSN时,就代表复制已经完成。
实现步骤:
- 先确认
config/database.php中pgsql连接的读写分离配置正确,暂时关闭sticky参数避免框架自动路由干扰:
'pgsql' => [ 'driver' => 'pgsql', 'read' => [ 'host' => [env('DB_READ_HOST', '127.0.0.1')], ], 'write' => [ 'host' => env('DB_WRITE_HOST', '127.0.0.1'), ], 'sticky' => false, // 其余端口、库名、账号密码配置保持不变 ],
- 封装通用的复制状态校验方法,可放到全局辅助函数或者BaseModel里:
/** * 等待从库同步追上主库指定位点 * @param string $primaryLsn 主库写入位点 * @param int $timeout 最大等待秒数 * @param int $intervalUs 轮询间隔(微秒) * @return bool 同步是否完成 */ function waitReplicationDone(string $primaryLsn, int $timeout = 2, int $intervalUs = 50000): bool { $startTime = microtime(true); // 拿到从库连接实例 $readPdo = DB::connection('pgsql')->getReadPdo(); while (microtime(true) - $startTime < $timeout) { // 查询从库当前回放的LSN位点 $replicaLsn = $readPdo->query("SELECT pg_last_wal_replay_lsn() AS lsn")->fetch(\PDO::FETCH_ASSOC)['lsn']; if ($replicaLsn) { // 位点对比:从库回放位点 >= 主库写入位点代表同步完成 $isDone = $readPdo->query("SELECT '{$primaryLsn}'::pg_lsn <= '{$replicaLsn}'::pg_lsn AS done")->fetch(\PDO::FETCH_ASSOC)['done']; if ($isDone) return true; } usleep($intervalUs); } // 超时未同步完成返回false,可根据业务降级走主库查询或抛出友好提示 return false; }
- 业务调用逻辑和预期写法完全对齐:
// 执行插入操作,需要拿自增ID用insertGetId即可 $userId = DB::table('users')->insertGetId([ // 写入字段 ]); // 从主库拿本次写入对应的WAL位点 $primaryLsn = DB::connection('pgsql')->getPdo() ->query("SELECT pg_current_wal_lsn() AS lsn") ->fetch(\PDO::FETCH_ASSOC)['lsn']; if (waitReplicationDone($primaryLsn)) { // 复制完成,后续查询走从库也能读到最新数据 $user = DB::table('users')->find($userId); // 执行后续业务逻辑 }
方案2:开启Laravel自带Sticky粘滞模式(同请求下零成本解决)
Laravel 8 原生支持读写分离场景下的sticky配置,开启后同一个请求生命周期内只要执行过写操作,后续所有读请求会自动路由到主库,完全不需要额外做复制校验,配置成本极低:
'pgsql' => [ 'driver' => 'pgsql', 'read' => [ 'host' => [env('DB_READ_HOST', '127.0.0.1')], ], 'write' => [ 'host' => env('DB_WRITE_HOST', '127.0.0.1'), ], 'sticky' => true, // 开启粘滞 // 其余配置不变 ],
注意:sticky模式仅作用于当前请求,比如写入后跳转到详情页的场景属于新的请求,sticky不会生效,这类跨请求的写后读场景需要用方案1或者方案3。
方案3:强一致场景强制走主库(最简单兜底)
对于写入后必须立刻读到最新数据的核心场景,不需要等待复制,直接在查询时指定使用写连接(主库)即可,没有额外轮询开销:
$userId = DB::table('users')->insertGetId([...]); // 强制走主库查询,完全不受从库延迟影响 $user = DB::table('users')->useWritePdo()->find($userId);
注意事项
- 轮询校验复制的方案超时时间不要设置过长,内网正常环境下PG主从复制延迟通常在几十毫秒级别,设置1-2秒超时即可,超时后直接降级走主库查询,避免阻塞用户请求。
- 不要全局给所有写操作加复制等待逻辑,只在确实存在写后读从库需求的场景使用,否则会整体拉高所有写接口的响应耗时。
内容的提问来源于stack exchange,提问作者Mikaele
相关产品推荐
相关产品推荐

