Todo comenzó con una tarea sencilla: mostrar nuevos productos al usuario, teniendo en cuenta sus preferencias individuales. Y si no hubo problemas para obtener nuevos productos, entonces correlacionar los nuevos productos con las preferencias (analizar estadísticas) ya creaba una carga tangible (por ejemplo, definamos en 4 segundos). La peculiaridad de la tarea era que organizaciones enteras podían actuar como usuarios. Y no es raro que entre 200 y 300 solicitudes de un usuario lleguen al servidor a la vez (en 2-3 segundos). Aquellos. se genera el mismo bloque para muchos usuarios a la vez.
La solución obvia es almacenarlo en la memoria RAM (no expondremos el DBMS a la violencia, lo que lo obligará a procesar un gran flujo de llamadas). Esquema clásico:
- La solicitud llegó
- Comprobando la caché. Si hay datos en él y no están desactualizados, simplemente los devolvemos.
- Sin datos => generando un problema
- Enviamos al usuario
- Además, lo agregamos al caché, indicando el TTL
La desventaja de esta solución: si no hay datos en la caché, todas las solicitudes que llegaron durante la primera generación los generarán, gastando los recursos del servidor en esto (picos de carga). Y, por supuesto, todos los usuarios esperarán en la "primera llamada".
También tenga en cuenta que con valores de caché individuales, la cantidad de registros puede crecer tanto que la RAM del servidor disponible simplemente no es suficiente. Entonces parece lógico utilizar un servidor HDD local como almacenamiento en caché. Pero inmediatamente perdemos velocidad.
¿Cómo ser?
Lo primero que me viene a la mente: sería genial almacenar registros en 2 lugares: en la RAM (solicitada con frecuencia) y en el disco duro (todos o solo en raras ocasiones). El concepto de "datos fríos y calientes" en su forma más pura. Hay muchas implementaciones de este enfoque, por lo que no nos detendremos en él. Designemos este componente como 2L. En mi caso, se implementó con éxito en base al Scylla DBMS.
Pero, ¿cómo deshacerse de las reducciones cuando el caché está desactualizado? Y aquí incluimos el concepto de 2R, cuyo significado es simple: para el registro de caché, debe especificar no 1 valor TTL, sino 2. TTL1 es una marca de tiempo que significa que "los datos están desactualizados, deben regenerarse, pero aún puede usarlos"; TTL2: "todo está tan desactualizado que ya no se puede utilizar".
Por lo tanto, obtenemos un esquema de almacenamiento en caché ligeramente diferente:
- La solicitud llegó
- Buscamos datos en la caché. Si los datos están ahí y no están desactualizados (t <TTL1), se los devolvemos al usuario, como de costumbre, y no hacemos nada más.
- Los datos están ahí, desactualizados, pero puede usar (TTL1 <t <TTL2) - déselo al usuario E inicialice el procedimiento para actualizar el registro de caché
- No hay ningún dato (eliminado después de que expira TTL2); lo generamos "como de costumbre" y lo escribimos en la caché.
- Después de entregar el contenido al usuario o en un flujo paralelo, realizamos los procedimientos para actualizar los registros de la caché.
Como resultado, tenemos:
- si los registros de caché se utilizan con suficiente frecuencia, el usuario nunca se encontrará en una situación de "esperar a que se actualice la caché"; siempre recibirá un resultado listo para usar.
- si la cola de "actualizaciones" está bien organizada, entonces es posible lograr el hecho de que en el caso de varios accesos simultáneos al registro con TTL1 <t <TTL2, solo una tarea de actualización estará en la cola y no varias idénticas.
A modo de ejemplo: para el servicio de noticias, puede especificar TTL1 = 1 hora (sin embargo, el contenido nuevo no aparece con mucha intensidad) y TTL2 - 1 semana.
En el caso más simple, el código PHP para implementar 2R podría ser:
$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);
}
}
En la práctica, por supuesto, es probable que la implementación sea más difícil. Por ejemplo, un generador de registros de caché es un script independiente que se lanza como servicio; cola: a través de Rabbit, el letrero "tal clave ya está en la cola para la regeneración", a través de Redis o Scylla.
Entonces, si combinamos el enfoque de “dos bandas” y el concepto de datos “calientes / fríos”, obtenemos 2R2L.
¡Gracias!