Laravel读写分离延迟怎么处理

wen PHP项目 1

本文目录导读:

Laravel读写分离延迟怎么处理

  1. 强制读取主库(最直接)
  2. 手动指定读主库(请求级)
  3. 延迟读取 / 等待同步(时间戳方案)
  4. 使用“查询主库”标识(Cookie / Header)
  5. 架构级解决:使用单调读(Monotonic Read)
  6. 终极方案:使用无延迟的数据库
  7. 总结建议

在 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 后,该请求强制读主库。

实现思路

  1. AfterMiddleware 中,如果检测到有写操作,给响应设置 Cookie。
  2. BeforeMiddleware 中,如果请求携带该 Cookie,强制切到主库。

优点:可以跨请求,解决“写入后重定向”的问题。 缺点:需要修改前端/中间件;用户清除 Cookie 后可能失效。


架构级解决:使用单调读(Monotonic Read)

如果业务允许,可以将用户某个时间段的所有读操作绑定到同一个从库实例,只要不切换从库,用户就不会看到数据“回退”。

Laravel 自身没有内置这个功能,但可以通过自定义数据库连接池或使用云数据库(如 AWS Aurora)的“读取端点”来实现。


终极方案:使用无延迟的数据库

如果业务对一致性要求极高(如支付、库存),可以考虑:

  • 直接全部读主库:牺牲读写分离带来的性能提升,保证强一致性。
  • 使用 Aurora(或 Galera Cluster):它们的主从延迟通常在毫秒级(甚至写后立即读可见),几乎没有延迟。
  • Redis 缓存中间层:写入时同步更新 Redis,读取时优先读 Redis,延迟为零。

总结建议

场景 推荐方案
同一请求内写后读 sticky 配置(最简单,推荐首选)
跨请求写后读(如注册、支付回跳) 中间件 + Session 记录写入时间戳手动强制读主库
延迟极短(< 100ms) 继续使用默认读写分离,业务通常可接受
延迟较长(> 1s) 考虑数据库架构升级(如 Aurora),或在业务层增加等待/重试机制

最佳实践

  1. 先开 sticky 解决 90% 的问题。
  2. 对于极少数需要强一致性的接口(如创建订单后的立即查询),手动调用 DB::connection('mysql::write')
  3. 监控主从延迟,确保延迟在业务可接受范围内。

抱歉,评论功能暂时关闭!