Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
Новини 27 жовтня 2026

Laravel 13.36-13.38: репліки бази даних без застарілих даних

Три випуски Laravel за тиждень, з 13.36 13 жовтня до 13.38 20 жовтня, мають спільну тему: роботу з репліками бази даних. Хто розносить читання й запис по різних серверах, давно бореться із застарілими даними після запису й тримав для цього власні макроси. Тепер ці інструменти є у фреймворку. Головні з них написав Джек Бейліс (Jack Bayliss), який, за його словами, саме впроваджує розділення читання й запису у себе.

Звідки береться проблема

Розділене з'єднання в config/database.php виглядає так:

'mysql' => [
    'driver' => 'mysql',
    'read' => [
        'host' => ['10.0.0.11', '10.0.0.12'],
    ],
    'write' => [
        'host' => ['10.0.0.10'],
    ],
    'sticky' => true,
    // ...
],

Опція sticky гарантує, що після запису наступні читання йдуть на write-сервер. Але лише до кінця поточного запиту. Форма відправляється PUT-запитом і робить редирект на GET - це вже два запити, і другий знову читає з репліки, яка може ще не мати змін. Автор PR пише, що в них затримка реплікації зазвичай до секунди, але буває і 15. Результат - користувач оновлює сторінку, бачить старий заголовок і йде писати баг-репорт.

PreventStaleDatabaseReads (13.38)

Новий middleware (#62078) розв'язує саме цей сценарій. Підключіть його до групи web у bootstrap/app.php:

use Illuminate\Foundation\Http\Middleware\PreventStaleDatabaseReads;

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        PreventStaleDatabaseReads::class,
    ]);
})

Або лише до потрібних маршрутів, з власним вікном у секундах:

Route::middleware(PreventStaleDatabaseReads::for(15))->group(function () {
    Route::get('/posts/{post}', [PostController::class, 'show']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});

Механіка проста. Якщо запит щось записав, middleware зберігає в сесії мітку часу _database_modified_at. На наступних запитах, поки мітка в межах вікна (2 секунди за замовчуванням), він вмикає useWriteConnectionWhenReading(), і всі читання цього користувача йдуть на write-сервер. Вікно варто ставити не менше за найдовшу затримку, яку показують ваші метрики реплікації: коротше вікно пропустить старі дані.

Обмеження варто знати заздалегідь:

  • потрібна сесія, тож для stateless API middleware не підходить;
  • відстежується лише з'єднання за замовчуванням;
  • захищено лише того, хто писав: інші користувачі й далі можуть читати старі дані з репліки.

DB::usingWritePdo() (13.38)

Для коду, якому свіжі дані потрібні незалежно від користувача, є usingWritePdo() (#62099). Він закриває пастку, на яку легко наступити з onWriteConnection(): той діє лише на запит, який сам починає, а зв'язок, завантажений з результату, - це вже новий запит, і він іде на репліку.

// posts() усе одно читає з репліки
$posts = User::onWriteConnection()
    ->where('email', $email)
    ->first()
    ?->posts()
    ->whereNull('published_at')
    ->get();

// тут усі читання в колбеку йдуть на write-сервер
$posts = DB::usingWritePdo(fn () => User::query()
    ->where('email', $email)
    ->first()
    ?->posts()
    ->whereNull('published_at')
    ->get());

Метод повертає результат колбеку і відновлює попередній стан з'єднання, навіть якщо колбек кинув виняток. Усередині DB::transaction() він не потрібен: запити в транзакції й так ідуть на write-сервер.

Як це протестувати

Кожен запис у query log має ключ readWriteType зі значенням read або write. За ним можна перевірити, куди пішов запит після оновлення, - приклад тесту є в Laravel News:

DB::forgetRecordModificationState();
DB::enableQueryLog();

$this->actingAs($user)->get("/posts/{$post->id}")->assertOk();

$this->assertSame('write', DB::getQueryLog()[0]['readWriteType']);

Головна пастка: RefreshDatabase для такого тесту не годиться. Він загортає кожен тест у транзакцію, а в транзакції всі запити й так ідуть на write-сервер, тож тест пройде навіть без middleware. Використовуйте DatabaseMigrations або DatabaseTruncation.

Sticky-з'єднання у воркерах черг (13.36)

Воркер черги - це один довгоживучий процес, який тримає те саме з'єднання між джобами. Якщо sticky увімкнено, одна джоба, що щось записала, переводить на write-сервер читання всіх наступних джоб цього воркера - аж до його перезапуску. Тобто репліки для черг фактично простоюють.

Laravel 13.36 додав прапорець (#61915), з яким воркер скидає цей стан перед кожною джобою:

// AppServiceProvider::boot()
use Illuminate\Queue\Worker;

Worker::$flushStickyConnections = true;

За замовчуванням прапорець вимкнено, бо якась джоба може покладатися на те, що прочитає дані, записані попередньою джобою в тому самому воркері. Правильно таке оформлювати ланцюжком чи батчем, але перед увімкненням перевірте такі місця.

Нюанс із драйвером черги database: щоб зарезервувати джобу, він оновлює таблицю jobs. Якщо вона на тому самому з'єднанні, що й дані застосунку, цей UPDATE знову вмикає sticky-стан одразу після скидання, і прапорець нічого не дає. Як пояснює Laravel News, рішення - винести чергу на окреме з'єднання через DB_QUEUE_CONNECTION=queue. Драйверів Redis і SQS це не стосується.

Інше корисне

  • DB::olap() для PlanetScale і Vitess (#61932, 13.36). Перемикає з'єднання в OLAP-режим на час колбеку, щоб обійти обмеження OLTP-режиму (на PlanetScale - не більше 100 000 рядків на запит), і ставить режим і на read-, і на write-з'єднання. На звичайному MySQL просто виконує колбек. Поруч з'явився isVitess().
  • Rule::-білдери для всіх правил валідації (#61833, 13.36): Rule::requiredWithout('email'), Rule::url()->protocols(['https']), Rule::uuid()->version(4), Rule::distinct()->ignoreCase(). Автодоповнення в IDE замість рядків, а значення з комою більше не розбивається на два.
  • #[CountCrashesAsExceptions] тепер працює на слухачах у черзі, mailables і сповіщеннях, а не лише на джобах (13.38). Раніше слухач, що падав від нестачі пам'яті, з $tries = 0 повторювався без кінця.
  • #[DeleteWhenMissingModels] і #[FailOnTimeout] більше не ігноруються на mailables у черзі (13.36): лист для видаленого замовлення не потрапить у failed_jobs, а лист, що впав за таймаутом, не прийде двічі.
  • lazy() і lazyById() звільняють попередній чанк перед завантаженням наступного - пікове споживання пам'яті приблизно вдвічі менше (13.38).
  • Екранування в Markdown-листах (13.36): значення користувача в тексті Markdown-посилання могло через зворотну скісну риску змінити адресу посилання. Якщо ви підставляєте введення користувачів у Markdown-листи, це привід оновитися.

13.37 вийшов здебільшого випуском виправлень, але кілька речей помітні: підтримка JPEG XL в Image API, опція strict у Rule::numeric(), виправлення N+1 при вкладених include у JSON:API-ресурсах і звільнення unique-локу, коли постановка джоби в чергу не вдалася.

Чи варто оновлюватися

Так, якщо у вас розділені read/write-з'єднання: оновіться до 13.38 і почніть з PreventStaleDatabaseReads на маршрутах, де користувачі редагують дані, підібравши вікно за метриками реплікації. Worker::$flushStickyConnections вмикайте свідомо, після перевірки джоб, що читають дані одна одної, а з драйвером database - лише разом з окремим DB_QUEUE_CONNECTION. Усі нові можливості опційні, тож саме оновлення нічого не змінить, доки ви їх не ввімкнете.

7

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

Новини 27 жовтня 2026

Filament v5.11.0: поля й віджети на React, Vue і Svelte та жорсткіший захист Livewire

Filament v5.11.0 дозволяє писати власні поля форм, компоненти схем і віджети на React, Vue, Svelte або чистому JavaScript. Паралельно v5.11.0 і v4.16.0 закривають для клієнта внутрішні Livewire-методи та стан і додають опційні ліміти для репітерів і пагінації таблиць.

2
Новини 27 жовтня 2026

Livewire 4.4.8 закриває обхід #[Authorize], а NativePHP Mobile - сім вразливостей

Livewire 4.4.8 виправляє GHSA-mhf7-xp6r-6qrc (CVSS 6.5): атрибут #[Authorize] можна було обійти й виконати дію над чужим записом. Того ж тижня NativePHP опублікувала сім попереджень для NativePHP for Mobile - виправлення в nativephp/mobile 4.6.1 і 3.3.11, але до користувачів вони дійдуть лише з новою збіркою застосунку.

1

Вакансії за темою

Middle Backend Engineer PHP/Laravel

Розробка бекенду для нової платформи запису й редагування відео з AI: REST API для керування контентом (S3-завантаження, метадані, мініатюри, планування), оркестрація задач обробки відео на AI-воркерах RunPod, інтеграції з платежами, аналітикою та AI-сервісами, WebSocket-синхронізація з клієнтами та фонові черги. Потрібен досвід із бекенд-фреймворками, проєктуванням баз даних, автентифікацією й безпекою API, роботою з вебхуками та Docker, Git і CI/CD.

Senior/Middle PHP Developer

Підтримка та розвиток великих веб-сайтів компанії (понад 1 млн відвідувань на місяць) на Laravel та OctoberCMS, інтеграція з внутрішніми системами та платіжними шлюзами (Приват24, Portmone, EasyPay, LiqPay), оптимізація продуктивності та робота з великим legacy-кодом без ризику для продакшену. Потрібен досвід PHP від 4 років, впевнена робота з Laravel і OctoberCMS, знання HTML5, CSS3 та JavaScript, вища освіта.

SoftHouseGroup Нова
2 дні тому

PHP Software Engineer

Розробник буде частиною full-stack команди, яка розвиває велику складну систему у страховому домені з багатьма модулями та інтеграціями з платіжними провайдерами. Щодня потрібні розробка нових модулів, підтримка існуючих, виправлення багів та перевірка коду, згенерованого AI. Ключові вимоги: понад 4 роки досвіду PHP, Laravel або Yii2, знання ООП і патернів проєктування, MySQL, REST API, фронтенд (HTML, CSS, JavaScript, jQuery, Vue.js), Git і англійська на рівні Pre-Intermediate для читання.

Пакети за темою

Kalimera

welldigit/kalimera

Interactive installer that scaffolds Laravel applications the Well Digit way.

v1.0.0 15

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,269 v2.5.0 13 26