Skip to content

Repository files navigation

ResiCache

A protection-enhancement annotation ecosystem for Spring Cache — beyond @Cacheable, use a single @RedisCacheable annotation to add cache-penetration, cache-breakdown, cache-avalanche, and hot-key early-refresh defenses to your Redis cache. Protection is injected through a composable responsibility chain, without re-inventing AOP.

CI License: MIT

Project status: early (v0.0.2) · Non-SLA best-effort · solo-maintained. Read ⚠️ Known Limitations before any production use.

简体中文

What it is

Spring Cache (@Cacheable / @CachePut / @CacheEvict) solves "caching", not "protection" — cache penetration, breakdown, avalanche, and hot-key expiry are left to the business layer. ResiCache turns these defenses into declarative capabilities via @RedisCacheable enhancement annotations and a composable responsibility chain.

  • Coexists with Spring Cache: extends RedisCacheManager / CacheInterceptor — does not replace @EnableCaching, does not re-invent AOP.
  • Difference from JetCache: JetCache focuses on multi-level caching; ResiCache focuses on cache-defense-in-depth — every handler on the chain is pluggable and composable, which JetCache does not offer.

Features

Feature Description
Bloom filter Prevents cache penetration; blocks non-existent keys
Distributed lock Redisson-based; prevents cache breakdown (requires Redisson on classpath)
TTL jitter Randomizes TTL; prevents cache avalanche
Null-value caching Caches null; prevents penetration
Early expiration Async early refresh for hot keys; improves hit rate
Composable chain Handlers strung together by priority; custom handlers can be inserted (differentiator)
Safe serialization Whitelisted deserialization; defends against Jackson polymorphic-type attacks

ResiCache does not provide circuit breaking / rate limiting / multi-level local cache / Reactive support — see Not in Scope.

Architecture

ResiCache uses a responsibility chain for cache-write protection. Handler ordering is defined in a single source of truth, the HandlerOrder enum, bound via @HandlerPriority:

┌─────────────────────────────────────────────────────────────┐
│                    CacheHandlerChain                        │
├─────────────────────────────────────────────────────────────┤
│  ① BloomFilter      (100) ── Bloom filter, anti-penetration │
│  ② SyncLock         (200) ── Distributed lock, anti-breakdown│
│  ③ EarlyExpiration  (250) ── Early expiry, hot-key guard    │
│  ④ TTL              (300) ── TTL jitter, anti-avalanche     │
│  ⑤ NullValue        (400) ── Null caching, anti-penetration │
│  ⑥ ActualCache      (500) ── Actual Redis write             │
└─────────────────────────────────────────────────────────────┘

Any handler can set output.skipRemaining=true to short-circuit the rest of the chain; PostProcessHandler callbacks run after the chain completes. Third-party handlers can insert by extending the HandlerOrder enum.

Quick start

1. Add the dependency

<dependency>
    <groupId>io.github.davidhlp</groupId>
    <artifactId>ResiCache</artifactId>
    <version>0.0.2</version>
</dependency>

2. Configure Redis

spring:
  data:
    redis:
      host: localhost
      port: 6379

ResiCache activates via Spring Boot auto-configuration (entry point RedisCacheAutoConfiguration, see META-INF/spring/...AutoConfiguration.imports). No extra @EnableXxx is required.

3. Enable caching

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4. Use the annotations

Recommended: @RedisCacheable (the protection entry point)

@Service
public class UserService {
    @RedisCacheable(value = "users", key = "#id",
                    useBloomFilter = true,        // Bloom filter, anti-penetration
                    cacheNullValues = true,       // null caching
                    randomTtl = true,              // TTL jitter, anti-avalanche
                    variance = 0.2,                // jitter amplitude ±20%
                    enableEarlyExpiration = true)  // hot-key early refresh
    public User getUserById(Long id) {
        return userRepository.findById(id);
    }
}

Compatible: @Cacheable (no protection)

@Cacheable(value = "users", key = "#id")  // coexists, but gains no protection
public User getUserById(Long id) { ... }

@Cacheable coexists with ResiCache but gains no protection — the protection attributes (useBloomFilter / randomTtl / ...) live only on @RedisCacheable. Since v0.0.3, the default nativeAnnotationMode=SELECTIVE means plain @Cacheable is handled entirely by Spring's native cache infrastructure. Use @RedisCacheable for protection.

Configuration

All properties use the resi-cache.* prefix (bound to RedisProCacheProperties).

Master switches (new in v0.0.3)

resi-cache:
  enabled: true                 # master kill-switch; false disables ResiCache entirely
  protection:
    enabled: true               # false skips bloom/lock/early-exp/null-value; TTL preserved (startup-only)

Global

resi-cache:
  default-ttl: 30m
  key-prefix: ""
  transaction-aware: false
  fail-on-spel-error: true

Bloom filter

resi-cache:
  bloom-filter:
    enabled: true
    expected-insertions: 100000
    false-probability: 0.01
    hash-cache-size: 10000
    rebuild-window-seconds: 30   # post-CLEAR rebuild window (s); 0 = disabled (v0.0.x behavior)

Distributed lock

resi-cache:
  sync-lock:
    timeout: 3000
    unit: MILLISECONDS
    prefix: "cache:lock:"
    local-only: false   # true = accept single-JVM sync when Redisson absent (else fail-fast)

Early expiration (hot-key)

resi-cache:
  early-expiration:
    enabled: true
    pool-size: 2
    max-pool-size: 10
    queue-capacity: 100

Serialization safety

resi-cache:
  serializer:
    type-property: "@class"
    polymorphic-typing-enabled: false   # off by default, safer
    fail-on-unknown-type: true
    allowed-package-prefixes:           # deserialization whitelist
      - "io.github.davidhlp."
      - "com.example."                   # ← you MUST add your own business packages

⚠️ The whitelist defaults to only io.github.davidhlp.. When caching custom business types (e.g. com.example.User), you must add your package to allowed-package-prefixes, otherwise deserialization throws.

Wildcard form (added in v0.0.3): any prefix ending in .* is a wildcard sentinel — it matches the class directly (com.example.Foo), all sub-package classes (com.example.sub.Bar, com.example.foo.bar.baz.Qux, …), and is dot-boundary protected (so com.example.* does not match com.exampleX.Foo). Use it when you want to allow a whole package subtree without listing each sub-package.

resi-cache:
  serializer:
    allowed-package-prefixes:
      - "io.github.davidhlp."
      - "com.example.*"        # entire com.example subtree in one entry

Annotation attributes (@RedisCacheable)

Attribute Default Description
ttl 60 Cache TTL (seconds)
cacheNullValues false Cache null
useBloomFilter false Enable Bloom filter
expectedInsertions 10000 Bloom expected insertions
falseProbability 0.03 Bloom false-positive rate
randomTtl false Enable TTL jitter
variance 0.2 TTL jitter amplitude
enableEarlyExpiration false Enable early expiry
earlyExpirationThreshold 0.3 Early-expiry threshold (remaining TTL ratio)
sync / syncTimeout false / 10 Sync wait & timeout

The five protection attributes default to false — enable each explicitly on @RedisCacheable. sync=true (anti-breakdown) requires Redisson on the classpath; without it, ResiCache fails fast (refuses to silently degrade to a single-JVM lock, which is useless across instances). For an explicit single-instance/test degradation, set resi-cache.sync-lock.local-only=true.

How it works

Cache penetration — the Bloom filter intercepts requests for non-existent keys before the cache layer. Cache breakdown — a distributed lock ensures only one request loads the data. Cache avalanche — TTL randomization (TTL = baseTtl ± variance × baseTtl when randomTtl=true) avoids mass simultaneous expiry.

Comparison

ResiCache is one of four common options for caching on top of Redis: JetCache, Caffeine, raw Redisson, and ResiCache. The detailed feature matrix, honest trade-offs, and "when to pick which" guidance live in docs/comparison.md — including the line the project ships under: "ResiCache for Redisson — the declarative cache protection chain Redisson forgot to ship".

Headline takeaway: the 3 protections JetCache is missing, in one Redisson-native chain — bloom-filter (penetration), TTL jitter (avalanche), and distributed breakdown lock (breakdown). ResiCache is the Redisson companion that closes those gaps; JetCache is the multi-level / broadcast invalidator. The two are complementary in scope, not direct substitutes.

Known Limitations

  • Protection off by default: the five protection attributes default false; enable each explicitly on @RedisCacheable.
  • Serialization envelope incompatible with Spring native: ResiCache uses a {version, payload} envelope, incompatible with Spring's GenericJackson2JsonRedisSerializer / JdkSerializerexisting projects must migrate, otherwise the entire cache misses on cutover.
  • Serialization whitelist defaults to the author's package: allowed-package-prefixes defaults to io.github.davidhlp.; custom types must be added explicitly (see Serialization safety).
  • nativeAnnotationMode defaults to SELECTIVE: plain @Cacheable is handled entirely by Spring's native cache infrastructure, removing the dual-advisor risk. Use @RedisCacheable for protection.
  • No Reactive support (WebFlux / Mono / Flux): RedisCacheInterceptor is blocking; such methods log an explicit "caching will not take effect" warning.
  • @CacheEvict(allEntries=true) (CLEAN) is best-effort, not atomic — parity with Spring's native RedisCache.clear / DefaultRedisCacheWriter.clean: it uses a SCAN cursor + batched UNLINK/DEL, so keys written mid-CLEAN may be stranded and the cache is briefly half-deleted on large key sets. Lua/MULTI atomicity is intentionally not used (Redis single-thread O(keyspace) block, Cluster cross-slot). When the Bloom filter is enabled, rebuild-window-seconds prevents silent nulls during the post-wipe rebuild.

Not in Scope

ResiCache deliberately omits these to avoid bloat — pair with dedicated tools:

  • Circuit breaking / rate limitingResilience4j
  • Multi-level local + remote cacheCaffeine for the local tier
  • Reactive caching (WebFlux) → not supported

Dependencies

Dependency Version
Spring Boot 4.0.0 (parent)
Java 21+
Redisson 3.50.0 (optional)
Caffeine 3.1.8
Testcontainers 1.20.4 (CI Docker compatibility override)

Full matrix: COMPATIBILITY.md.

Project status & maintenance

  • Version: v0.0.2 — Semantic Versioning < 1.0; APIs may change in minor releases; breaking items are marked ⚠️ in CHANGELOG.md.
  • Maintenance: solo-maintained (DavidHLP), Non-SLA best-effort — no guaranteed response time, but issues are actively addressed.
  • Contributing: PRs welcome — see CONTRIBUTING.md.
  • Security: report privately — see SECURITY.md.
  • Compatibility: see COMPATIBILITY.md.

License

MIT License © 2026 DavidHLP

About

ResiCache 是一个 Spring Boot 4 的缓存扩展,为基于 Redis 的缓存提供穿透/击穿/雪崩/热 key 防护。它用一条可插拔的责任链增强 Spring Cache,支持 @RedisCacheable、@RedisCachePut、@RedisCacheEvict、@RedisCaching 注解。

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages