Géo & cache chaud
1. Géo & cache chaud (couche transverse Redis)
Section titled “1. Géo & cache chaud (couche transverse Redis)”1.1 En un coup d’œil
Section titled “1.1 En un coup d’œil”La couche chaude, ici incarnée par Redis, représente principalement toutes les parties des tables SQL relues très fréquemment par des mécanismes qui doivent refléter le réel à la minute près. Ainsi, toutes les données inhérentes au matching, à la tension, aux workers, aux missions et aux compétences sont enregistrées et maintenues en chaud dans Redis. J’ai donc d’un côté MySQL pour le maintien de la vérité, et Redis comme surcouche d’efficience alimentée par SQL.
1.2 Comment ça marche
Section titled “1.2 Comment ça marche”Prenons un cas concret. Un travailleur se met en ligne. En base, son ready_to_work passe à 1, et dans la foulée de la même action, une fonction utilitaire l’inscrit dans Redis (un set par compétence). SQL et Redis bougent ensemble, jamais l’un sans l’autre. Voici les principaux cas.
| Action utilisateur | Écriture SQL | Écriture Redis |
|---|---|---|
| Worker passe en ligne | ready_to_work = 1 | SADD dans le set de chaque compétence + label « online » |
| Worker met à jour sa position | mise à jour des coordonnées | GEOADD dans le GeoSet des workers (workers:position), même membre = mise à jour |
| Worker se déconnecte | ready_to_work = 0 | SREM de chaque set |
| Mission créée ou rouverte | statut open | GEOADD (position) dans le GeoSet des missions |
| Mission fermée ou pourvue | statut ≠ open | retrait du GeoSet |
| Position d’une mission change | mise à jour des coordonnées | GEOADD (même membre = mise à jour) |
Concrètement, deux règles tiennent cette cohérence.
- On n’appelle jamais Redis directement pour écrire. J’ai créé des fonctions utilitaires qui, elles, appellent Redis. Ainsi le principe DRY évite bien des erreurs potentielles, avec un seul point de responsabilité et la lib encapsulée.
- Ensuite, pour chaque action utilisateur concernée, celle qui écrit dans MySQL, j’appelle une de ces fonctions utilitaires, de sorte que Redis ne dérive jamais de SQL.
1.3 Choix de conception (ADR)
Section titled “1.3 Choix de conception (ADR)”Les bons choix sont ceux pris en considérant notre contexte spécifique. Je devais donc réfléchir à ce que nous visions, ce que nous étions, le temps que nous avions et choisir la bonne solution au regard des critères ci-dessous :
-
Tout d’abord, bien que nous ayons de hautes ambitions, notre flux se limite aujourd’hui à 500 travailleurs pour 35 restaurants. On peut donc le considérer comme modeste.
-
Notre équipe est tout aussi modeste, composée uniquement de moi-même comme personnel qualifié, et de stagiaires quand le besoin s’en faisait sentir.
-
Mais en dépit de ces contraintes, nous visons des sommets et ne devons surtout pas nous fermer de portes d’entrée de jeu.
Se posent donc trois possibilités pour l’architecture du caching :
| Stratégie | Avantages | Scalabilité | Inconvénient |
|---|---|---|---|
| Inline (write-through) | Cohérence immédiate, simplicité, une seule brique, aucune infra en plus | Bonne à flux modeste ; le coût grandit avec le volume d’écritures (chaque écriture touche Redis) | Couplage fort (chaque écriture doit penser à Redis), dérive possible si l’écriture Redis échoue |
| Job asynchrone (queue) | Découplé, ré-essayable, n’alourdit pas la requête | Absorbe les pics et scale horizontalement (workers de queue) | Cohérence en léger retard (eventual), Redis temporairement en retard sur SQL, une queue de plus à opérer |
| event-driven ou CDC | Centralisé, découplé du code métier, difficile d’oublier une écriture | Robuste à grande échelle, mais dépend de l’infra CDC | Infra plus lourde et complexe, souvent surdimensionnée pour un flux modeste |
Vous vous en doutez certainement, à la manière dont j’ai amené le sujet. J’ai adopté la première solution. Nous étions en plein refactor du back-end, avec des enjeux business imminents et une équipe réduite (c’est peu de le dire). Il était impensable de monter une véritable architecture event-driven (ou CDC) dans ces conditions, et c’était inutile. En effet, la première stratégie permet de tenir sans problème notre premier palier de 5 000 utilisateurs (encore loin). À la suite de quoi on pourra commencer à découpler et fiabiliser, en passant sur un système de job asynchrone avec retries.
C’est donc un véritable arbitrage qui a été fait ici, où j’ai choisi sciemment la manière la plus simple à comprendre et à entretenir au quotidien seul, et surtout la plus rapide à livrer sans trop rogner sur la qualité produit. Elle a naturellement des défauts, comme l’hyper-couplage, le fait qu’un oubli puisse créer de la désynchronisation, ou qu’une erreur d’écriture Redis survienne sans ré-essai possible tout en ne bloquant pas la fonction. Mais j’ai estimé que tous ces points étaient une dette acceptable à notre échelle, et que la sur-ingénierie aurait été un mal bien plus grand pour le business.
1.4 Invariants & modes de panne
Section titled “1.4 Invariants & modes de panne”Toute la robustesse de cette couche tient sur une règle simple, MySQL est la seule vérité, Redis n’en est qu’une projection. Rien ne vit dans Redis sans exister d’abord en base, et tout ce qui est en Redis peut être régénéré depuis MySQL. Redis ne fait donc jamais autorité ; en cas de doute, c’est toujours SQL qui tranche. C’est cet invariant qui rend la réhydratation toujours possible.
S’ajoute une seconde garantie, mes écritures Redis sont idempotentes. Un GEOADD sur un membre déjà présent met simplement à jour sa position, un SADD ou un SREM rejoué ne change rien. Rejouer une écriture, lors d’un retry ou d’une réhydratation complète, ne peut donc jamais corrompre l’état.
Côté pannes, trois cas et leur parade :
- Redis tombe ou perd sa RAM. Les GeoSets et les sets disparaissent d’un coup. La commande
redis:rehydratereconstruit tout depuis MySQL (missions ouvertes, workersready_to_workavec leurs compétences et positions) et se relance au démarrage via le flagboot:rehydrated. Comme MySQL détient la vérité, la reconstruction est toujours possible, sans perte. - Une écriture Redis échoue alors que le SQL a réussi. C’est le défaut inévitable de mon choix inline, pas de retry immédiat, pas de rollback possible, donc Redis peut se retrouver en léger décalage sur SQL. Ce n’est jamais dangereux, puisque Redis n’est pas autoritaire — au pire un worker ou une mission manque temporairement du chaud, et la prochaine réhydratation resynchronise l’ensemble. À notre échelle, ce risque n’est jamais survenue et est tolérable dans l’absolu.
- Des entrées périmées s’accumulent. Un worker mal déconnecté peut rester « en ligne » dans Redis. Le cron
neeko:auto-offline-users, exécuté toutes les heures, repasse hors ligne tout worker sans signe de vie depuis plus d’une semaine et nettoie ses entrées.
1.5 Scalabilité
Section titled “1.5 Scalabilité”Honnêtement, ce n’est pas très compliqué à cette échelle. Avec 500 travailleurs et 35 restaurants, l’inline tient très large, et je l’estime bon jusqu’à environ 5 000 utilisateurs sans le moindre souci. Le seul goulot quand la charge monte, c’est le couplage lui-même. En effet, chaque écriture SQL paie aussi son écriture Redis, dans la même requête. Négligeable aujourd’hui, mais au-delà d’un certain volume ça alourdit les requêtes et le couplage devient trop risqué.
La montée se fait alors par paliers, exactement dans l’ordre des stratégies présentées en 1.3 :
- Au-delà de ~5 000 utilisateurs, je passe l’écriture Redis en job asynchrone, elle sort de la requête, part sur la queue avec des retries, ce qui découple, absorbe les pics et permet une meilleur gestion des erreurs potentiels.
- Plus loin encore, si le volume l’exige vraiment, un CDC (capture des changements en sortie de base) prend le relais pour un découplage total à grande échelle via un système d’évènement. Mais on est encore trés loin de ce cas de figure.
Le point important, c’est que cette montée est peu coûteuse. Comme rien n’appelle Redis directement ( tout passe par mes fonctions utilitaires - voir 1.2 ) passer de l’inline à l’asynchrone ne touche que l’intérieur de ces fonctions, jamais le code métier. J’ai donc choisi le simple aujourd’hui, mais l’architecture garde la porte du scalable déjà ouverte.
Périmètre — fichiers & dépendances
Section titled “Périmètre — fichiers & dépendances”Fichiers
Services/RedisCache/RedisGeoService— le cœur de la couche : GeoSet des missions ouvertes, sets de workers par compétence, configuration du zoning et réhydratation.Services/RedisCache/WorkerCacheService— la position des workers en GeoSet (workers:position) et leur état (label « en ligne »).Console/Commands/RehydrateRedis— reconstruit les GeoSets depuis MySQL (redis:rehydrate, option--if-needed).Console/Commands/AutoOfflineUsers— repasse hors ligne les workers inactifs (nettoyage d’état).
Dépendances
- Redis — le stockage chaud en RAM lui-même (GeoSets, sets par compétence).
- MySQL — source de vérité pour la réhydratation (missions ouvertes, workers
ready_to_work, compétences). - AppSetting — la configuration du zoning (paliers de rayon selon l’âge de la mission).
Consommé par — le domaine Matching (pré-filtre géographique). Le domaine Tension réutilise le même GeoSet de missions, mais son cache dédié est traité sur sa propre page. C’est une couche transverse, pas un domaine métier.