Skip to content

SuppieRK/spring-boot-multilevel-cache-starter

Repository files navigation

Spring Boot multi-level cache starter

Opinionated version of multi-level caching for Spring Boot with Redis as L2 (remote) cache and Caffeine as L1 (local) cache with a Circuit Breaker pattern for L2 cache calls.

This version does not allow setting most of the local cache properties in favor of managing local cache expiry by itself.

Use cases

Suitable for

  • Microservices working with immutable cached entities under low latency requirements
    • The goal is to not only reduce the number of calls to external service but also reduce the number of calls to Redis

Not a good fit for

  • Mutable cached entities
  • Entities with short time to live (< 5 minutes)
  • Cases when entities in local cache must outlive entities in distributed cache
    • Consider using only local cache instead
  • Cases when all calls to Redis must be synchronized with distributed locks

Ideas

  • Use well-known Spring primitives for implementation
  • Microservices environment needs to fit the requirement of fault tolerance:
    • Redis calls covered by Resilience4j Circuit Breaker which allows falling back to use local cache at the cost of increased latency and more calls to external services.
  • Redis TTL behaves similar to expireAfterWrite in Caffeine which allows us to set randomized expiry time for local cache:
    • This is useful to ensure that local cache entries will expire earlier for a higher chance to hit Redis instead of performing external call.
    • This also implicitly reduces the load on the Redis by spreading calls to it over time.
    • In the case of Redis connection errors, randomized expiry and Circuit Breaker will help to mitigate thundering herd problem.
  • Expiry randomization follows the rule: (time-to-live / 2) * (1 ± ((expiry-jitter / 100) * RNG(0, 1))), for example:
    • If spring.cache.multilevel.time-to-live is 1h
    • And spring.cache.multilevel.local.expiry-jitter is 50 (percents)
    • Then entries in local cache will expire in approximately 15-45m:
(1h / 2) * (1 ± ((50 / 100) * RNG(0, 1))) ->
30m * (1 ± MAXRNG(0.5)) ->
30m * RANGE(0.5, 1.5) ->
15-45m

Usage

Maven

<dependency>
  <groupId>io.github.suppierk</groupId>
  <artifactId>spring-boot-multilevel-cache-starter</artifactId>
  <version>3.2.5.2</version>
</dependency>

Gradle

implementation 'io.github.suppierk:spring-boot-multilevel-cache-starter:3.2.5.2'

Default configuration

spring:
  data:
    redis:
      host: ${HOST:localhost}
      port: ${PORT:6379}
  cache:
    type: redis
    
    # These properties are custom
    multilevel:
      # Redis properties
      time-to-live: 1h
      use-key-prefix: false
      key-prefix: ""
      topic: "cache:multilevel:topic"
      # Local Caffeine cache properties
      local:
        max-size: 2000
        expiry-jitter: 50
        expiration-mode: after-create
        # other valid values for expiration-mode: after-update, after-read
      # Resilience4j Circuit Breaker properties for Redis
      circuit-breaker:
        failure-rate-threshold: 25
        slow-call-rate-threshold: 25
        slow-call-duration-threshold: 250ms
        sliding-window-type: count_based
        permitted-number-of-calls-in-half-open-state: 20
        max-wait-duration-in-half-open-state: 5s
        sliding-window-size: 40
        minimum-number-of-calls: 10
        wait-duration-in-open-state: 2500ms

Honorable mentions