Ce qui se passe
Laravel 13.38 avait apporté le middleware PreventStaleDatabaseReads : après une écriture, les lectures de
l'utilisateur vont au primaire pendant deux secondes, une estimation fixe du retard de réplication. Trop
courte, il relit du périmé ; trop longue, le primaire travaille pour rien. Laravel 13.40 ajoute trois méthodes
de connexion, contribuées par Tobias Petry (PR #62198), qui remplacent l'estimation par une question posée au
réplica.
Les trois méthodes
DB::getLastCommitId()interroge toujours la connexion d'écriture et renvoie sa position de réplication :- MySQL : ensemble GTID de
@@global.gtid_executed, par exemple3e11fa47-71ca-11e1-9e33-c80aa9429562:1-77; - MariaDB : position GTID de
@@gtid_binlog_pos, par exemple0-1-1052; - PostgreSQL : position WAL de
pg_current_wal_lsn(), par exemple0/16B3748.
- MySQL : ensemble GTID de
DB::hasCommitId($id, wait: 0)répondtruesi le serveur a tout appliqué jusqu'à cette position.waitest un entier en secondes : à 0, une seule vérification ; au-delà, la base bloque jusqu'au rattrapage, et la méthode répondfalsesi le délai expire.DB::mergeCommitIds([...])combine plusieurs identifiants : la plus haute position WAL sur PostgreSQL, l'union des ensembles GTID sur MySQL et MariaDB. Un tableau vide lève uneRuntimeException.
Les prérequis
| Base | Réglages serveur | wait |
|---|---|---|
| MySQL | log_bin, gtid_mode = ON, enforce_gtid_consistency = ON |
oui |
| MariaDB | log_bin |
oui |
| PostgreSQL | aucun | à partir de PostgreSQL 19 |
| SQLite, SQL Server | non pris en charge | non |
MySQL 8 active le journal binaire par défaut, pas les GTID ; MariaDB n'active pas le journal binaire. Un réglage
manquant lève une RuntimeException qui le nomme. Un service managé peut refuser ces modifications.
Lire ses propres écritures
Le schéma du billet : un middleware du groupe web, placé après la session, garde l'identifiant après chaque
requête qui écrit, et le vérifie à la suivante.
Une nouvelle écriture avant que le réplica ait rattrapé ? mergeCommitIds() fusionne les deux identifiants dans la
session. Le coût : une requête de plus sur le réplica tant qu'un identifiant est en attente, une sur le primaire par
requête qui écrit. Si la réplication s'arrête, l'utilisateur reste sur le primaire et voit des données à jour. Le
middleware vérifie sans wait, pour ne pas retenir la réponse.
Dans un job
Un job envoyé juste après une écriture peut démarrer avant que le réplica ait la ligne. Une esquisse :
$order = DB::transaction(fn () => Order::create($data));
// Après le commit : relevé dans la transaction, l'identifiant n'inclurait pas la commande.
SendOrderConfirmation::dispatch($order->id, DB::getLastCommitId());app/Jobs/SendOrderConfirmation.phpPHPpublic function handle(): void
{
$replicaReady = DB::hasCommitId($this->commitId, wait: 2);
$replicaReady
? $this->send()
: DB::usingWritePdo(fn () => $this->send());
}Deux pièges. Passez l'identifiant de la commande, pas le modèle : avec SerializesModels, Laravel recharge le
modèle avant handle(), sur un réplica, et un réplica en retard lève ModelNotFoundException. Et préférez
usingWritePdo() à useWriteConnectionWhenReading() : un worker réutilise sa connexion d'un job à l'autre, et le
premier rétablit le réglage à la fin du callback.
Où s'exécute la vérification
hasCommitId() est une lecture : elle va là où vont les lectures de la requête. Dans une transaction, avec sticky
après une écriture, ou quand les lectures sont forcées vers l'écriture, c'est le primaire — qui répond true, ce qui
reste juste puisque les lectures suivantes y vont aussi. Une connexion garde un seul PDO de lecture par requête ;
la suivante peut tomber sur un autre réplica, donc on vérifie à chaque requête, sans mettre le résultat en cache.
Source : Laravel News, en anglais. Voir aussi la fiche
du 22 octobre sur PreventStaleDatabaseReads.