2021.12.23
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ

どうも!師走のわいです!
クリスマスも近いので、今回は良質な記事をプレゼントします!
実装環境
- PHP 8.0.13
- Laravel Framework 8.51.0
- Amazon Aurora (MySQL 5.7 互換)
また、今回のサンプルはステートフルなアプリケーション向けの実装で、セッションを使用しています。
`sticky`とは
DBの負荷分散のために、下記設定をおこなうことはよくあると思います。
SELECT
はReaderに対して実行INSERT
UPDATE
DELETE
はWriterに対して実行
Laravelではこの設定を config/database.php
内で簡単におこなうことができます。
さらに sticky
オプションを有効にすると、INSERT
UPDATE
DELETE
を実行した同一のHTTPリクエスト上では以後の SELECT
をWriterに対して実行してくれます。
このおかげで、WriterからReaderへのデータ同期の遅延を考えずに、更新後の最新データを容易に取得することが可能になります。
参考:https://readouble.com/laravel/8.x/ja/database.html
しかし、今回この「同一のHTTPリクエスト上」というのが問題になりました。
「登録・編集フォームの値をPOSTして、問題なければDBに保存し、その後一覧画面にリダイレクトする」といったよくあるフローを実装しようとしたときに、リダイレクトされるので別のHTTPリクエストになってしまい、SELECT
がReaderに対しておこなわれ更新前のデータを表示してしまうということが起こりました。
本記事では、この問題に対する解決策を提示したいと思います。
実装方針
SELECT
をReaderに向けるかWriterに向けるかは、以下の getReadPdo()
メソッドで決められています。
<?php
namespace Illuminate\Database;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Connection implements ConnectionInterface
{
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* Get the current PDO connection used for reading.
*
* @return \PDO
*/
public function getReadPdo()
{
if ($this->transactions > 0) {
return $this->getPdo();
}
if ($this->readOnWriteConnection ||
($this->recordsModified && $this->getConfig('sticky'))) {
return $this->getPdo();
}
if ($this->readPdo instanceof Closure) {
return $this->readPdo = call_user_func($this->readPdo);
}
return $this->readPdo ?: $this->getPdo();
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
ここで注目すべきは、2つ目のif文です。
以下のどちらかの条件が成立するときに、SELECT
がWriterに対しておこなわれるということです。
$this->readOnWriteConnection
(Laravel自体は制御しない真偽値)が True$this->recordsModified
(DB更新があったときにLaravelが True にする)かつsticky
オプションが有効
1つ目の条件は、Laravelが制御していないプロパティの真偽値です。同クラスにこのプロパティを制御するための useWriteConnectionWhenReading($value = true)
というpublicメソッドが定義されてありますが、Laravelのどこからも呼ばれていません。つまり、こちらは実装者が外部から SELECT
の向け先を指定したいときに使用するものです。
2つ目の条件は、まさしく先ほど紹介した sticky
オプションの機能です。こちらを使って、リダイレクト後にも sticky
効果を適用するか決定します。ちなみに、$this->recordsModified
をpublicから取得するには同クラスの hasModifiedRecords()
メソッドを使用します。
以上のことを踏まえて、2つのミドルウェアを作成します。
以下、リクエストの流れとミドルウェアの実装方針です。
- HTTPリクエスト①
- DB更新して一覧画面へリダイレクト
- ミドルウェア①
- レスポンスの型がリダイレクトであるか
sticky
オプションが有効であるかhasModifiedRecords()
が True であるか(DB更新がおこなわれたか)- 上記のすべてを満たしていたら、
shouldConnectWriter
をセッションに一時保存
- HTTPリクエスト②(リダイレクトのリクエスト)
- ミドルウェア②
shouldConnectWriter
がセッションにあるか確認- あれば、
useWriteConnectionWhenReading()
を呼ぶ
- 一覧表示用の
SELECT
処理(Writerに向く)
実装内容
参考:https://readouble.com/laravel/8.x/ja/middleware.html
ミドルウェア①
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Database\Connection;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class AfterDataBaseStickiness
{
private Connection $db;
public function __construct(Connection $db)
{
$this->db = $db;
}
// コントローラの処理後に実行
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if (! ($response instanceof RedirectResponse))
{ // レスポンスがリダイレクトでない場合
return $response;
}
if (! $this->db->getConfig('sticky'))
{ // `sticky`オプションが有効でない場合
return $response;
}
if (! $this->db->hasModifiedRecords())
{ // DB更新がなかった場合
return $response;
}
// 次のHTTPリクエスト上で`shouldConnectWriter`を取得できるようにする
$request->session()->flash('shouldConnectWriter');
return $response;
}
}
ミドルウェア②
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Database\Connection;
use Illuminate\Http\Request;
class BeforeDataBaseStickiness
{
private Connection $db;
public function __construct(Connection $db)
{
$this->db = $db;
}
// コントローラの処理前に実行
public function handle(Request $request, Closure $next)
{
if ($request->session()->exists('shouldConnectWriter'))
{ // 今回のHTTPリクエスト上ではすべて、Writerに接続するようにする
$this->db->useWriteConnectionWhenReading();
}
return $next($request);
}
}
上記ミドルウェアの登録
あとは対象のルートに対して、ミドルウェアを指定してあげれば終わりです。
対象のルートが多い場合は、app/Http/Kernel.php
の web
ミドルウェアグループの末尾に追加して routes/web.php
の全ルートに適用すると良いと思います。重い処理はないので、問題ないです。
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
protected $middlewareGroups = [
'web' => [
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
\App\Http\Middleware\BeforeDataBaseStickiness::class,
\App\Http\Middleware\AfterDataBaseStickiness::class,
],
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
さいごに
こんな感じで実装自体はかなりシンプルだと思います。
LaravelのCoreクラスが読みやすいのと、ちゃんとpublicメソッドが提供されていることに、とても助けられました。
これで臆せずDBの負荷分散ができますね!
以上、わいでした。
健闘を祈る!!