21. Événements de changement : LISTEN, NOTIFY, WAIT FOR CHANGES
Éditions Entreprise, Cluster et Developer. En édition Express, ces instructions rendent l'erreur 9001.
Une application qui veut savoir ce qui a changé (écran de stock, tableau de bord, cache) n'a pas besoin d'interroger la base en boucle : elle s'abonne aux tables ou aux canaux qui l'intéressent, puis attend sur une connexion dédiée. MIRAJ lui rend les changements dès leur validation :
-- Connexion dédiée de l'application
LISTEN TABLE articles COLUMNS (qte, prix);
WAIT FOR CHANGES TIMEOUT 30;+------------------+------------------+--------+-------+----------+------+---------+---------+------------+-------------------------+
| seq | commit_seq | event | db | name | pk | columns | payload | session_id | at |
+------------------+------------------+--------+-------+----------+------+---------+---------+------------+-------------------------+
| 1792376601136069 | 1792376601136069 | UPDATE | stock | articles | [25] | prix | NULL | 169 | 2026-10-19 03:26:06.508 |
| 1792376601136070 | 1792376601136070 | INSERT | stock | articles | [27] | NULL | NULL | 169 | 2026-10-19 03:26:06.508 |
+------------------+------------------+--------+-------+----------+------+---------+---------+------------+-------------------------+Ici, une autre session a changé le prix de l'article 25, renommé ce même article (changement écarté : nom n'est pas dans COLUMNS) puis ajouté l'article 27.
Principes :
- Aucun déclencheur à écrire : MIRAJ relève lui-même la clé primaire et les colonnes modifiées de chaque ligne écrite dans une table écoutée.
- Publication à la validation : un événement n'est rendu qu'une fois la transaction validée, jamais après un
ROLLBACK. Les écritures d'une même transaction sont fusionnées : une ligne insérée puis modifiée donne un seulINSERT, et une ligne insérée puis supprimée ne donne rien. - Reprise après une coupure : chaque événement porte un numéro croissant. Un client qui se reconnecte reprend là où il s'était arrêté.
- Un écrivain n'attend jamais un abonné : un abonné lent ou absent ne ralentit pas les écritures. S'il prend trop de retard, il reçoit
RESYNCet doit tout relire. - Aucun pilote particulier :
WAIT FOR CHANGESest une instruction ordinaire qui rend un jeu de résultats. Tout client SQL l'utilise (Delphi, PHP, outils graphiques,miraj-cli). Une application qui embarque le moteur parmiraj.dlldispose en plus demiraj_session_wait_changes(WaitChanges/wait_changes/waitChangesdans les enveloppes, voir 27.9) : elle attend au plus un délai donné et rend au plus un nombre donné de lignes.
21.1 S'abonner#
LISTEN TABLE [base.]table [COLUMNS (c1, …)] [WHERE condition] [WITH ROW] [FROM n]
LISTEN [CHANNEL] canal [FROM n]
UNLISTEN TABLE [base.]table
UNLISTEN [CHANNEL] canal
UNLISTEN *LISTEN TABLEsuit les écritures d'une table : insertions, modifications, suppressions, vidages et changements de structure. Il demande le privilègeSELECTsur la table, vérifié au moment duLISTEN.COLUMNS (…): seuls lesUPDATEqui changent au moins une de ces colonnes sont rendus. Les insertions et suppressions sont toujours rendues.WHERE condition: seules les lignes qui satisfont la condition sont rendues. La condition porte sur les colonnes de la table (l'image d'après, ou l'image d'avant pour une suppression). Elle ne peut contenir ni sous-requête, ni agrégat, ni variable, sinon l'erreur 1235 est rendue.WITH ROW: la colonnepayloadcontient la ligne entière, en objet JSON.
LISTEN canalécoute un canal nommé, alimenté parNOTIFY(§21.4). Il ne demande aucun privilège.LISTENrend une ligne à une colonneseq: la position de la session dans la suite des événements (§21.3).
LISTEN TABLE articles WHERE qte < 20 WITH ROW;+------------------+
| seq |
+------------------+
| 1792376601136070 |
+------------------+Après UPDATE articles SET qte = 5 WHERE id = 26 et UPDATE articles SET qte = 500 WHERE id = 25 (écarté par le filtre), WAIT FOR CHANGES rend :
+------------------+------------------+--------+-------+----------+------+---------+----------------------------------------------------+------------+-------------------------+
| seq | commit_seq | event | db | name | pk | columns | payload | session_id | at |
+------------------+------------------+--------+-------+----------+------+---------+----------------------------------------------------+------------+-------------------------+
| 1792376601136071 | 1792376601136071 | UPDATE | stock | articles | [26] | qte | {"id": 26, "nom": "Écrou", "qte": 5, "prix": 0.80} | 172 | 2026-10-19 03:26:06.536 |
+------------------+------------------+--------+-------+----------+------+---------+----------------------------------------------------+------------+-------------------------+- Une session peut avoir jusqu'à 64 abonnements (
change_events_max_listeners) ; au-delà, l'erreur 9008 est rendue. Un nouveauLISTENsur la même cible remplace le précédent. UNLISTEN *retire tous les abonnements de la session. La déconnexion etKILLles retirent aussi.- Une table sans clé primaire peut être écoutée, avec l'avertissement 9009 (
Table 'stock.nk' has no primary key: only table-level events are delivered) : ses écritures ne donnent que des événementsBULK, sans clé, de motiftable without primary key. - Les tables temporaires ne sont jamais écoutées.
LISTEN, UNLISTEN et WAIT FOR CHANGES sont refusés dans une procédure, une fonction, un déclencheur ou un événement planifié : la routine se crée, mais son exécution rend l'erreur 1314 (LISTEN is not allowed in stored procedures).
21.2 Attendre les changements#
WAIT FOR CHANGES [TIMEOUT secondes] [LIMIT n]- Rend, dans l'ordre, les événements publiés depuis la position de la session qui correspondent à ses abonnements, au plus
LIMIT(1 000 par défaut), puis avance la position : un événement rendu ne l'est plus. - S'il n'y a rien à rendre, l'instruction attend le premier événement, au plus
TIMEOUTsecondes (par défaut@@change_events_wait_timeout, 30 s). Le délai écoulé, elle rend un résultat vide. TIMEOUT 0ne bloque pas : on relève ce qui est arrivé et on rend la main aussitôt.- L'attente ne tient aucun verrou : elle ne retarde ni les écritures ni les
ALTER TABLE. KILL QUERYl'interrompt (erreur 1317), tout commemax_statement_time(erreur 1969) ; cette limite valant 300 s par défaut, unTIMEOUTplus long demande de la relever pour la session (SET max_statement_time = 0).SHOW PROCESSLISTmontre la session à l'étatWaiting for changes.WAIT FOR CHANGESest refusé dans une transaction ouverte (erreur 9006) et sans abonnement (erreur 9007). Avecautocommit = 0, une transaction implicite qui n'a fait que lire est fermée d'office.- Un résultat vide garde les colonnes ci-dessous, sans ligne.
Colonnes rendues :
| Colonne | Contenu |
|---|---|
seq | Numéro de l'événement, croissant. |
commit_seq | Numéro commun à tous les événements d'une même validation. Il permet de regrouper les changements d'une transaction. |
event | INSERT, UPDATE, DELETE, BULK, TRUNCATE, SCHEMA, NOTIFY ou RESYNC (voir le tableau ci-dessous). |
db | Base de la table ; NULL pour NOTIFY (un canal n'appartient à aucune base). |
name | Nom de la table, ou du canal pour NOTIFY. |
pk | Clé primaire de la ligne, en tableau JSON ([25], ["FR", 2026]) ; les valeurs binaires sont écrites "0x…", les dates au format SQL. NULL pour un événement de table. |
columns | Pour un UPDATE : colonnes dont la valeur a changé, séparées par des virgules. |
payload | Charge d'un NOTIFY ; ligne en JSON avec WITH ROW ; motif d'un événement de table (1500 rows, ALTER TABLE, RENAME TABLE TO base.nouveau…). |
session_id | Session à l'origine de l'écriture, pour qu'une application ignore ses propres modifications. NULL sur un secondaire de cluster (§21.7). |
at | Date et heure de publication, à la milliseconde. |
Événements :
| Événement | Quand |
|---|---|
INSERT, UPDATE, DELETE | Une ligne ajoutée, modifiée ou supprimée. Une clé primaire modifiée donne DELETE de l'ancienne clé puis INSERT de la nouvelle ; un REPLACE qui remplace une ligne donne UPDATE. |
BULK | Plus de change_events_bulk_rows lignes (1 000 par défaut) d'une table modifiées par une même transaction, ou table sans clé primaire : un seul événement, sans clé, qui invite à relire la table. |
TRUNCATE | TRUNCATE TABLE (motif TRUNCATE). Un DELETE sans condition donne un DELETE par ligne, ou un BULK au-delà du seuil. |
SCHEMA | ALTER TABLE, RENAME TABLE, DROP TABLE ou DROP DATABASE de la table écoutée. L'abonnement reste : une table recréée sous le même nom est de nouveau suivie. |
NOTIFY | Notification d'un canal écouté (§21.4). |
RESYNC | Des événements ont été perdus pour cette session (§21.3) : relire les données. |
Exemple : écritures successives d'une session sur une table écoutée (LISTEN TABLE articles), dont une transaction qui insère la ligne 40 puis la modifie, insère la ligne 41 puis la supprime ; ensuite, hors transaction, la clé 40 devient 42, un REPLACE remplace la ligne 42, un DELETE sans condition vide la table et un ALTER TABLE lui ajoute une colonne :
+------------------+------------------+--------+-------+----------+------+---------+-------------+------------+-------------------------+
| seq | commit_seq | event | db | name | pk | columns | payload | session_id | at |
+------------------+------------------+--------+-------+----------+------+---------+-------------+------------+-------------------------+
| 1792376601136075 | 1792376601136075 | INSERT | stock | articles | [40] | NULL | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136076 | 1792376601136076 | DELETE | stock | articles | [40] | NULL | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136077 | 1792376601136076 | INSERT | stock | articles | [42] | NULL | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136078 | 1792376601136078 | UPDATE | stock | articles | [42] | nom,qte | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136079 | 1792376601136079 | DELETE | stock | articles | [25] | NULL | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136080 | 1792376601136079 | DELETE | stock | articles | [26] | NULL | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136081 | 1792376601136079 | DELETE | stock | articles | [27] | NULL | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136082 | 1792376601136079 | DELETE | stock | articles | [42] | NULL | NULL | 178 | 2026-10-19 03:26:06.580 |
| 1792376601136083 | 1792376601136083 | SCHEMA | stock | articles | NULL | NULL | ALTER TABLE | 178 | 2026-10-19 03:26:06.580 |
+------------------+------------------+--------+-------+----------+------+---------+-------------+------------+-------------------------+TRUNCATE TABLE b, l'insertion de 1 500 lignes en une instruction puis RENAME TABLE b TO b2 donnent :
+------------------+------------------+----------+-------+------+------+---------+--------------------------+------------+-------------------------+
| seq | commit_seq | event | db | name | pk | columns | payload | session_id | at |
+------------------+------------------+----------+-------+------+------+---------+--------------------------+------------+-------------------------+
| 1792376601136087 | 1792376601136087 | TRUNCATE | stock | b | NULL | NULL | TRUNCATE | 232 | 2026-10-19 03:26:46.994 |
| 1792376601136088 | 1792376601136088 | BULK | stock | b | NULL | NULL | 1500 rows | 232 | 2026-10-19 03:26:47.000 |
| 1792376601136089 | 1792376601136089 | SCHEMA | stock | b | NULL | NULL | RENAME TABLE TO stock.b2 | 232 | 2026-10-19 03:26:47.020 |
+------------------+------------------+----------+-------+------+------+---------+--------------------------+------------+-------------------------+21.3 Reprise après une coupure#
Chaque session a une position : le numéro du dernier événement qu'elle a reçu. Le premier LISTEN la place sur le dernier événement publié, et chaque WAIT FOR CHANGES l'avance.
Pour reprendre après une coupure (redémarrage de l'application, réseau coupé), le client garde le dernier seq traité et se réabonne avec FROM :
LISTEN TABLE articles FROM 1790294400000042;
WAIT FOR CHANGES; -- rend les événements qui suivent 1790294400000042Les événements sont gardés en mémoire dans une file de taille bornée (100 000 événements et 64 Mio par défaut, change_events_buffer_events et change_events_buffer_size). Une table reste écoutée pendant change_events_retention (10 minutes par défaut) après la déconnexion de son dernier abonné, pour que ses changements soient retrouvés à la reconnexion. En revanche, un UNLISTEN explicite arrête le suivi aussitôt.
La session reçoit une seule ligne RESYNC quand la reprise est impossible :
- les événements attendus ont quitté la file (abonné trop en retard, ou absent plus longtemps que la rétention) ;
- le serveur a redémarré depuis : les événements ne sont pas conservés sur disque ;
- le numéro vient d'un autre serveur, par exemple d'un autre nœud du cluster (§21.7).
Après RESYNC, relire les données. Pour un rechargement sans trou, relever d'abord la position :
SELECT MIRAJ_CHANGE_SEQ(); -- par exemple 1790294400000108
SELECT * FROM articles; -- rechargement complet
LISTEN TABLE articles FROM 1790294400000108;Les numéros sont propres à chaque démarrage du serveur : ils partent de l'heure du démarrage, en microsecondes. Un numéro d'un démarrage précédent ne peut donc pas être confondu avec un numéro du démarrage en cours.
21.4 Notifications applicatives : NOTIFY#
NOTIFY canal [, 'charge']
SELECT MIRAJ_NOTIFY('canal', 'charge'); -- 1NOTIFYenvoie une notification aux sessions qui écoutent le canal. La charge est un texte libre de 8 000 octets au plus (change_events_max_payload), au-delà l'erreur 9010 est rendue (NOTIFY payload exceeds 8000 bytes).MIRAJ_NOTIFY()rend 1.
LISTEN alertes;
NOTIFY alertes, 'stock bas';
SELECT MIRAJ_NOTIFY('alertes', 'x');
WAIT FOR CHANGES TIMEOUT 0;+------------------+------------------+--------+------+---------+------+---------+-----------+------------+-------------------------+
| seq | commit_seq | event | db | name | pk | columns | payload | session_id | at |
+------------------+------------------+--------+------+---------+------+---------+-----------+------------+-------------------------+
| 1792376601136073 | 1792376601136073 | NOTIFY | NULL | alertes | NULL | NULL | stock bas | 175 | 2026-10-19 03:26:06.559 |
| 1792376601136074 | 1792376601136074 | NOTIFY | NULL | alertes | NULL | NULL | x | 175 | 2026-10-19 03:26:06.559 |
+------------------+------------------+--------+------+---------+------+---------+-----------+------------+-------------------------+- Dans une transaction, la notification n'est publiée qu'au
COMMIT, avec les changements des tables et sous le mêmecommit_seq. UnROLLBACKl'annule. Deux notifications identiques (même canal, même charge) d'une transaction n'en font qu'une. NOTIFYetMIRAJ_NOTIFY()sont permis dans les procédures, les fonctions et les déclencheurs. Dans un déclencheur, on écrit par exempleSET @n = MIRAJ_NOTIFY('stock_bas', NEW.id);. Une instruction rejouée après un conflit d'écriture ne publie sa notification qu'une fois.
21.5 Suivi#
SHOW LISTENERS;
SHOW STATUS LIKE 'Miraj_change_events_%';
SELECT MIRAJ_CHANGE_SEQ();SHOW LISTENERSliste les abonnements :Id,User,Kind,Target,Columns,Filter,With_row,Cursor,Lag(événements publiés que la session n'a pas encore reçus),WaitingetTime. Avec le privilègePROCESS, il montre toutes les sessions, sinon celles de l'utilisateur.SHOW STATUS:Miraj_change_events_published,_buffered,_buffer_bytes,_evicted,_resyncs,_bulk,_listeners,_waits,_last_seq.
SHOW LISTENERS;+-----+------+-------+----------------+----------+--------+----------+------------------+-----+---------+------+
| Id | User | Kind | Target | Columns | Filter | With_row | Cursor | Lag | Waiting | Time |
+-----+------+-------+----------------+----------+--------+----------+------------------+-----+---------+------+
| 169 | root | TABLE | stock.articles | qte,prix | NULL | NO | 1792376601136070 | 0 | NO | 0 |
+-----+------+-------+----------------+----------+--------+----------+------------------+-----+---------+------+SHOW STATUS LIKE 'Miraj_change_events_%';+----------------------------------+------------------+
| Variable_name | Value |
+----------------------------------+------------------+
| Miraj_change_events_buffer_bytes | 3270 |
| Miraj_change_events_buffered | 17 |
| Miraj_change_events_bulk | 1 |
| Miraj_change_events_evicted | 0 |
| Miraj_change_events_last_seq | 1792376601136085 |
| Miraj_change_events_listeners | 0 |
| Miraj_change_events_published | 17 |
| Miraj_change_events_resyncs | 0 |
| Miraj_change_events_waits | 5 |
+----------------------------------+------------------+21.6 Réglages#
| Variable | Option du serveur | Défaut | Rôle |
|---|---|---|---|
change_events_buffer_events | --change-events-buffer-events <n> | 100 000 | Événements gardés en mémoire pour la reprise. |
change_events_buffer_size | --change-events-buffer-size <octets> | 64 Mio | Taille maximale de ces événements. |
change_events_bulk_rows | --change-events-bulk-rows <n> | 1 000 | Au-delà, par table et par transaction, un seul BULK. |
change_events_max_payload | --change-events-max-payload <octets> | 8 000 | Taille maximale d'une charge de NOTIFY. |
change_events_max_listeners | --change-events-max-listeners <n> | 64 | Abonnements par session. |
change_events_wait_timeout | --change-events-wait-timeout <secondes> | 30 | Attente de WAIT FOR CHANGES sans TIMEOUT (variable de session et globale). |
change_events_retention | --change-events-retention <secondes> | 600 | Suivi d'une table après la déconnexion de son dernier abonné ; 0 l'arrête aussitôt. |
Les options fixent la valeur au démarrage ; elles s'écrivent aussi, sous le même nom avec des soulignés, dans miraj_config.xml (chapitre 16, §16.2). SET GLOBAL les change ensuite jusqu'au prochain redémarrage (SHOW VARIABLES LIKE 'change_events%' les montre ; les deux durées s'affichent avec six décimales, 600.000000). Une valeur invalide empêche le serveur de démarrer. En édition Express, ces options sont acceptées et sans effet.
21.7 Sur un cluster (édition Cluster)#
Un abonné peut se connecter au primaire ou à un secondaire : chaque nœud publie les changements de ses propres tables.
- Sur le primaire, les événements sont publiés à la validation, comme sur un serveur seul.
- Sur un secondaire, ils sont publiés quand le secondaire applique le journal reçu du primaire. Il rend les mêmes événements que le primaire, dans le même ordre, avec un
commit_seqcommun à chaque transaction ; ils arrivent seulement avec le retard de réplication (LAG_SECONDS, §17.5.2). Les changements de structure (SCHEMA) y sont aussi rendus. - Sur un secondaire,
session_idvautNULL: la session qui a écrit est sur le primaire. - Les filtres (
COLUMNS,WHERE,WITH ROW) s'appliquent de la même façon sur tous les nœuds. - Les tables partitionnées s'écoutent sous leur nom, sur tous les nœuds, quelle que soit la partition écrite. Une ligne qui change de partition change de clé : elle donne
DELETEpuisINSERT. Le seuilchange_events_bulk_rowss'entend par partition. UnTRUNCATEde la table, ouALTER TABLE … TRUNCATE PARTITION, donne unBULK(motifTRUNCATE PARTITION) par partition vidée plutôt qu'unTRUNCATE. NOTIFYreste local au nœud qui l'exécute : il ne passe pas par la réplication. Comme les écritures, et donc lesNOTIFYdes déclencheurs, s'exécutent sur le primaire, une application qui écoute un canal doit s'abonner sur le primaire.- Chaque nœud a sa propre numérotation et ses propres réglages
change_events_…. Un numéro obtenu sur un nœud n'a pas de sens sur un autre. - Bascule : après une promotion, un client qui se reconnecte à un autre nœud avec
LISTEN … FROM nreçoitRESYNCet relit ses données. Un client déjà abonné au secondaire promu n'est pas touché : le nœud garde sa numérotation et continue de publier.
Stratégie recommandée : abonner les écrans et caches en lecture sur un secondaire, pour décharger le primaire, et les canaux NOTIFY sur le primaire. Sur RESYNC, relire les données puis se réabonner (§21.3).
Écarts entre nœuds, rares en pratique :
- Avec le modèle de concurrence pessimiste (
--concurrency pessimistic), une écriture hors transaction peut être décrite plus finement par un secondaire que par le primaire. Le secondaire rend unUPDATEde clé primaire enDELETE+INSERT, ne cite que les colonnes réellement changées, et détaille ligne à ligne certainsDELETEque le primaire résume enBULK. Le modèle multiversion, par défaut, donne les mêmes événements sur tous les nœuds. - Deux nœuds réglés avec des
change_events_bulk_rowsdifférents ne résument pas les mêmes transactions enBULK. - Les lignes d'un
CREATE TABLE … SELECTsont rendues par un secondaire si un abonnement porte déjà sur ce nom de table (par exemple resté après unDROP TABLE), pas par le primaire.
21.8 Limites#
- Les événements sont gardés en mémoire seulement : un redémarrage les perd, et le client reçoit
RESYNC. - Une session a une seule position pour tous ses abonnements : si des événements quittent la file avant qu'elle les lise, elle reçoit
RESYNC, même si ces événements concernaient d'autres tables. - Après
RENAME TABLE, l'abonnement reste sur l'ancien nom (le motif de l'événementSCHEMAdonne le nouveau) : se réabonner sous le nouveau nom. - Modèle pessimiste, écriture hors transaction : un
UPDATEde la clé primaire est rendu sous la nouvelle clé seulement ; une suppression déjà compactée donneBULK. - Si l'horloge du serveur a reculé entre deux démarrages, un numéro ancien peut, très rarement, retomber dans la plage du démarrage en cours.