LaravelのDBとしてAWSのAuroraを使っていてマスタースレーブ構成にしていたのですが、レプリケーション遅延により書き込んだはずのデータが見当たらないという事態が発生していました。
データを追加したユーザーが一覧ページに戻ってみると、追加したはずのデータが見当たらないというのはユーザー体験として良くないので、自分が取った対応方法について書いていこうと思います。
状況
コントローラの処理としては、以下のようなstoreアクションで書き込んだ後リダイレクト先のindexアクションで読み込みをしていて、この読み込みの際に書き込んだデータが反映されていないという現象が5回に1回程度発生します。
<?php
namespace App\Http\Controllers\Web\Professional;
use Illuminate\Routing\Controller;
use App\Models\Menu;
class MenuController extends Controller
{
public function index(FetchOrderedMenusService $service)
{
$menus = Menu::all();
return view('menu.index', compact('menus');
}
public function store(Request $request)
{
Menu::create($request->all());
return redirect()->route('menus.index');
}
}
Auroraのレプリカラグが20ms、書き込み処理からリダイレクトして読み込み直前までの時間を計測すると約100msなので、読み込みのタイミングでスレーブに反映されていないということは無い気がするのですが、たまたまレプリケーションに時間がかかったというのとリクエストの処理時間が早かったという状況が重なって発生しているのではと考えました。
実際に対策をすると今回の現象は発生しなくなりました。
対策
方針
config/database.php
のsticky
を有効化している場合だと、同一リクエスト上ではマスターに書き込みを行った後はマスターを参照するようになります。ただ、今回はリダイレクトしたときにマスターを参照して欲しいのでsticky
を有効化するだけでは対応できません。
sticky
を有効化している場合の処理を追ってみると、DB::recordsHaveBeenModified()
を呼び出すことでそれ以降の処理ではマスターを参照するようだったので、この関数を使って書き込み直後のリクエストでマスターを参照させます。
実装
以下のようなミドルウェアを作成し、既存の実装に影響が出ないようにします。
ミドルウェアの実装内容としては、DBに書き込みを行った直後のリクエストかどうかを判別できるように書き込みのタイミングでセッションに値を保存しておき、次のリクエストでセッションに値が存在すればDB::recordsHaveBeenModified()
を呼び出します。
<?php
namespace App\Http\Middleware;
use Closure;
use DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
class UseWriteConnectionWhenHaveBeenModified
{
public function handle(Request $request, Closure $next, ...$modelClassNames): mixed
{
$callback = function () {
session()->flash('haveBeenModified');
};
foreach ($modelClassNames as $modelClassName) {
if (!is_subclass_of($modelClassName, Model::class)) {
continue;
}
$modelClassName::created($callback);
$modelClassName::updated($callback);
$modelClassName::deleted($callback);
}
if (session()->has('haveBeenModified')) {
DB::recordsHaveBeenModified();
}
return $next($request);
}
}
書き込み直後のリクエストを処理し終わったらセッションから値が削除されて欲しいのでflash
関数を使っています。
作成したミドルウェアをコントローラから利用できるようにKernel.php
に追記します。
protected $routeMiddleware = [
...
'haveBeenModified' => \App\Http\Middleware\UseWriteConnectionWhenHaveBeenModified::class,
];
最後にコントローラのコンストラクタにミドルウェアを追加します。
public function __construct()
{
$this->middleware('haveBeenModified:' . Menu::class);
}
指定したeloquentモデルのデータに書き込み処理が行われると、直後のリクエストはマスターを参照するようになります。