Vai al contenuto
DedicatedPHP Contatto

Migrazioni dello schema su tabelle di grandi dimensioni senza bloccare le scritture

Pianifica modifiche allo schema in produzione con PHP: distribuisci per fasi, elabora i dati in batch e verifica la concorrenza prima di rimuovere la struttura precedente.

Diagramma di una migrazione graduale dello schema con backfill in batch, controllo delle scritture concorrenti e convalida prima di rimuovere la struttura precedente

Modificare una colonna può essere semplice in un database di piccole dimensioni. In una tabella grande e attiva, la stessa operazione può richiedere un lock, ricostruire indici o competere per le risorse con le query di produzione. L’effetto dipende dal motore, dalla sua versione, dal tipo di modifica e dalla configurazione: non è prudente supporre che un’istruzione sia istantanea o non blocchi le operazioni.

Le migrazioni dello schema su tabelle di grandi dimensioni separano la modifica strutturale dalla trasformazione dei dati. L’obiettivo è mantenere compatibili le versioni dell’applicazione durante la transizione, misurare l’impatto e poter interrompere il processo. Non esiste una ricetta universale: verifica le funzionalità del motore e il comportamento effettivo dell’applicazione.

Perché una piccola modifica può bloccare le operazioni

Perché una piccola modifica può bloccare le operazioni — guía visual de DedicatedPHP

Ampliare una colonna, aggiungere un vincolo o modificare un tipo può comportare un lavoro proporzionale alle dimensioni della tabella. A seconda del motore, l’operazione potrebbe mantenere dei lock, generare carico su disco, influire sulle repliche o attendere la conclusione delle transazioni aperte. Anche un’operazione considerata online può bloccare brevemente o avere limitazioni.

Valuta le dimensioni e la crescita della tabella, il carico di lettura e scrittura, le transazioni lunghe, gli indici e lo spazio disponibile. Consulta la documentazione della versione specifica e prova in un ambiente rappresentativo. Definisci soglie operative per latenza, lock, spazio di archiviazione e ritardo di replica.

Un aggiornamento convenzionale dello schema modifica le strutture, per esempio aggiungendo una colonna. Una migrazione dei dati trasforma o copia i valori esistenti, spesso riga per riga. Le due attività possono rientrare nella stessa modifica funzionale, ma comportano rischi diversi ed è opportuno eseguirle e monitorarle separatamente.

Inventariare lettori, writer e dipendenze

Individua chi legge e scrive il dato: codice PHP, query SQL, job di coda, comandi pianificati, importazioni, report e servizi esterni. Verifica quali versioni possono coesistere durante un deployment graduale. Anche le query dinamiche e i consumer esterni al repository devono essere considerati.

  • Documenta i formati attuale e di destinazione, inclusi valori nulli, valori predefiniti e regole di conversione.
  • Individua indici, chiavi esterne, vincoli e viste dipendenti.
  • Verifica quali componenti aggiornano parzialmente i campi e quali scrivono il dato.
  • Stabilisci come rilevare e correggere i valori non validi.

Questo inventario determina l’ordine del deployment. Una versione precedente può fallire se viene eliminata una colonna che usa ancora. Se non puoi individuare tutti i consumer, presupponi che il vecchio codice possa restare attivo più a lungo.

Suddividere la modifica in fasi compatibili

Un pattern comune è espandere e poi contrarre: aggiungere la nuova struttura senza rimuovere quella precedente, distribuire codice compatibile, copiare i dati storici e cambiare le letture. Solo dopo aver verificato il risultato si elimina la struttura precedente.

  1. Espansione: aggiungi la nuova colonna o tabella senza interrompere le versioni in produzione. Valuta di creare indici e vincoli in un’operazione separata.
  2. Deployment della compatibilità: rilascia lettori che tollerino dati ancora da elaborare e writer che mantengano coerenti entrambe le rappresentazioni.
  3. Completamento dei dati storici: esegui il backfill in batch e monitora l’avanzamento.
  4. Cambio di utilizzo: leggi principalmente dalla nuova struttura e monitora errori, latenza e discrepanze.
  5. Contrazione: interrompi prima le scritture sulla struttura precedente e, in un deployment successivo, rimuovi la vecchia struttura.

Distribuire il codice non obbliga a esporre subito la funzionalità. Verifica che ogni versione funzioni con lo schema di ciascuna fase, anche nel caso in cui sia necessario ripristinare il codice.

Eseguire un backfill controllato da PHP

Evita di caricare in memoria l’intera tabella o di mantenere una transazione globale. Un comando PHP da console consente di controllare le dimensioni dei batch, registrare l’avanzamento e interrompere il processo senza vincolarlo al ciclo di una richiesta web. Questo pattern con PDO usa una versione ottimistica. progressStore rappresenta un record di avanzamento salvato nello stesso database e confermato nella stessa transazione delle righe del batch.

$limit = 200;
$maxAttempts = 5;
$cursor = (int) $progressStore->load('backfill');

// Limite superiore iniziale; da solo non copre gli inserimenti tardivi.
$upperId = (int) $pdo->query('SELECT MAX(id) FROM records')->fetchColumn();

while ($cursor < $upperId) {
    $done = false;
    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        $select = $pdo->prepare(
            'SELECT id, old_value, version FROM records
             WHERE id > :cursor AND id <= :upper_id
             ORDER BY id LIMIT ' . (int) $limit
        );
        $select->execute([':cursor' => $cursor, ':upper_id' => $upperId]);
        $rows = $select->fetchAll(PDO::FETCH_ASSOC);
        if (!$rows) {
            $cursor = $upperId;
            $done = true;
            break;
        }

        try {
            $pdo->beginTransaction();
            $update = $pdo->prepare(
                'UPDATE records SET new_value = :value, version = version + 1
                 WHERE id = :id AND version = :version'
            );
            foreach ($rows as $row) {
                $update->execute([
                    ':value' => transform($row['old_value']),
                    ':id' => $row['id'], ':version' => $row['version'],
                ]);
                if ($update->rowCount() !== 1) {
                    throw new VersionConflict('La riga è cambiata durante il backfill');
                }
            }
            $next = (int) end($rows)['id'];
            $progressStore->saveWithinTransaction('backfill', $next);
            $pdo->commit();
            $cursor = $next;
            $done = true;
            break;
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $retryable = $e instanceof VersionConflict
                || ($e instanceof PDOException && isRetryableDatabaseError($e));
            if (!$retryable || $attempt === $maxAttempts) throw $e;
            usleep(min(100000 * (2 ** ($attempt - 1)), 2000000));
            // Rileggere il batch con lo stesso cursore; l’avanzamento non è ancora avvenuto.
        }
    }
    if (!$done) throw new RuntimeException('Batch in sospeso; cursore invariato.');
}

VersionConflict deve essere un’eccezione personalizzata, non un’eccezione generica che raggruppa gli errori di trasformazione. In questo modo, gli errori permanenti di transform() interrompono il comando e possono essere diagnosticati. isRetryableDatabaseError() deve riconoscere solo gli errori transitori classificati per il motore e il driver utilizzati, come deadlock o timeout. Gli altri errori del database interrompono il processo. Il numero massimo di tentativi e il backoff limitato evitano retry indefiniti; se i tentativi si esauriscono, il comando fallisce senza far avanzare il cursore. Verifica come la combinazione di PDO e motore in uso restituisce rowCount().

La protezione ottimistica richiede che tutti i writer incrementino version nella stessa transazione in cui aggiornano i dati. Questo requisito riguarda il codice PHP, le code, le importazioni e i servizi esterni e deve essere implementato e verificato prima di avviare il backfill. Se un writer non lo rispetta, il confronto potrebbe non rilevare la race condition: aggiornalo, instradalo attraverso un meccanismo comune, disattivalo temporaneamente o usa lock adeguati. Non considerare valida la protezione prima di aver verificato il requisito per ogni writer.

Il limite superiore iniziale riduce il lavoro, ma non garantisce l’inclusione degli inserimenti confermati in ritardo. Al termine del passaggio, esegui una riconciliazione che cerchi esplicitamente le righe non trasformate o con discrepanze, senza basarsi su un ID superiore al cursore. Correggi e verifica di nuovo questi record; ripeti il passaggio finché non rimangono elementi in sospeso, secondo un criterio verificabile e con writer che mantengano la compatibilità. Se non puoi rilevare o riconciliare queste righe in modo affidabile, valuta l’acquisizione delle modifiche o una finestra di manutenzione. Non dichiarare completati i dati storici solo perché il cursore ha raggiunto il limite iniziale.

La trasformazione deve essere idempotente e l’avanzamento deve essere confermato atomicamente insieme alle righe. Se lo storage del cursore non condivide la transazione con i dati, progetta la ripresa in modo che le righe già confermate possano essere elaborate di nuovo senza rischi. Aggiungi opzioni di pausa, limiti per i batch, log strutturati e un codice di uscita che segnali gli errori; calibra le dimensioni sulla base delle misurazioni, non di supposizioni.

Evitare race condition con scritture concorrenti

La doppia scrittura, da sola, non evita tutte le race condition. Il backfill può leggere un valore precedente; una scrittura concorrente può aggiornare entrambi i campi; in seguito, il backfill potrebbe sovrascrivere il nuovo campo con un risultato obsoleto. La condizione sulla versione nell’esempio impedisce l’aggiornamento se la scrittura concorrente ha incrementato la versione; il conflitto viene mantenuto perché il cursore avanza solo dopo la conferma dell’intero batch.

Si può anche usare un aggiornamento condizionale basato sul valore originale, oppure lock adeguati, a seconda delle garanzie offerte dal motore e dal carico di lavoro. Ogni alternativa ha costi e semantiche differenti. Prova la strategia sul motore specifico con scritture intercalate, deadlock e timeout, e verifica sia le righe interessate sia la riconciliazione prima di affermare che il processo completa correttamente i dati storici.

Verificare prima di rimuovere la struttura precedente

Il fatto che il comando termini non dimostra che i dati siano corretti. Verifica che non rimangano righe in sospeso, convalida i vincoli e confronta i risultati con la trasformazione prevista. Controlla i valori nulli e i casi limite, e conferma che le letture usino il nuovo campo senza peggiorare il comportamento funzionale o le prestazioni.

Monitora anche i processi poco frequenti, come report o attività periodiche. Mantieni la struttura precedente finché esistono lettori o writer che dipendono da essa: la sua presenza è una misura di compatibilità, non la prova che la migrazione sia terminata.

Definire pause, ripristino e finestra di manutenzione

Definire pause, ripristino e finestra di manutenzione — guía visual de DedicatedPHP

Definisci i segnali che richiedono una pausa: errori o latenza oltre la soglia concordata, lock, ritardo eccessivo delle repliche, pressione sulle risorse o discrepanze in aumento. Stabilisci chi interrompe il processo e come riprenderlo dall’ultimo batch confermato. Monitora la durata dei batch, gli errori, CPU, disco e lock.

Ripristinare il codice non equivale a ripristinare i dati. Dopo aver accettato scritture solo nel nuovo formato, una trasformazione inversa può comportare una perdita di informazioni. Il recupero potrebbe consistere nel tornare a una versione compatibile e mantenere entrambe le strutture, non nell’annullare le modifiche ai dati. Una finestra di manutenzione può essere preferibile se non è possibile mantenere la coerenza tra le versioni, se i lock necessari non sono accettabili o se non puoi verificare il risultato con l’applicazione attiva.

  • Il motore e la sua versione consentono la modifica con lock accettabili?
  • I lettori e i writer precedenti possono coesistere con lo schema intermedio?
  • È possibile mettere in pausa e riprendere il comando PHP senza duplicare gli effetti né saltare righe?
  • Esiste una strategia testata per writer concorrenti, errori transitori e discrepanze?
  • Integrità e impatto saranno misurati prima di rimuovere la struttura precedente?
Vuoi applicare queste idee al tuo progetto?Parliamo della tua piattaforma PHP.
Visualizza il servizio correlato