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.
<dependency>
<groupId>io.github.suppierk</groupId>
<artifactId>spring-boot-multilevel-cache-starter</artifactId>
<version>3.4.1.1</version>
</dependency>
implementation 'io.github.suppierk:spring-boot-multilevel-cache-starter:3.4.1.1'
- 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
- 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
- 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
is1h
- And
spring.cache.multilevel.local.expiry-jitter
is50
(percents) - Then entries in local cache will expire in approximately
15-45m
:
- If
(1h / 2) * (1 ± ((50 / 100) * RNG(0, 1))) ->
30m * (1 ± MAXRNG(0.5)) ->
30m * RANGE(0.5, 1.5) ->
15-45m
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