本文目录导读:

- 强制读取主库(最直接)
- 手动指定读主库(请求级)
- 延迟读取 / 等待同步(时间戳方案)
- 使用“查询主库”标识(Cookie / Header)
- 架构级解决:使用单调读(Monotonic Read)
- 终极方案:使用无延迟的数据库
- 总结建议
在 Laravel 中处理读写分离延迟(主从延迟)是一个经典问题,核心难点在于:刚写入的数据,立即读取时,从库可能还没同步。
以下是几种常见的解决方案,按推荐程度和复杂度排序:
强制读取主库(最直接)
这是 Laravel 官方最推荐的解决方案,利用 sticky(粘性)选项。
在 config/database.php 的数据库连接配置中,将 sticky 设为 true:
// config/database.php
'mysql' => [
'read' => [
'host' => ['192.168.1.1', '192.168.1.2'], // 从库
],
'write' => [
'host' => ['192.168.1.3'], // 主库
],
'sticky' => true, // <--- 关键配置
'driver' => 'mysql',
// ...
],
原理:
- 当 Laravel 检测到当前请求(同一个请求生命周期内)有写过数据库的操作(INSERT、UPDATE、DELETE)。
- 当前请求中后续的所有读操作,Laravel 会自动强制使用写库连接(主库),而不是走从库。
- 下一个请求恢复正常读写分离。
优点:零代码侵入,配置即用。 缺点:只在同一个请求内有效,如果写入后立即重定向,用户页面加载时是一个新请求,仍可能读到从库的旧数据(用户注册后立即跳转到个人中心,看到资料不是最新的)。
手动指定读主库(请求级)
在写入操作后,明确告诉系统本次请求后续读主库。
DB::connection('mysql::write')
// 写入操作
$user = User::create([...]);
// 读取最新数据(强制使用写连接)
$freshUser = DB::connection('mysql::write')->table('users')->find($user->id);
fresh() 或 refresh()
针对 Eloquent 模型:
$user = User::create([...]); // 从数据库重新加载(默认会使用写连接吗?不一定,取决于配置) $user = $user->fresh(); // 建议配合 DB::connection 使用 // 或手动指定 $user = User::onWriteConnection()->find($user->id); // Laravel 早期的做法
优点:精确控制。 缺点:需要手动在每个关键业务点添加,容易遗漏,维护成本高。
延迟读取 / 等待同步(时间戳方案)
如果主从延迟通常很短(200ms),可以采用“以时间换一致性”的策略。
实现思路: 在 Redis 或 Session 中记录用户最后写入时间,读取时,判断当前请求时间与写入时间的时间差,如果小于某个阈值(500ms),强制读主库。
示例代码(中间件):
// 写入后记录时间
$user = User::create([...]);
Cache::put('user_last_write_'.$user->id, now(), 10);
// 查询中间件
public function handle($request, Closure $next)
{
if ($userId = auth()->id()) {
$lastWrite = Cache::get('user_last_write_'.$userId);
if ($lastWrite && $lastWrite->diffInSeconds(now()) < 1) {
// 1秒内刚写过,强制读主库
config(['database.connections.mysql.read.host' => config('database.connections.mysql.write.host')]);
}
}
return $next($request);
}
优点:自适应,不需要每个业务点都改。 缺点:引入了缓存依赖;如果延迟过长,阈值设置困难。
使用“查询主库”标识(Cookie / Header)
写入后,在响应中设置一个 Cookie(X-Last-Write: <timestamp>),前端请求时带回,后端检测到该 Cookie 后,该请求强制读主库。
实现思路:
- 在
AfterMiddleware中,如果检测到有写操作,给响应设置 Cookie。 - 在
BeforeMiddleware中,如果请求携带该 Cookie,强制切到主库。
优点:可以跨请求,解决“写入后重定向”的问题。 缺点:需要修改前端/中间件;用户清除 Cookie 后可能失效。
架构级解决:使用单调读(Monotonic Read)
如果业务允许,可以将用户某个时间段的所有读操作绑定到同一个从库实例,只要不切换从库,用户就不会看到数据“回退”。
Laravel 自身没有内置这个功能,但可以通过自定义数据库连接池或使用云数据库(如 AWS Aurora)的“读取端点”来实现。
终极方案:使用无延迟的数据库
如果业务对一致性要求极高(如支付、库存),可以考虑:
- 直接全部读主库:牺牲读写分离带来的性能提升,保证强一致性。
- 使用 Aurora(或 Galera Cluster):它们的主从延迟通常在毫秒级(甚至写后立即读可见),几乎没有延迟。
- Redis 缓存中间层:写入时同步更新 Redis,读取时优先读 Redis,延迟为零。
总结建议
| 场景 | 推荐方案 |
|---|---|
| 同一请求内写后读 | sticky 配置(最简单,推荐首选) |
| 跨请求写后读(如注册、支付回跳) | 中间件 + Session 记录写入时间戳 或 手动强制读主库 |
| 延迟极短(< 100ms) | 继续使用默认读写分离,业务通常可接受 |
| 延迟较长(> 1s) | 考虑数据库架构升级(如 Aurora),或在业务层增加等待/重试机制 |
最佳实践:
- 先开
sticky解决 90% 的问题。 - 对于极少数需要强一致性的接口(如创建订单后的立即查询),手动调用
DB::connection('mysql::write')。 - 监控主从延迟,确保延迟在业务可接受范围内。