logo cosasdedevs
Implementar un sistema de caché con Redis en Django

Implementar un sistema de caché con Redis en Django



My Profile
Nov 27, 2020

Por fin llegamos al último tutorial (por ahora) de esta serie en la que hemos estado trabajando con Redis. Para este tutorial vamos a implementar un sistema de caché con Redis en un blog hecho en Django. Esto nos permitirá reducir la carga de la página principal de una forma brutal, como ya veréis conforme vayamos avanzando.

He preparado un proyecto en GitHub para que podamos realizar este tutorial juntos, en el que actualmente tenemos un proyecto con un listado de posts. Os dejo el enlace para que descarguéis el proyecto https://github.com/albertorc87/redisxdjango/tree/initial. Solo debéis seguir los pasos del readme y en pocos minutos lo tendréis configurado en vuestro local 💪.

Proyecto sin Redis

Si ahora mismo lanzamos el proyecto como está tal cual, veremos como tarda bastante en cargar la página principal. En las pruebas que he realizado, me ha tardado de 20 a 40 segundos. También porque lo he configurado para que muestre todos los posts, todo hay que decirlo 😁, pero será una buena forma de ver como podemos optimizarlo con Redis 😎.

python manage.py runserver

Si recargamos la página una y otra vez, veremos que los tiempos son similares, pero no os preocupéis, vamos a mejorar estos tiempos con Redis 🚀.

Configurar Redis en Django

Para configurar Redis en Django, lo primero que tendremos que hacer es abrir el archivo redixdjango/settings.py y al final de este, añadir la configuración para poder conectarnos a Redis:

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/0",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

Aquí le decimos que vamos a utilizar la librería django_redis (que está en nuestro archivo requirements.txt y si has seguido los pasos del archivo readme, debería estar instalada) para utilizar el sistema de caché y en location realizamos la conexión a nuestra base de datos de Redis. Podéis ver más información acerca de esta librería pinchando aquí.

Lo siguiente que vamos a hacer, es implementar el sistema de caché para la página principal. Para ello, abrimos el archivo posts/views.py y realizamos las siguientes modificaciones para que quede así:

# Django
from django.views.generic import DetailView, ListView
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

# Models
from posts.models import Post

@method_decorator(cache_page(60*60), name='dispatch')
class PostsFeedView(ListView):
    template_name = 'posts/feed.html'
    model = Post
    ordering = ('-created',)
    paginate_by = 999
    context_object_name = 'posts'


class PostDetailView(DetailView):
    template_name = 'posts/detail.html'
    model = Post
    context_object_name = 'post'
    slug_field = 'url'
    slug_url_kwarg = 'url'

Lo que hacemos en este ejemplo, es añadir el decorador cache_page. En este caso como estamos usando una vista genérica, debemos ayudarnos de la función method_decorator e indicarle sobre que función queremos que se aplique, en este caso, en el método dispatch de la vista. Si tuviéramos una vista definida mediante una función, solo deberíamos añadir el decorador cache_page , tal y como se muestra en la documentación.

También le indicamos un tiempo de vida de la caché definido en segundos. Esto lo hacemos porque se considera una buena práctica siempre darle un tiempo de vida a nuestra caché y nosotros siempre vamos de la mano de las buenas prácticas 😁. Aquí nosotros le pasamos 60 * 60 que son 3600 segundos de vida, una hora vamos. Una vez pasado ese tiempo, la caché expirará y deberá generarse de nuevo.

Si ahora recargamos la página, veremos que sigue tardando bastante tiempo, pero ahora viene lo bueno, si recargamos una segunda vez, podremos ver como el tiempo se ha reducido de forma drástica, en mi caso vemos como pasamos a unos 75 milisegundos 😯.

Vale, esto está muy bien pero si yo ahora me voy al administrador y añado un nuevo post, no aparecerá hasta que expire la caché dentro de una hora y eso no mola nada, pero bueno, hay solución para todo, así que vamos a implementarla 😁.

Refrescar caché al añadir nuevos Posts

Para realizar este cambio, lo primero que necesitaremos será poder identificar la caché relacionada con la página principal. Para ello vamos a realizar un cambio en el archivo posts/views.py. Abrimos el archivo y modificamos el decorador para que ahora quede así:

@method_decorator(cache_page(60*60, key_prefix='main'), name='dispatch')

Lo que hemos hecho, es añadir el parámetro key_prefix. En él pasamos el valor main que será el valor que utilizaremos para identificar la caché.

Ahora vamos a ir al archivo posts/admin.py y lo que vamos a hacer es modificar la función de guardado para añadir una configuración extra.

Abrimos el archivo y sustituimos el código en él por el siguiente:

from django.contrib import admin
from django.test import Client
from django.core.cache import cache

from posts.models import Post
from posts import views

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    """Post admin."""

    list_display = ('id', 'user', 'title', 'created')
    search_fields = ('title', 'user__username', 'user__email')

    def save_model(self, request, obj, form, change):
        super().save_model(request, obj, form, change)

        # Si es un post nuevo, debemos cargar la caché de la página principal
        if not change:
            res = cache.keys('*main*')
            if res:
                delete_cache = cache.delete_many(res)
            c = Client()
            response = c.get('/', SERVER_NAME='127.0.0.1:8000')

Aquí lo que hemos hecho, es modificar el comportamiento del método save_model. Primero usamos super() para realizar el funcionamiento normal de este método que sería guardar un nuevo post o editarlo. Después de eso, verificamos si es un post nuevo, esto lo hacemos con la variable change. Si su valor es igual a false, significa que es un post nuevo.

Después utilizamos el método keys para buscar todas las claves que tenemos en Redis y que contengan la palabra main que fue el valor que le dimos anteriormente a la caché en el archivo posts/views.py.

Si obtenemos resultados, los borramos con el método delete_many.

Por último, utilizamos una herramienta que se suele utilizar para realizar tests pero que en nuestro caso nos viene de perlas que es la clase Client. Esta nos permite realizar peticiones a nuestra app.

Hacemos una petición al home y muy importante, pasamos el parámetro SERVER_NAME. Este le indica sobre que dominio queremos que realice la petición. Si no lo indicamos, lo realizará sobre un dominio de test y no veremos reflejados los cambios en la caché cuando posteriormente carguemos nuestra página. Yo he puesto el dominio de desarrollo que es con el que estoy haciendo pruebas. Para un entorno de producción se podría recuperar el dominio desde el que se realiza la petición desde el request o guardarlo en el archivo settings.py y luego acceder a él. Como siempre, ¡hay mil formas de hacerlo!.

Automatizar generación de caché cada x tiempo

Ahora que ya hemos solucionado el problema a la hora de crear nuevos posts, solo quedaría automatizar el proceso de regeneración de caché cuando esta expire, de esta forma, siempre tendremos la caché operativa y no repercutirá en la experiencia del usuario.

Para automatizar la generación de la caché, vamos a utilizar celery. Para el que no lo conozca, celery es un sistema de manejo de cola de tareas en el que podemos indicar un tiempo en el que queramos que se ejecute cierta tarea. Más adelante crearé un tutorial para entrar en profundidad en esta herramienta, mientras tanto, os dejo la documentación por si queréis echarle un vistazo.

Para instalar celery, solo tenemos que lanzar el siguiente comando:

pip install -U "Celery[redis]"

Ya que celery necesita un broker de mensajería y podemos usar Redis para ello, instalamos una versión de celery especialmente preparada para esta base de datos.

Una vez instalado, procederemos a configurarlo. Para ello, lo primero será crear un archivo en la carpeta redisxdjango llamado celery.py con el siguiente código:

import os

from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'redisxdjango.settings')

app = Celery('redisxdjango')

app.config_from_object('django.conf:settings', namespace='CELERY')

app.autodiscover_tasks()

app.conf.update(
    BROKER_URL = 'redis://127.0.0.1:6379/0',
)

Después modificaremos el archivo redisxdjango/init.py y añadiremos el siguiente código:

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

__all__ = ('celery_app',)

Ahora lo que vamos a hacer es crear una tarea y migraremos la funcionalidad para reiniciar la caché que teníamos en posts/admin.py. Para ello crearemos un archivo llamado tasks.py en la carpeta posts y añadiremos el siguiente código:

from redisxdjango.celery import app
from django.test import Client
from django.core.cache import cache


@app.task
def reboot_cache_main():
    res = cache.keys('*main*')
    if res:
        delete_cache = cache.delete_many(res)
    c = Client()
    response = c.get('/', SERVER_NAME='127.0.0.1:8000')

Por último, vamos al archivo posts/admin.py y modificamos el código para ahora utilizar la tarea que hemos creado anteriormente:

from django.contrib import admin

from posts.models import Post
from posts import tasks

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    """Post admin."""

    list_display = ('id', 'user', 'title', 'created')
    search_fields = ('title', 'user__username', 'user__email')

    def save_model(self, request, obj, form, change):
        super().save_model(request, obj, form, change)

        # Si es un post nuevo, debemos cargar la caché de la página principal
        if not change:
            tasks.reboot_cache_main()

Ahora solo nos queda automatizar una tarea para que cuando pase x tiempo, verifique el estado de la caché de la página principal, si queda poco tiempo para que expire, la regenerará.

Para realizar este proceso, volvemos al archivo posts/tasks.py y añadimos una nueva tarea de tal forma que el código quedará así:

from redisxdjango.celery import app
from django.test import Client
from django.core.cache import cache
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)

@app.task
def reboot_cache_main():
    res = cache.keys('*main*')
    if res:
        time_life = cache.ttl(res[0])
        delete_cache = cache.delete_many(res)
    c = Client()
    response = c.get('/', SERVER_NAME='127.0.0.1:8000')

@app.task
def reboot_cache_main_crontab():
    res = cache.keys('*main*')
    logger.info(f'Check cache')
    if res:
        for id_cache in res:
            time_life = cache.ttl(id_cache)
            logger.info(f'Time to expire {time_life}')
            if time_life is None:
                break
            if time_life > 300:
                return
            break
    logger.info(f'Reload cache')
    c = Client()
    response = c.get('/', SERVER_NAME='127.0.0.1:8000')

La nueva tarea es similar a la anterior, pero en este caso verificamos si tiene un tiempo de vida mayor a 300 segundos, si es así, no hacemos nada, si no, la volvemos a recargar.

Para automatizar la tarea debemos volver al archivo redixdjango/celery.py y ahí modificarlo para que ahora contenga una configuración adicional:

import os

from celery import Celery
from celery.schedules import crontab

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'redisxdjango.settings')

app = Celery('redisxdjango')

app.config_from_object('django.conf:settings', namespace='CELERY')

app.autodiscover_tasks()

app.conf.update(
    BROKER_URL = 'redis://127.0.0.1:6379/0',
)

app.conf.beat_schedule = {
    'reboot-cache-main-crontab': {
        'task': 'posts.tasks.reboot_cache_main_crontab',
        'schedule': crontab(minute="*"),
    },
}

Hemos añadido una configuración en la que nosotros podemos automatizar tareas según un tiempo dentro de app.conf.beat_schedule. Ahí le decimos que tarea queremos automatizar y en schedule con crontab le decimos que queremos que la revise cada minuto. Si queréis más información acerca de las tareas periódicas, os dejo el link a la doc.

Por último y para que funcione todo, debemos lanzar dos procesos de celery. El primero se encargará de revisar que tareas están configuradas por horario y el segundo proceso se encargará de ejecutar las tareas.

Para ello, abrimos dos terminales y lanzamos en cada una los siguientes comandos:

celery -A redisxdjango beat -l INFO
celery -A redisxdjango worker -l INFO -P solo --without-gossip
 --without-mingle --without-heartbeat -Ofair

El segundo proceso recibirá los trabajos y nosotros ahí podremos ver si se ejecuta nuestra tarea, el logging que hemos añadido y si hay errores.

Conclusiones

Si tenemos proyectos que se han vuelto muy lentos por la cantidad de datos que manejan, Redis puede ser una buena opción para optimizarlos. Os animo a que lo implementéis en vuestros proyectos y como siempre, cualquier duda la podéis dejar en los comentarios 😁.

También os dejo el enlace al proyecto ya finalizado https://github.com/albertorc87/redisxdjango.

Como siempre, os recomiendo seguirme en Twitter y nos vemos en el siguiente tutorial 👋.

1563 vistas

🐍 Sígueme en Twitter

Si te gusta el contenido que subo y no quieres perderte nada, sígueme en Twitter y te avisaré cada vez que cree contenido nuevo 💪
Luego ¡Te sigo!

Nos tomamos en serio tu privacidad

Utilizamos cookies propias y de terceros para mejorar la experiencia del usuario a través de su navegación. Si pulsas entendido aceptas su uso. Ver política de cookies.