Tout a commencĂ© par une tĂąche simple: prĂ©senter de nouveaux produits Ă l'utilisateur, en tenant compte de ses prĂ©fĂ©rences individuelles. Et s'il n'y avait aucun problĂšme pour obtenir de nouveaux produits, la corrĂ©lation des nouveaux produits avec les prĂ©fĂ©rences (analyse des statistiques) crĂ©ait dĂ©jĂ une charge tangible (par exemple, dĂ©finissons-la Ă 4 secondes). La particularitĂ© de la tĂąche Ă©tait que des organisations entiĂšres pouvaient agir en tant qu'utilisateurs. Et il n'est pas rare que 200 Ă 300 requĂȘtes concernant un utilisateur arrivent sur le serveur en mĂȘme temps (dans les 2-3 secondes). Ceux. le mĂȘme bloc est gĂ©nĂ©rĂ© pour plusieurs utilisateurs Ă la fois.
La solution évidente est de le mettre en cache dans la RAM (nous n'exposerons pas le SGBD à la violence, le forçant à traiter un grand flux d'appels). Schéma classique:
- La demande est venue
- Vérification du cache. S'il contient des données et qu'elles ne sont pas obsolÚtes, nous les restituons simplement.
- Pas de données => générer un problÚme
- Nous envoyons Ă l'utilisateur
- De plus, nous l'ajoutons au cache, indiquant le TTL
L'inconvĂ©nient de cette solution: s'il n'y a pas de donnĂ©es dans le cache, toutes les requĂȘtes qui sont arrivĂ©es lors de la premiĂšre gĂ©nĂ©ration vont les gĂ©nĂ©rer, dĂ©pensant des ressources serveur sur cela (pics de charge). Et bien sĂ»r, tous les utilisateurs attendront au "premier appel".
Notez également qu'avec des valeurs de cache individuelles, le nombre d'entrées peut augmenter tellement que la RAM du serveur disponible n'est tout simplement pas suffisante. Ensuite, il semble logique d'utiliser un serveur HDD local comme stockage cache. Mais nous perdons immédiatement de la vitesse.
Comment ĂȘtre?
La premiÚre chose qui me vient à l'esprit: ce serait bien de stocker les enregistrements à 2 endroits - dans la RAM (fréquemment demandée) et le disque dur (tous ou rarement demandés). Le concept de «données chaudes et froides» dans sa forme la plus pure. Il existe de nombreuses implémentations de cette approche, nous ne nous attarderons donc pas dessus. Désignons simplement ce composant comme 2L. Dans mon cas, il est implémenté avec succÚs sur la base du SGBD Scylla.
Mais comment se dĂ©barrasser des tirages lorsque le cache est pĂ©rimĂ©? Et ici, nous incluons le concept de 2R, dont la signification est simple: pour un enregistrement de cache, vous devez spĂ©cifier non pas 1 valeur TTL, mais 2. TTL1 est un horodatage qui signifie "les donnĂ©es sont obsolĂštes, elles doivent ĂȘtre rĂ©gĂ©nĂ©rĂ©es, mais vous pouvez toujours les utiliser"; TTL2 - "tout est tellement obsolĂšte qu'il ne peut plus ĂȘtre utilisĂ©."
Ainsi, nous obtenons un schéma de mise en cache légÚrement différent:
- La demande est venue
- Nous recherchons des données dans le cache. Si les données sont là et ne sont pas obsolÚtes (t <TTL1) - nous les rendons à l'utilisateur, comme d'habitude et ne faisons rien d'autre.
- Les données sont là , obsolÚtes, mais vous pouvez utiliser (TTL1 <t <TTL2) - donnez-les à l'utilisateur ET initialisez la procédure de mise à jour de l'enregistrement de cache
- Il n'y a aucune donnée (tuée aprÚs l'expiration de TTL2) - nous la générons "comme d'habitude" et l'écrivons dans le cache.
- AprÚs avoir servi le contenu à l'utilisateur ou dans un flux parallÚle, nous exécutons les procédures de mise à jour des enregistrements de cache.
En conséquence, nous avons:
- si les enregistrements de cache sont utilisĂ©s assez souvent, l'utilisateur ne se trouvera jamais dans la situation "d'attendre que le cache soit mis Ă jour" - il obtiendra toujours un rĂ©sultat prĂȘt Ă l'emploi.
- si la file des "mises à jour" est correctement organisée, alors il est possible de réaliser que dans le cas de plusieurs accÚs simultanés à un enregistrement avec TTL1 <t <TTL2, il n'y aura qu'une seule tùche de mise à jour dans la file, et non plusieurs identiques.
A titre d'exemple: pour un nouveau flux de produits, vous pouvez spécifier TTL1 = 1 heure (néanmoins, le nouveau contenu n'apparaßt pas de maniÚre trÚs intensive), et TTL2 - 1 semaine.
Dans le cas le plus simple, le code PHP pour implĂ©menter 2R pourrait ĂȘtre:
$tmp = cache_get($key);
If (!$tmp){
$items = generate_items();
cache_set($items, 60*60, 60*60*24*7);
}else{
$items = $tmp[âitemsâ];
If (time()-$tmp[âtmâ] > 60*60){
$need_rebuild[] = [âtoâ=>$key, âmethodâ=>âgenerate_itemsâ];
}
}
âŠ
//
echo json_encode($items);
âŠ
// ,
If (isset($need_rebuild) && count($need_rebuild)>0){
foreach($need_rebuild as $k=>$v){
$tmp = ['tm'=>time(), 'items'=>$$v[âmethodâ]];
cache_set($tmp, 60*60, 60*60*24*7);
}
}
Dans la pratique, bien entendu, la mise en Ćuvre sera probablement plus difficile. Par exemple, un gĂ©nĂ©rateur d'enregistrements de cache est un script distinct lancĂ© en tant que service; file d'attente - via Rabbit, le signe "une telle clĂ© est dĂ©jĂ dans la file d'attente pour la rĂ©gĂ©nĂ©ration" - via Redis ou Scylla.
Donc, si nous combinons l'approche «deux bandes» et le concept de données «chaudes / froides», nous obtenons 2R2L.
Merci!