Estrategia de Fallback de Base de Datos Solo Lectura en Laravel

- Andrés Cruz - EN In english

Video thumbnail

En el diseño de arquitectura de software, el término fallback hace referencia a un mecanismo de respaldo o ruta alternativa que se activa automáticamente cuando el sistema principal experimenta un fallo, garantizando así la continuidad del servicio.

En esta sección explicaremos cómo implementar un sistema de fallback enfocado exclusivamente en operaciones de lectura, manteniendo un réplica o espejo de los datos principales. Esta estrategia resulta sumamente útil en aplicaciones web donde el tráfico es masivamente de consulta (como blogs o plataformas educativas), donde el grueso de la interacción consiste en leer publicaciones, listados o detalles de cursos.

Contexto y Limitaciones de Infraestructura

En entornos de alojamiento compartido o servidores con recursos limitados, las bases de datos de producción (como MySQL o PostgreSQL) suelen imponer un límite estricto de conexiones simultáneas (por ejemplo, un máximo de 20 conexiones concurrentes). Si se supera este umbral, el servidor rechaza las conexiones adicionales lanzando un error del sistema (como el código de error 2002: Operación no permitida).

Para mitigar este cuello de botella sin incurrir inmediatamente en costos de infraestructura más elevados, podemos implementar un mecanismo de fallback utilizando SQLite. Dado que SQLite gestiona los datos directamente en un archivo local sin pasar por un socket de red o servicio de base de datos remoto, se eliminan los límites de concurrencia por conexión del servidor principal.

Configuración de la Base de Datos SQLite

El primer paso consiste en configurar la conexión secundaria en el archivo config/database.php de Laravel, deshabilitando la revisión de claves foráneas para facilitar la sincronización masiva de datos:

config/database.php

'sqlite_fallback' => [
    'driver' => 'sqlite',
    'database' => database_path('fallback.sqlite'),
    'prefix' => '',
    'foreign_key_constraints' => false, // Evita conflictos de llaves foráneas en el fallback
],

Para inicializar el archivo y crear el esquema de tablas, se ejecutan las migraciones especificando la conexión alternativa:

$ php artisan migrate:fresh --database=sqlite_fallback

Sincronización de Datos para la Réplica

Puesto que el objetivo es respaldar únicamente las tablas críticas de consulta (como categorías, publicaciones o lecciones), se implementa un proceso de sincronización que limpia la base de datos local e inserta los datos actualizados desde la base de datos principal en bloques (chunks) para optimizar la memoria:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
***
public array $availableTables = [
    'categories' => 'Categorías',
    'posts' => 'Publicaciones (Posts)',
    ***
];

$syncedCount = 0;
$totalRecords = 0;
foreach ($this->selectedTables as $table) {
    if (Schema::connection('mysql')->hasTable($table)) {
        // Extraer desde MariaDB/MySQL
        $data = DB::connection('mysql')->table($table)->get()->map(fn($item) => (array) $item)->toArray();

        // Limpiar e insertar en SQLite
        DB::connection('sqlite_fallback')->table($table)->truncate();

        if (!empty($data)) {
            // Insertar en bloques de 500 registros para optimizar memoria en SQLite
            foreach (array_chunk($data, 500) as $chunk) {
                DB::connection('sqlite_fallback')->table($table)->insert($chunk);
            }
            $totalRecords += count($data);
        }

        $syncedCount++;
    }
}

Detección Automática de Fallos mediante un Trait en Eloquent

Para lograr que este cambio de conexión sea completamente transparente para controladores y componentes de la interfaz (como Livewire o Inertia), aprovechamos la naturaleza lazy (perezosa) de las conexiones en Eloquent sobrescribiendo el método getConnection() a través de un Trait reestructurado:

app/Traits/HasSqliteFallback.php

<?php

namespace App\Traits;

use Illuminate\Support\Facades\DB;
use PDOException;
use PDO;

trait HasSqliteFallback
{
    /**
     * Sobrescribe el método de obtención de conexión de Eloquent para este modelo.
     */
    public function getConnection()
    {
        try {
            // probar a fallar, fuerza una excepción
            throw new PDOException("SQLSTATE[HY000] [1040] Too many connections", 1040);

            $connection = parent::getConnection();

            // Forzamos un ping o prueba ligera del socket PDO
            $connection->getPdo();

            return $connection;

        } catch (PDOException $e) {
            // Si el socket falló, dio "Too many connections" (1040) o error de conexión
            if (in_array($e->getCode(), [1040, 2002]) || str_contains($e->getMessage(), '2002') || str_contains($e->getMessage(), 'Too many connections')) {
                \Log::warning("MySQL fuera de servicio. Conmutando modelo [" . static::class . "] a SQLite Fallback.");
                // 1. Registramos los polyfills en la conexión de SQLite antes de devolverla
                //$this->registerMysqlSqlitePolyfills('sqlite_fallback');

                // 2. Devolvemos la conexión de fallback
                return DB::connection('sqlite_fallback');
            }

            throw $e;
        }
    }
}

Es importante del uso de:

$connection->getPdo();

Ya que, hacemos una conexión ANTES de la consulta para ver si podemos conectarnos y si no es posible conectar da el error justamente en el trait, con el cual podemos MANEJAR la consulta y mandarla a la base de datos de tipo lectura.

PDO es la interfaz implementada en PHP para la conexión y gestión de base de datos.

Uso en los Modelos Eloquent

Una vez definido el Trait, simplemente se incluye en aquellos modelos que representen tablas de solo lectura o de alta consulta:

namespace App\Models;

use App\Traits\HasSqliteFallback;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasSqliteFallback;

    // Lógica del modelo...
}

Con esta implementación, ante cualquier saturación en la base de datos MySQL, Eloquent interceptará el fallo en el método getConnection() y conmutará dinámicamente hacia la réplica local SQLite sin interrumpir la experiencia del usuario ni requerir modificaciones en la capa de controladores o vistas.

Consideraciones Técnicas

  • Alineación de funciones SQL: Es importante asegurarse de utilizar consultas compatibles con ambos motores, evitando métodos o funciones propias de MySQL que no estén presentes en SQLite.
  • Uso restringido a lectura: Esta arquitectura no está diseñada para sincronización bidireccional inmediata. Si se requiere persistir escrituras durante una falla, se debe incorporar una tabla de transacciones diferidas o un sistema de colas.

Aprende a implementar una estrategia de fallback de base de datos en Laravel usando SQLite. Evita errores de saturación y garantiza alta disponibilidad en solo lectura.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.