django-cacheops

repository·master·Indexed 25 days ago

https://github.com/suor/django-cacheops

A Django application providing automatic or manual queryset caching with granular, event-driven invalidation using Redis. It includes features such as the @cached_as and @cached_view_as decorators, manual invalidation via the 'invalidate' management command, and support for file-based caching. It provides tools to handle the dog-pile effect with locking, cache-aware mass updates via .invalidated_update(), and template fragment caching for Django and Jinja2.

Tokens
5.2K
Snippets
15
Records
27
Agent score
81%

What's inside django-cacheops

  1. Prevent the dog-pile effect with locking

    master

    To prevent multiple processes from simultaneously performing the same heavy task (the dog-pile effect), you can enable locking. This works with @cached_as and querysets.

    Locking has no overhead on cache hits.

    @cached_as(qs, lock=True)
    def heavy_func(...):
        ...
    
    # Or via queryset
    for item in qs.cache(lock=True):
        ...
  2. Understand Cacheops limitations and caveats

    master

    Cacheops has several design compromises and technical limitations regarding query invalidation granularity and supported query types. Understanding these helps avoid unexpected cache behavior:

    • Invalidation Granularity: Conditions other than __exact, __in, and __isnull=True do not make invalidation more granular. Conditions on TextFields, FileFields, and BinaryFields also do not improve granularity.
    • Relationships: Updating a select_related object does not invalidate the cache for the parent queryset. Use .prefetch_related() instead to ensure proper invalidation.
    • Mass Updates: Mass updates do not trigger invalidation by default. Use .invalidated_update() to ensure the cache is cleared.
    • Query Types: Cacheops does not work correctly with .raw() SQL queries, subqueries (conditions on subqueries don't affect invalidation), or multi-table inheritance.
    • Slicing: Sliced queries are invalidated as if they were non-sliced queries.

    If you encounter these limitations, you can often bypass them using the @cached_as() decorator.

  3. Cache Django template fragments

    master

    Cacheops provides template tags for Django and Jinja2 to cache fragments of a template.

    Django

    Use {% load cacheops %} and the following tags:

    • {% cached_as <queryset> <timeout> <fragment_name> [<extra1> ...] %}
    • {% cached <timeout> <fragment_name> [<extra1> ...] %}

    To invalidate a fragment, use from cacheops import invalidate_fragment and call invalidate_fragment(fragment_name, extra1, ...).

    Jinja2

    Add cacheops.jinja2.cache to your Jinjin2 extensions. The tags {% cached_as %} and {% cached %} work similarly to the Django versions.

  4. Install django-cacheops via pip

    master

    Install the package using pip to add automatic or manual queryset caching to your Django project.

    Requirements:

    • Python 3.8+
    • Django 3.2+
    • Redis 4.0+
    $ pip install django-cacheops
    
    # Or from github directly
    $ pip install git+https://github.com/Suor/django-cacheops.git@master
  5. Performance tips for Cacheops and Django ORM

    master

    Follow these best practices to maximize performance:

    1. Serialization: Use django-pickling to speed up model instance pickling/unpickling.
    2. Querysets: Use .inplace() for micro-optimizations in hot code paths to avoid queryset cloning.
    3. Template Caching: Use template fragment caching where possible. Caching a string is significantly faster than pickling/unpickling a list of model instances.
    4. Redis Configuration: Run a separate Redis instance for the cache and disable persistence. Manually trigger SAVE or BGSAVE to maintain data if necessary.
    5. Filter Complexity: Avoid caching querysets with many complex filters. High filter complexity can lead to frequent cache misses and slow down invalidation processes. Consider disabling cache for requests that use many primary fields in filters.
    6. Granularity: Split queries into smaller parts to allow for more granular invalidation.
  6. Configure CACHEOPS_SENTINEL for Redis Sentinel

    master

    If you are using Redis Sentinel, specify the CACHEOPS_SENTINEL dictionary. All keys are passed to the redis.sentinel.Sentinel constructor.

    CACHEOPS_SENTINEL = {
        'locations': [('localhost', 26379)], # sentinel locations, required
        'service_name': 'mymaster',          # sentinel service name, required
        'socket_timeout': 0.1,
        'db': 0                              
        # ... everything else is passed to Sentinel()
    }
  7. Configure CACHEOPS model caching rules

    master

    The CACHEOPS dictionary defines which operations (ops) are cached for specific models and for how long (timeout).

    Key options:

    • ops: A set of operations to cache. Supported values: get, fetch, count, aggregate, exists. Use 'all' as an alias for all five. An empty set () or None disables caching.
    • timeout: Cache duration in seconds.
    • local_get: If True, caches simple gets in process local memory (very fast, but not invalidated until process restart).
    • cache_on_save: If True, writes an instance to cache upon save (caches by primary key). If a field name is provided, it caches by that field.
    • CACHEOPS_DEFAULTS: A dictionary to set default ops and timeout for all models to avoid repetition.

    Warning: Using '*.*' with non-empty ops is not recommended as it may cache unintended tables like migrations. Prefer 'app_name.*'.

    CACHEOPS_DEFAULTS = {
        'timeout': 60*60
    }
    
    CACHEOPS = {
        # Cache User.get() calls for 15 minutes
        'auth.user': {'ops': 'get', 'timeout': 60*15},
    
        # Cache all gets and fetches for auth models for an hour
        'auth.*': {'ops': {'fetch', 'get'}, 'timeout': 60*60},
    
        # Cache all queries (get, fetch, count, aggregate, exists) for Permission
        'auth.permission': {'ops': 'all', 'timeout': 60*60},
    
        # Enable manual caching on all other models with 1 hour timeout
        '*.*': {'timeout': 60*60},
    
        # Explicitly forbid caching for a specific app
        'some_app.*': None,
    }
  8. Configure custom serialization for Cacheops

    master

    By default, Cacheops uses pickle. You can specify a custom serializer by setting CACHEOPS_SERIALIZER in your settings. The serializer must be a module or a class providing .dumps() and .loads() methods.

    Example using dill or fixing a specific pickle protocol:

    # Use dill
    CACHEOPS_SERIALIZER = 'dill'
    
    # Use a custom class to fix pickle protocol
    import pickle
    
    class CACHEOPS_SERIALIZER:
        dumps = lambda data: pickle.dumps(data, 3)
        loads = pickle.loads
  9. Configure CACHEOPS_REDIS connection

    master

    Define your Redis connection settings. You can use a dictionary for detailed configuration or a connection URL string.

    It is highly recommended to use a separate Redis database or instance for Cacheops.

    # Using a dictionary
    CACHEOPS_REDIS = {
        'host': 'localhost',
        'port': 6379,
        'db': 1,             # Highly recommended to use a separate DB
        'socket_timeout': 3, 
        'password': '...',     
        'unix_socket_path': '' 
    }
    
    # Using a URL
    CACHEOPS_REDIS = "redis://localhost:6379/1"
    CACHEOPS_REDIS = "unix://path/to/socket?db=1"
    CACHEOPS_REDIS = "redis://:password@localhost:6379/1"
  10. Improve cache granularity by splitting database queries

    master

    To achieve more precise cache invalidation, split complex queries into smaller, simpler ones. This allows individual parts of the data to be invalidated independently rather than flushing larger chunks of the cache.

    Example: Inefficient (Broad Invalidation)

    Post.objects.filter(category__slug="foo")
    # A single query that invalidates on ANY Post change OR any Category with slug='foo' change.

    Example: Efficient (Granular Invalidation)

    Post.objects.filter(category=Category.objects.get(slug="foo"))
    # Two queries: one for the category and one for the post. 
    # This invalidates only on specific category changes or specific post changes.
    Post.objects.filter(category__slug="foo")
    
    # A single database query, but will be invalidated not only on
    # any Category with .slug == "foo" change, but also for any Post change
    
    Post.objects.filter(category=Category.objects.get(slug="foo"))
    # Two queries, each invalidates only on a granular event:
    # either category.slug == "foo" or Post with .category_id == <whatever is there>
  11. Cache views with @cached_view_as

    master

    Use @cached_view_as to cache and invalidate a Django view based on a queryset. The cache key is constructed using the request path.

    • Extra Keys: Use extra to include request-specific data (like request.user.is_staff) in the cache key.
    • Class-Based Views: You can wrap the .as_view() method of a CBV.
    from cacheops import cached_view_as
    
    # Function-based view
    @cached_view_as(News, extra=lambda req: req.user.is_staff)
    def news_index(request):
        return render(request, 'news.html')
    
    # Class-based view
    class NewsIndex(ListView):
        model = News
    
    news_index = cached_view_as(News, ...)(NewsIndex.as_view())