From 29247958ae2e5080dc64c52e6529c819e7bb82ad Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Fri, 18 Sep 2026 13:02:39 +0800 Subject: [PATCH 1/3] docs(readme): restructure bilingual project guides --- README.md | 665 ++++++++++++++++++++++++++++++------------------ README.zh-CN.md | 564 +++++++++++++++++++++++++--------------- 2 files changed, 774 insertions(+), 455 deletions(-) diff --git a/README.md b/README.md index 2f400ade..a842fce4 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,121 @@ # 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. +**Protection-in-depth for Spring Cache on Redis.** ResiCache adds declarative +cache-penetration, cache-breakdown, cache-avalanche, and hot-key refresh +protection through a composable responsibility chain, while keeping Spring +Cache as the application-facing model. [![CI](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml/badge.svg)](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -> **Project status: early (v0.0.2) · Non-SLA best-effort · solo-maintained.** -> Read [⚠️ Known Limitations](#known-limitations) before any production use. +[English](README.md) · [简体中文](README.zh-CN.md) + +> [!WARNING] +> ResiCache is pre-1.0 (`v0.0.2`), non-SLA, and currently maintained by one +> maintainer. The current `main` line targets Spring Boot 4.0 and Java 21, but +> it does not have a matching Maven Central artifact yet. Read +> [Compatibility](COMPATIBILITY.md) and [Limitations](#limitations) before +> adopting it in production. + +## Contents + +- [Overview](#overview) +- [Features](#features) +- [Compatibility and requirements](#compatibility-and-requirements) +- [Quick start](#quick-start) +- [How it works](#how-it-works) +- [Configuration](#configuration) +- [Extension points](#extension-points) +- [Runtime semantics](#runtime-semantics) +- [Comparison](#comparison) +- [Limitations](#limitations) +- [Not in scope](#not-in-scope) +- [Project layout](#project-layout) +- [Development](#development) +- [Project status and support](#project-status-and-support) +- [Security](#security) +- [License](#license) + +## Overview + +Spring Cache provides a consistent programming model for caching, but it does +not by itself provide protection against cache penetration, breakdown, avalanche, +or hot-key expiry. ResiCache adds those protections as explicit capabilities on +top of Spring Cache and Redis. + +ResiCache is designed to: + +- **Keep Spring Cache at the boundary**: applications still use + `@EnableCaching`, cache operations, and Spring's cache abstractions. +- **Make protection composable**: each protection mechanism is a typed handler + in one ordered chain rather than unrelated advice scattered across services. +- **Fail closed at important boundaries**: missing distributed-lock support, + unsafe serialization, and invalid configuration are observable instead of + silently becoming weaker guarantees. +- **Stay focused**: it is not a replacement for a multi-level cache, + circuit-breaker, rate-limiter, or reactive cache framework. -[简体中文](README.zh-CN.md) - -## What it is +## Features -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**. +| Capability | What it provides | +|---|---| +| Bloom filter | Rejects requests for keys that are not known to exist, reducing cache penetration | +| Distributed lock | Redisson-based coordination for cache breakdown protection | +| TTL jitter | Randomizes expiration to reduce synchronized cache avalanche | +| Null-value caching | Caches negative lookups when explicitly enabled | +| Early expiration | Refreshes hot keys before their normal expiration boundary | +| Composable handler chain | Orders protection mechanisms through `HandlerOrder` and `@HandlerPriority` | +| Safe serialization | Uses a controlled deserialization whitelist and an internal wire envelope | +| Spring Cache integration | Works with Spring Cache instead of introducing a second AOP model | -- **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. +All five protection attributes on `@RedisCacheable` default to `false`; enable +the mechanisms that match the application's risk profile. -## Features +## Compatibility and requirements -| 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 | +The repository currently ships one supported build line: -> ResiCache does **not** provide circuit breaking / rate limiting / multi-level -> local cache / Reactive support — see [Not in Scope](#not-in-scope). +| Component | Current `main` line | +|---|---:| +| Java | 21 | +| Spring Boot | 4.0.0 | +| Spring Framework / Spring Cache | 7.x, through Boot 4 | +| Spring Data Redis | 4.0.x | +| Redis Server | 7.x | +| Redisson | 3.50.0, optional | +| Caffeine | 3.1.8, bundled for internal support | +| Docker | Required for Testcontainers-backed verification | +| Testcontainers | 1.20.6, test scope | -## Architecture +See the complete matrix and known compatibility boundaries in +[COMPATIBILITY.md](COMPATIBILITY.md). -ResiCache uses a **responsibility chain** for cache-operation protection. Handler -ordering is defined in a single source of truth, the `HandlerOrder` enum, bound -via `@HandlerPriority`: +### Published artifact status -``` -┌─────────────────────────────────────────────────────────────┐ -│ 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 │ -└─────────────────────────────────────────────────────────────┘ -``` +The current Boot 4 / Java 21 line is source-first and has not been published to +Maven Central. The `io.github.davidhlp:ResiCache:0.0.2` coordinate is a +historical Boot 3 / Java 17 artifact; do not use it as a dependency for the +current `main` line. -Each handler returns a typed `HandlerResult` carrying explicit `FlowControl` -(`CONTINUE`, `SKIP_ALL`, `TERMINATE`) to govern execution without hidden state. -Handlers opting into post-processing (e.g. Bloom filter async backfilling) -override `requiresPostProcess` and `afterChainExecution`. -Built-in handlers are registered by the library's auto-configuration, which -scans only the library-internal `cache` runtime package. A host application's -custom handler must be a host-scanned `@Component` or an application `@Bean`, -implement `CacheHandler`, and use `@HandlerPriority`; the library's internal -scan does not see host packages. +To try the current line, [build it from source](#development) or use a release +that explicitly declares its supported compatibility line. ## Quick start -### 1. Add the dependency +### 1. Build and install the current source line locally -```xml - - io.github.davidhlp - ResiCache - 0.0.2 - +```bash +git clone https://github.com/davidhlp/ResiCache.git +cd ResiCache +./mvnw -Punit test -B +./mvnw install -DskipTests -B ``` -> The `0.0.2` artifact on Maven Central is the **earlier Boot 3 / Java 17 -> line** (verified 2026-09-05). The current Boot 4 / Java 21 line has no -> published artifact yet; the coordinate above is the planned one. +The first command is the no-Docker contributor check. The second installs the +current source checkout into the local Maven repository for a local consumer; +it does not publish an artifact. Do not confuse that local build with the +historical `0.0.2` artifact on Maven Central. The full Redis and Redis Cluster +verification command is documented in [Development](#development). ### 2. Configure Redis @@ -98,11 +127,11 @@ spring: port: 6379 ``` -> ResiCache activates via Spring Boot auto-configuration (entry point -> `RedisCacheAutoConfiguration`, see `META-INF/spring/...AutoConfiguration.imports`). -> No extra `@EnableXxx` is required. +ResiCache is discovered through Spring Boot auto-configuration via +`RedisCacheAutoConfiguration`. It does not add `@EnableCaching` for the +application; the application remains responsible for enabling Spring Cache. -### 3. Enable caching +### 3. Enable Spring Cache ```java @SpringBootApplication @@ -114,65 +143,117 @@ public class Application { } ``` -### 4. Use the annotations - -**Recommended: `@RedisCacheable` (the protection entry point)** +### 4. Use the protected annotation ```java @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 + + @RedisCacheable( + value = "users", + key = "#id", + cacheNullValues = true, + randomTtl = true, + variance = 0.2, + enableEarlyExpiration = true) public User getUserById(Long id) { return userRepository.findById(id); } } ``` -**Compatible: `@Cacheable` (no protection)** +The protection attributes are deliberately explicit. A plain `@Cacheable` +remains compatible with Spring Cache, but it does not gain ResiCache protection +in the default `nativeAnnotationMode=SELECTIVE` mode: ```java -@Cacheable(value = "users", key = "#id") // coexists, but gains no protection -public User getUserById(Long id) { ... } +@Cacheable(value = "users", key = "#id") +public User getUserById(Long id) { + // Spring's native cache path; use @RedisCacheable for protection. +} +``` + +## How it works + +### Handler chain + +Protection is assembled as a responsibility chain. The order is defined by the +`HandlerOrder` enum and attached to handlers through `@HandlerPriority`: + +```text +┌─────────────────────────────────────────────────────────────┐ +│ 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 │ +└─────────────────────────────────────────────────────────────┘ ``` -> `@Cacheable` coexists with ResiCache but **gains no protection** — the -> protection attributes (`useBloomFilter` / `randomTtl` / ...) live only on -> `@RedisCacheable`. In the current unreleased contract, the default -> `nativeAnnotationMode=SELECTIVE` means plain `@Cacheable` is handled -> entirely by Spring's native cache infrastructure. Use `@RedisCacheable` for -> protection. +Each handler returns a typed `HandlerResult` with explicit `FlowControl` +decisions (`CONTINUE`, `SKIP_ALL`, or `TERMINATE`). A handler can opt into +post-processing through `requiresPostProcess` and +`afterChainExecution`; the chain engine owns progression and keeps the main +path separate from isolated post-processing failures. + +Built-in handlers are registered by the library's auto-configuration, which +scans the library-internal runtime package. A host application's custom handler +must be discovered by the host application as an `@Component` or `@Bean`; the +library's internal scan does not scan host packages. + +### Protection boundaries + +- **Cache penetration**: the Bloom filter treats a missing membership bit as a + definite miss. The default implementation is populated after successful cache + writes; it does not scan or rebuild from the data source. Before enabling it + for a cache whose keys are not already in the filter, seed or maintain the + public `BloomIFilter` seam. +- **Cache breakdown**: the sync handler coordinates concurrent loads through a + distributed lock when Redisson or another `LockManager` is available. +- **Cache avalanche**: TTL jitter spreads expiration times when `randomTtl=true`. +- **Hot-key expiry**: early expiration schedules refresh work before the normal + expiry boundary when explicitly enabled. +- **Negative lookups**: null-value caching can retain an absent result when the + annotation or configuration enables it. ## Configuration -Most properties use the `resi-cache.*` prefix and bind to -`RedisProCacheProperties`; the four `resi-cache.bloom.*` implementation keys -are explicitly bound by auto-configuration and described in additional -configuration metadata. +Most settings use the `resi-cache.*` prefix and bind to +`RedisProCacheProperties`. Configuration is resolved during startup; changing +protection switches requires an application restart. -### Master switches (current unreleased contract) +### Enablement and protection switches ```yaml resi-cache: - enabled: true # master kill-switch; false disables ResiCache entirely + enabled: true + native-annotation-mode: SELECTIVE # FULL | NONE | SELECTIVE protection: - enabled: true # false skips bloom/lock/early-exp/null-value; TTL preserved - bloom-filter-enabled: null # per-mechanism overrides, resolved once at startup: - sync-lock-enabled: null # null inherits the total switch; false disables that - early-expiration-enabled: null # mechanism only; true cannot re-enable a mechanism - null-value-enabled: null # when the total switch is false. Restart to apply. + enabled: true + bloom-filter-enabled: null # null inherits protection.enabled + sync-lock-enabled: null + early-expiration-enabled: null + null-value-enabled: null ``` -Protection toggles are **startup-only**: the handler chain is built once and -cached; per-mechanism `true` cannot override a `false` total switch, and -changing protection configuration requires an application restart. TTL and -the actual-cache handler always remain. +`resi-cache.enabled=false` disables the library auto-configuration. The +`protection.enabled` switch disables Bloom, sync-lock, early-expiration, and +null-value protection while keeping the base TTL and actual-cache handlers. +Per-mechanism `true` cannot re-enable a mechanism when the total protection +switch is `false`. + +`native-annotation-mode` controls Spring's native cache annotations: -### Global +- `SELECTIVE` (default): only converts native annotations when a ResiCache + annotation is also present, avoiding a dual-advisor path. +- `FULL`: converts all supported Spring cache annotations. +- `NONE`: ignores native Spring cache annotations in the ResiCache operation + source. + +### Global settings ```yaml resi-cache: @@ -183,9 +264,17 @@ resi-cache: ### Metrics and health -Cache metrics require `resi-cache.metrics.enabled=true` (default OFF) and a -`MeterRegistry`. The health indicator uses the same explicit property and -requires Actuator. +Metrics are opt-in and require both the property and an available +`MeterRegistry`: + +```yaml +resi-cache: + metrics: + enabled: true +``` + +The Redis cache health indicator uses the same explicit property and requires +Spring Boot Actuator. ### Bloom filter @@ -197,6 +286,7 @@ resi-cache: hash-functions: 3 hash-cache-size: 10000 ``` +These settings are bound under the `resi-cache.bloom.*` prefix. ### Distributed lock @@ -206,172 +296,255 @@ resi-cache: timeout: 3000 unit: MILLISECONDS prefix: "cache:lock:" - local-only: false # true = accept single-JVM sync when Redisson absent (else fail-fast) + local-only: false ``` -### Early expiration (hot-key) +`sync=true` requires a distributed lock implementation such as Redisson. With +`local-only=false` (the default), missing distributed support fails closed +instead of silently claiming multi-instance protection. Set `local-only=true` +only when single-JVM degradation is an explicit, acceptable choice. + +### Early expiration ```yaml resi-cache: protection: - early-expiration-enabled: true # optional mechanism override + early-expiration-enabled: true early-expiration: pool-size: 2 max-pool-size: 10 queue-capacity: 100 ``` +### Redis deployment + +```yaml +resi-cache: + redis: + mode: single # single | cluster | sentinel + host: localhost + port: 6379 + database: 0 + tls-enabled: false + # cluster-nodes: [host1:6379, host2:6379] + # sentinel-master: mymaster + # sentinel-nodes: [host1:26379] +``` + +The deployment validator checks mode-specific fields during configuration +binding. Credentials, when needed, belong in the application's secret +management system rather than in a committed README snippet. + ### Serialization safety ```yaml resi-cache: serializer: type-property: "@class" - polymorphic-typing-enabled: false # off by default, safer + polymorphic-typing-enabled: false fail-on-unknown-type: true - allowed-package-prefixes: # deserialization whitelist - - "io.github.davidhlp." - - "com.example." # ← you MUST add your own business packages + allowed-package-prefixes: + - "io.github.davidhlp" + - "com.example.*" ``` -> ⚠️ The whitelist defaults to **only** `io.github.davidhlp` (a literal prefix -> using the existing `startsWith` semantics). To enforce a package boundary, -> configure `io.github.davidhlp.*`; custom business types (e.g. `com.example.User`) -> still require an explicit entry in `allowed-package-prefixes`, otherwise -> deserialization throws. -> -> **Wildcard form (current unreleased):** 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. -> -> ```yaml -> 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` | 100000 | Bloom expected insertions | -| `falseProbability` | 0.01 | 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`. +The default whitelist is the literal prefix `io.github.davidhlp`, so +applications should add their own packages explicitly. For dot-boundary subtree +matching, use a prefix ending in `.*`; for example, `com.example.*` matches +`com.example.User` and `com.example.orders.Order`, but not `com.exampleX.User`. -## How it works +### Per-cache overrides + +```yaml +resi-cache: + caches: + users: + ttl: 10m + cache-null-values: true + key-prefix: "users:" +``` + +### `@RedisCacheable` attributes + +| Attribute | Default | Purpose | +|---|---:|---| +| `ttl` | `60` | Cache TTL in seconds | +| `cacheNullValues` | `false` | Cache `null` results | +| `useBloomFilter` | `false` | Enable Bloom-filter protection | +| `expectedInsertions` | `100000` | Expected Bloom-filter insertions | +| `falseProbability` | `0.01` | Bloom false-positive target | +| `randomTtl` | `false` | Enable TTL jitter | +| `variance` | `0.2` | TTL jitter amplitude | +| `enableEarlyExpiration` | `false` | Enable hot-key early refresh | +| `earlyExpirationThreshold` | `0.3` | Remaining-TTL ratio that triggers refresh | +| `sync` / `syncTimeout` | `false` / `10` | Enable synchronized loading and its wait timeout | + +## Extension points + +### Annotation family + +The public annotation family mirrors Spring Cache operations: + +- `@RedisCacheable` for read-through caching and protection attributes. +- `@RedisCachePut` for explicit cache writes. +- `@RedisCacheEvict` for cache removal. +- `@RedisCaching` for grouping multiple ResiCache operations on one method or + type. + +### Custom handlers + +A custom handler implements the public `CacheHandler` contract and uses +`@HandlerPriority` with a `HandlerOrder` value. It must be registered in the +host application's component scan or supplied as an application bean. The +handler can return a typed `HandlerResult` and can opt into the post-processing +hooks without managing a linked-list successor itself. + +Only the documented public seams are intended for replacement. See +[STABILITY.md](STABILITY.md) before depending on a public type or changing +handler behavior. -**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. +## Runtime semantics + +### Cache I/O failure behavior + +| Operation | Behavior | +|---|---| +| GET | Degrades to a cache miss and records the internal failure | +| PUT / PUT_IF_ABSENT / CLEAN | Throws a typed runtime failure retaining the original cause | +| REMOVE | Observable best-effort removal; does not throw for the removal failure | +| `get(key, loader)` with a successful loader | Returns the loader value even if write-back fails; write-back failure is logged without the raw key | +| `get(key, loader)` with a failed loader | Surfaces Spring's `Cache.ValueRetrievalException` | + +`@CacheEvict(allEntries=true)` / CLEAN is best-effort and non-atomic, using a +SCAN cursor and batched deletion. Bloom membership bits are a separately +maintained set, populated by successful writes by default; they are not +automatically rebuilt from the data source. Clearing cache entries does not +remove those bits. + +### Serialization migration + +ResiCache stores values in an internal `{version, payload}` envelope. It is not +wire-compatible with Spring's `GenericJackson2JsonRedisSerializer` or +`JdkSerializer`. Existing applications should use a bounded +shadow-read → dual-write → cutover migration instead of assuming an in-place +serializer swap is safe. See the migration guidance in +[COMPATIBILITY.md](COMPATIBILITY.md). ## Comparison -ResiCache is one of four common options for caching on top of Redis: JetCache, -Caffeine, raw Redisson, and ResiCache. The project ships under one line: -**"ResiCache for Redisson — the declarative cache protection chain Redisson -forgot to ship"**. +ResiCache is intentionally narrower than general-purpose caching frameworks: | Capability | JetCache | Caffeine | Raw Redisson | **ResiCache** | -|------------|:--------:|:--------:|:------------:|:-------------:| -| Multi-level local + remote cache | ✅ | local only | — | — | -| Bloom filter (anti-penetration) | — | — | manual | ✅ | -| TTL jitter (anti-avalanche) | — | — | manual | ✅ | -| Distributed breakdown lock | — | — | manual | ✅ | -| Null-value caching | — | — | manual | ✅ | -| Hot-key early refresh | — | — | manual | ✅ | -| Declarative `@Annotation` chain | partial | — | — | ✅ | -| Broadcast invalidation | ✅ | — | — | — | - -**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` / `JdkSerializer` — **existing 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](#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. -- **Cache I/O failure semantics**: GET degrades to a miss and logs the - failure; PUT, PUT_IF_ABSENT, and CLEAN throw a typed runtime failure - retaining the original cause; REMOVE is observable best-effort and does not - throw. **Read-through (`get(key, loader)`) is availability-first**: the - loader's successful value is always returned — a cache write-back failure - after a successful load is logged (redacted) and never overrides the value; - loader failures still surface as Spring `Cache.ValueRetrievalException`. -- **`@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). Bloom is a data-source existence hint: CLEAN removes - cache entries but preserves existing Bloom bits, so it can only introduce - false-positives and never block a valid loader with a false-negative. - -## Not in Scope - -ResiCache deliberately omits these to avoid bloat — pair with dedicated tools: - -- **Circuit breaking / rate limiting** → [Resilience4j](https://resilience4j.readthedocs.io/) -- **Multi-level local + remote cache** → [Caffeine](https://github.com/ben-manes/caffeine) 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.6 | - -Full matrix: [COMPATIBILITY.md](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](CHANGELOG.md). -- **Maintenance**: solo-maintained ([DavidHLP](https://github.com/davidhlp)), - **Non-SLA best-effort** — no guaranteed response time, but issues are actively - addressed. -- **Contributing**: PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). -- **Performance Benchmarks**: JMH baseline results & SLOs — see [PERFORMANCE.md](PERFORMANCE.md). -- **API Stability Contract**: 0.x vs 1.0 stability guarantees — see [STABILITY.md](STABILITY.md). -- **Architecture Decisions**: accepted decisions and rationale — see - [ADR index](docs/adr/README.md). -- **Compatibility Matrix**: supported Spring Boot / Java / Redisson lines — see [COMPATIBILITY.md](COMPATIBILITY.md). -- **Security**: report privately — see [SECURITY.md](SECURITY.md). +|---|:---:|:---:|:---:|:---:| +| Multi-level local + remote cache | Yes | Local only | — | — | +| Bloom filter | — | — | Manual | Yes | +| TTL jitter | — | — | Manual | Yes | +| Distributed breakdown lock | — | — | Manual | Yes | +| Null-value caching | — | — | Manual | Yes | +| Hot-key early refresh | — | — | Manual | Yes | +| Declarative protection chain | Partial | — | — | Yes | +| Broadcast invalidation | Yes | — | — | — | + +JetCache is a multi-level and broadcast-invalidation option. ResiCache is a +Redisson-oriented protection chain. They solve different parts of the caching +problem and are not presented as drop-in substitutes. + +## Limitations + +- **Pre-1.0 API**: minor releases may still change public contracts; review + [STABILITY.md](STABILITY.md) before pinning an extension point. +- **Protection is opt-in**: the five `@RedisCacheable` protection attributes + default to `false`. +- **Bloom membership is write-populated by default**: it does not scan existing + data, and an absent bit short-circuits the loader. Seed or maintain the public + `BloomIFilter` seam before enabling `useBloomFilter` for existing keys. +- **Current artifact gap**: the current Boot 4 / Java 21 line is not yet + published to Maven Central; `0.0.2` is the historical Boot 3 / Java 17 line. +- **Serializer migration required**: existing Spring-native serialized values + are not automatically compatible with ResiCache's envelope. +- **Reactive caching is unsupported**: WebFlux `Mono` and `Flux` methods do not + use the blocking ResiCache interceptor. +- **Async cached methods**: `@Async` methods are not supported for sync-lock and + Bloom-filter enhancements. +- **Protection switches are startup-only**: changing `resi-cache.protection.*` + requires an application restart. +- **Time-to-idle reads**: the native writer path intentionally does not refresh + TTL on read, avoiding write amplification. + +For the complete, tested boundary list, use +[COMPATIBILITY.md](COMPATIBILITY.md). + +## Not in scope + +ResiCache deliberately does not implement capabilities that are better owned by +specialized components: + +- **Circuit breaking and rate limiting** → [Resilience4j](https://resilience4j.readthedocs.io/) +- **Multi-level local plus remote caching** → [Caffeine](https://github.com/ben-manes/caffeine) for the local tier +- **Reactive caching** → not supported by the current blocking interceptor + +## Project layout + +```text +ResiCache/ +├── src/main/java/io/github/davidhlp/spring/cache/redis/ +│ ├── annotation/ # Public ResiCache annotations +│ ├── cache/ # Internal runtime, AOP, chain, operations, assembly +│ ├── chain/ # Stable handler, operation, and result contracts +│ ├── config/ # Auto-configuration and RedisProCacheProperties +│ ├── protection/ # Stable BloomIFilter and LockManager seams +│ └── serialization/ # Serializer and migration contracts +├── src/test/java/ # Unit, contract, and integration tests +├── resicache-bench/ # Standalone JMH benchmark module +├── scripts/ci/ # CI and repository guards +└── docs/adr/ # Accepted architecture decisions +``` + +## Development + +### Prerequisites + +- JDK 21 +- Maven 3.x, or the bundled Maven Wrapper +- Docker for Testcontainers-backed Redis and Cluster verification + +### Verification commands + +```bash +# No-Docker daily path +./mvnw -Punit test -B + +# Full Redis/Testcontainers verification +./mvnw clean verify -B + +# Separate style and naming gates +./mvnw checkstyle:check -B +bash scripts/ci/check-test-names.sh + +# Package without tests +./mvnw clean package -DskipTests -B +``` + +`./mvnw clean verify -B` enforces the JaCoCo gate of 70% line coverage and 40% +branch coverage. The full development and contribution workflow is documented +in [CONTRIBUTING.md](CONTRIBUTING.md). + +## Project status and support + +- **Version**: `v0.0.2`; semantic-versioning guarantees are intentionally + limited before 1.0. +- **Maintenance**: solo-maintained, non-SLA, best-effort support. +- **Change history**: [CHANGELOG.md](CHANGELOG.md) +- **Compatibility policy**: [COMPATIBILITY.md](COMPATIBILITY.md) +- **API stability**: [STABILITY.md](STABILITY.md) +- **Performance baseline**: [PERFORMANCE.md](PERFORMANCE.md) +- **Architecture decisions**: [ADR index](docs/adr/README.md) +- **Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md) + +## Security + +Do not open public issues for suspected vulnerabilities. Follow the private +reporting process in [SECURITY.md](SECURITY.md). ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 82b2418b..5b5a4b45 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,68 +1,114 @@ # ResiCache -> ⚠️ **本中文版可能滞后,请以 [英文 README](README.md) 为准(canonical / source of truth)。** - -**Spring Cache 的防护增强注解生态** —— 在 `@Cacheable` 之外,用 `@RedisCacheable` 一行注解为 Redis 缓存补齐防穿透 / 防击穿 / 防雪崩 / 热 key 早刷新能力,通过可编排的责任链注入防护,不重造 AOP。 +**面向 Redis 的 Spring Cache 防护增强。** ResiCache 通过可编排的责任链, +为 Spring Cache 增加缓存穿透、缓存击穿、缓存雪崩和热点 key 提前刷新防护, +同时保留 Spring Cache 作为应用侧的使用模型。 [![CI](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml/badge.svg)](https://github.com/davidhlp/ResiCache/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -> **项目状态:早期(v0.0.2)· Non-SLA best-effort · 单人维护** -> 生产采用前请务必先读 [⚠️ Known Limitations](README.md#known-limitations)。 - -## 这是什么 - -Spring Cache(`@Cacheable` / `@CachePut` / `@CacheEvict`)只解决"缓存",不解决"防护"——缓存穿透、击穿、雪崩、热 key 过期都要业务自己补。ResiCache 用一套 **`@RedisCacheable` 增强注解** + **可编排的责任链**,把这些防护变成声明式能力: - -- **与 Spring Cache 共存**:继承 `RedisCacheManager` / `CacheInterceptor`,不替换 `@EnableCaching`,不重造 AOP -- **与 JetCache 的差异**:JetCache 主打**多级缓存**,ResiCache 主打**缓存防护纵深**——责任链上每个 handler 可插拔、可编排,这是 JetCache 做不到的 - -## 📋 功能特性 - -| 特性 | 说明 | -|------|------| -| **布隆过滤器** | 防缓存穿透,拦截不存在的 key | -| **分布式锁** | 基于 Redisson,防缓存击穿(**需 Redisson 在 classpath**) | -| **TTL 抖动** | 随机化 TTL,防缓存雪崩 | -| **空值缓存** | 缓存 null,防穿透 | -| **提前过期** | 异步提前刷新热 key,提升命中率 | -| **可编排责任链** | handler 按优先级串接,支持自定义插队(差异化能力) | -| **安全序列化** | 白名单反序列化,防 Jackson 多态类型攻击 | - -> ResiCache **不提供** 熔断 / 限流 / 多级本地缓存 / Reactive 支持,见 [🚫 Not in Scope](#-not-in-scope)。 +[English](README.md) · [简体中文](README.zh-CN.md) + +> [!WARNING] +> ResiCache 仍处于 1.0 之前(`v0.0.2`),不提供 SLA,目前由单人维护。 +> 当前 `main` 构建线目标为 Spring Boot 4.0 与 Java 21,但尚未发布匹配的 +> Maven Central 产物。正式采用前请先阅读[兼容性矩阵](COMPATIBILITY.md)和 +> [已知限制](#已知限制)。 + +## 目录 + +- [概览](#概览) +- [功能特性](#功能特性) +- [兼容性与要求](#兼容性与要求) +- [快速开始](#快速开始) +- [工作原理](#工作原理) +- [配置](#配置) +- [扩展点](#扩展点) +- [运行时语义](#运行时语义) +- [方案对比](#方案对比) +- [已知限制](#已知限制) +- [不在范围内](#不在范围内) +- [项目结构](#项目结构) +- [开发与验证](#开发与验证) +- [项目状态与支持](#项目状态与支持) +- [安全](#安全) +- [许可证](#许可证) + +## 概览 + +Spring Cache 提供了统一的缓存编程模型,但本身不负责缓存穿透、缓存击穿、 +缓存雪崩或热点 key 过期防护。ResiCache 在 Spring Cache 与 Redis 之上, +将这些防护能力显式化、可组合化。 + +ResiCache 的设计目标是: + +- **保持 Spring Cache 作为边界**:应用继续使用 `@EnableCaching`、缓存操作 + 和 Spring 的缓存抽象。 +- **让防护可组合**:每种机制都是一条有明确顺序的类型化 Handler,而不是 + 分散在业务服务中的独立 AOP 逻辑。 +- **关键边界默认失败关闭**:缺少分布式锁支持、不安全的序列化和非法配置都 + 应当可观测,而不是静默地降低保证等级。 +- **保持职责聚焦**:ResiCache 不是多级缓存、熔断器、限流器或 Reactive 缓存 + 框架的替代品。 + +## 功能特性 + +| 能力 | 说明 | +|---|---| +| 布隆过滤器 | 拦截已知不存在的 key,降低缓存穿透 | +| 分布式锁 | 基于 Redisson,为缓存击穿提供并发加载协调 | +| TTL 抖动 | 随机化过期时间,减少缓存雪崩 | +| 空值缓存 | 显式开启后缓存负查询结果 | +| 提前过期 | 在正常过期边界前刷新热点 key | +| 可编排责任链 | 通过 `HandlerOrder` 与 `@HandlerPriority` 统一排列防护机制 | +| 安全序列化 | 使用受控的反序列化白名单和内部 wire envelope | +| Spring Cache 集成 | 复用 Spring Cache,而不是引入第二套 AOP 模型 | + +`@RedisCacheable` 上的五类防护属性默认全部为 `false`;请根据应用的风险 +模型显式开启需要的机制。 + +## 兼容性与要求 + +当前仓库只维护一条构建线: + +| 组件 | 当前 `main` 构建线 | +|---|---:| +| Java | 21 | +| Spring Boot | 4.0.0 | +| Spring Framework / Spring Cache | 7.x,由 Boot 提供 | +| Spring Data Redis | 4.0.x | +| Redis Server | 7.x | +| Redisson | 3.50.0,可选 | +| Caffeine | 3.1.8,供内部能力使用 | +| Docker | Testcontainers 验证必需 | +| Testcontainers | 1.20.6,仅用于测试 | -## 🏗️ 架构设计 +完整版本矩阵和边界说明见 [COMPATIBILITY.md](COMPATIBILITY.md)。 -ResiCache 采用 **责任链模式** 实现缓存写入防护。处理器顺序由 `HandlerOrder` 枚举统一定义,通过 `@HandlerPriority` 绑定: +### 已发布产物状态 -``` -┌─────────────────────────────────────────────────────────────┐ -│ CacheHandlerChain │ -├─────────────────────────────────────────────────────────────┤ -│ ① BloomFilter (100) ── 布隆过滤器,防缓存穿透 │ -│ ② SyncLock (200) ── 分布式锁,防缓存击穿 │ -│ ③ EarlyExpiration (250) ── 提前过期,热 key 保护 │ -│ ④ TTL (300) ── TTL 抖动,防缓存雪崩 │ -│ ⑤ NullValue (400) ── 空值缓存,防穿透 │ -│ ⑥ ActualCache (500) ── 实际 Redis 写入 │ -└─────────────────────────────────────────────────────────────┘ -``` +当前 Boot 4 / Java 21 构建线以源码为主,尚未发布到 Maven Central。 +`io.github.davidhlp:ResiCache:0.0.2` 是历史上的 Boot 3 / Java 17 产物, +不要把它当作当前 `main` 构建线的依赖。 -每个 Handler 通过返回包含明确控制决策(`FlowControl.CONTINUE`、`SKIP_ALL`、`TERMINATE`)的 `HandlerResult` 来控制责任链调度;需要后置回填或异步通知的 Handler 重写 `requiresPostProcess` 与 `afterChainExecution` 钩子。自定义 Handler 只需实现 `CacheHandler` 接口并标注 `@HandlerPriority`。 +如需试用当前构建线,请[从源码构建](#开发与验证),或使用明确声明兼容性 +构建线的正式 release。 -## 🚀 快速开始 +## 快速开始 -### 1. 添加依赖 +### 1. 在本地构建并安装当前源码 -```xml - - io.github.davidhlp - ResiCache - 0.0.2 - +```bash +git clone https://github.com/davidhlp/ResiCache.git +cd ResiCache +./mvnw -Punit test -B +./mvnw install -DskipTests -B ``` -> Maven Central 上的 `0.0.2` 是**早期 Boot 3 / Java 17 构建**(2026-09-05 已核实:Central 全部版本均属旧线)。当前 Boot 4 / Java 21 构建线尚未发布产物,上述坐标为规划坐标。 +第一条命令是不依赖 Docker 的贡献者检查;第二条命令会把当前源码按 POM +版本安装到本地 Maven 仓库,供本地消费者试用,不会发布产物。不要把这个本地 +构建与 Maven Central 上历史版本的 `0.0.2` 混淆。完整的 Redis 与 Redis +Cluster 验证命令见[开发与验证](#开发与验证)。 ### 2. 配置 Redis @@ -74,9 +120,10 @@ spring: port: 6379 ``` -> ResiCache 通过 Spring Boot 自动装配生效(入口 `RedisCacheAutoConfiguration`,见 `META-INF/spring/...AutoConfiguration.imports`),无需额外 `@EnableXxx`。 +ResiCache 通过 Spring Boot 自动配置入口 `RedisCacheAutoConfiguration` 被发现。 +它不会替应用添加 `@EnableCaching`;是否启用 Spring Cache 仍由应用负责。 -### 3. 启用缓存 +### 3. 启用 Spring Cache ```java @SpringBootApplication @@ -88,67 +135,125 @@ public class Application { } ``` -### 4. 使用注解 - -**推荐:`@RedisCacheable`(防护入口)** +### 4. 使用防护注解 ```java @Service public class UserService { - @RedisCacheable(value = "users", key = "#id", - useBloomFilter = true, // 布隆过滤防穿透 - cacheNullValues = true, // 空值缓存 - randomTtl = true, // TTL 抖动防雪崩 - variance = 0.2, // 抖动幅度 ±20% - enableEarlyExpiration = true) // 热 key 提前刷新 + + @RedisCacheable( + value = "users", + key = "#id", + cacheNullValues = true, + randomTtl = true, + variance = 0.2, + enableEarlyExpiration = true) public User getUserById(Long id) { return userRepository.findById(id); } } ``` -**兼容:`@Cacheable`(不增强防护)** +防护属性刻意要求显式开启。普通 `@Cacheable` 仍可与 Spring Cache 共存, +但在默认的 `nativeAnnotationMode=SELECTIVE` 模式下不会获得 ResiCache 防护: ```java -@Cacheable(value = "users", key = "#id") // 可共存,但不获得防护 -public User getUserById(Long id) { ... } +@Cacheable(value = "users", key = "#id") +public User getUserById(Long id) { + // 走 Spring 原生缓存路径;需要防护时请使用 @RedisCacheable。 +} +``` + +## 工作原理 + +### Handler 责任链 + +防护能力以责任链方式组装。执行顺序由 `HandlerOrder` 枚举统一定义,并通过 +`@HandlerPriority` 绑定: + +```text +┌─────────────────────────────────────────────────────────────┐ +│ CacheHandlerChain │ +├─────────────────────────────────────────────────────────────┤ +│ ① BloomFilter (100) ── 布隆过滤器,防缓存穿透 │ +│ ② SyncLock (200) ── 分布式锁,防缓存击穿 │ +│ ③ EarlyExpiration (250) ── 提前过期,热点 key 保护 │ +│ ④ TTL (300) ── TTL 抖动,防缓存雪崩 │ +│ ⑤ NullValue (400) ── 空值缓存,防缓存穿透 │ +│ ⑥ ActualCache (500) ── 实际 Redis 写入 │ +└─────────────────────────────────────────────────────────────┘ ``` -> `@Cacheable` 可与 ResiCache 共存,但**不获得防护**——防护属性(`useBloomFilter` / `randomTtl` / ...)仅在 `@RedisCacheable` 上。要防护请用 `@RedisCacheable`。 +每个 Handler 返回带有明确 `FlowControl` 决策(`CONTINUE`、`SKIP_ALL` 或 +`TERMINATE`)的类型化 `HandlerResult`。Handler 可以通过 +`requiresPostProcess` 与 `afterChainExecution` 参与后置处理;链引擎负责 +推进主链,并隔离后置处理失败。 + +内置 Handler 由库的自动配置注册,只扫描库内部 runtime 包。应用自定义 +Handler 必须由应用自己的组件扫描发现,或作为应用 Bean 提供;库的内部扫描 +不会扫描宿主应用包。 + +### 防护边界 -## ⚙️ 配置选项 +- **缓存穿透**:Bloom 缺少 membership bit 时会被视为确定 miss。默认实现只在缓存 + 写入成功后回填,不会扫描或从数据源自动重建。对于过滤器中尚未存在的缓存 key, + 启用前必须通过公共 `BloomIFilter` seam 预填充或维护 membership set。 +- **缓存击穿**:当 Redisson 或其他 `LockManager` 可用时,sync Handler 通过 + 分布式锁协调并发加载。 +- **缓存雪崩**:`randomTtl=true` 时,TTL 抖动会分散过期时间。 +- **热点 key 过期**:显式开启后,提前过期机制会在正常过期边界前安排刷新。 +- **负查询**:显式开启后,空值缓存可以保留不存在数据的结果。 -多数配置绑定到 `RedisProCacheProperties`;四个 `resi-cache.bloom.*` 实现参数由自动配置显式绑定,并由 additional metadata 描述。 +## 配置 -### 总开关(当前未发布契约) +大多数配置使用 `resi-cache.*` 前缀,并绑定到 +`RedisProCacheProperties`。配置在启动阶段解析;修改防护开关需要重启应用。 + +### 启用与防护开关 ```yaml resi-cache: - enabled: true # 主开关;false 完全禁用 ResiCache + enabled: true + native-annotation-mode: SELECTIVE # FULL | NONE | SELECTIVE protection: - enabled: true # false 跳过 bloom/lock/early-exp/null-value;TTL 保留 - bloom-filter-enabled: null # 机制级覆盖,启动时静态解析: - sync-lock-enabled: null # null 继承总开关;false 只关闭该机制;总开关为 - early-expiration-enabled: null # false 时分项 true 不能重新启用。修改配置需重启。 + enabled: true + bloom-filter-enabled: null # null 继承 protection.enabled + sync-lock-enabled: null + early-expiration-enabled: null null-value-enabled: null ``` -防护开关**仅启动时生效**:责任链单例在首次构建时缓存;分项 `true` 无法覆盖总开关 -`false`;修改防护配置需重启应用。TTL 与 ActualCache 始终保留。 +`resi-cache.enabled=false` 会关闭库的自动配置。`protection.enabled` 会关闭 +布隆、同步锁、提前过期和空值防护,但保留基础 TTL 与实际缓存 Handler。 +当总防护开关为 `false` 时,机制级 `true` 不能重新开启对应机制。 + +`native-annotation-mode` 控制 Spring 原生缓存注解: + +- `SELECTIVE`(默认):仅在同时存在 ResiCache 注解时转换原生注解,避免双 + Advisor 路径。 +- `FULL`:转换所有受支持的 Spring 缓存注解。 +- `NONE`:在 ResiCache operation source 中忽略 Spring 原生缓存注解。 ### 全局配置 ```yaml resi-cache: - default-ttl: 30m # 默认 TTL - key-prefix: "" # 全局 key 前缀 - transaction-aware: false # 事务感知缓存 + default-ttl: 30m + key-prefix: "" + transaction-aware: false ``` ### 指标与健康检查 -缓存指标需要显式设置 `resi-cache.metrics.enabled=true`(默认关闭),并且应用 -提供 `MeterRegistry`。健康检查使用同一显式属性,并需要 Actuator。 +指标默认选择性关闭,需要同时显式设置属性并提供可用的 `MeterRegistry`: + +```yaml +resi-cache: + metrics: + enabled: true +``` + +Redis cache health indicator 使用同一个显式属性,并且需要 Spring Boot Actuator。 ### 布隆过滤器 @@ -160,6 +265,7 @@ resi-cache: hash-functions: 3 hash-cache-size: 10000 ``` +这些配置绑定在 `resi-cache.bloom.*` 前缀下。 ### 分布式锁 @@ -169,79 +275,62 @@ resi-cache: timeout: 3000 unit: MILLISECONDS prefix: "cache:lock:" - local-only: false # true = 无 Redisson 时显式单 JVM 降级(否则 fail-fast) + local-only: false ``` -### 提前过期(热 key 保护) +`sync=true` 需要 Redisson 等分布式锁实现。当 `local-only=false`(默认)时, +缺少分布式支持会失败关闭,而不是静默声称具备多实例防护。只有在单 JVM 降级 +明确可接受时,才应设为 `local-only=true`。 + +### 提前过期 ```yaml resi-cache: protection: - early-expiration-enabled: true # 可选的机制级覆盖(仅启动时生效;null 继承总开关) + early-expiration-enabled: true early-expiration: - pool-size: 2 # 核心线程数 - max-pool-size: 10 # 最大线程数 - queue-capacity: 100 # 队列容量 + pool-size: 2 + max-pool-size: 10 + queue-capacity: 100 ``` -### Redis 部署 +### Redis 部署方式 ```yaml resi-cache: redis: - mode: single # single | cluster | sentinel + mode: single # single | cluster | sentinel host: localhost port: 6379 database: 0 tls-enabled: false # cluster-nodes: [host1:6379, host2:6379] # sentinel-master: mymaster - # sentinel-nodes: [host1:26379, host2:26379] + # sentinel-nodes: [host1:26379] ``` +部署配置会在绑定阶段校验与模式相关的字段。生产环境凭据应交由应用的密钥 +管理系统处理,不要写入提交的 README 示例。 + ### 序列化安全 ```yaml resi-cache: serializer: - type-property: "@class" # Jackson 类型标签 - polymorphic-typing-enabled: false # 默认关闭,更安全 - fail-on-unknown-type: true # 未知类型即失败 - allowed-package-prefixes: # 反序列化白名单 - - "io.github.davidhlp." - - "com.example." # ← 务必加上你自己的业务包名 + type-property: "@class" + polymorphic-typing-enabled: false + fail-on-unknown-type: true + allowed-package-prefixes: + - "io.github.davidhlp" + - "com.example.*" ``` -> ⚠️ **白名单默认仅含 `io.github.davidhlp`**(字面前缀,沿用现有 `startsWith` 语义)。如需 dot 边界保护,请配置 `io.github.davidhlp.*`;缓存自定义业务类型(如 `com.example.User`)时,**必须**显式添加你的包名,否则反序列化会抛异常。 -> -> **通配形式(当前未发布)**:以 `.*` 结尾的前缀被解释为通配符——匹配直接类(`com.example.Foo`)、所有子包内的类(`com.example.sub.Bar`、`com.example.foo.bar.baz.Qux` …),并以 dot 边界保护(`com.example.*` **不会**误匹配 `com.exampleX.Foo`)。当你想允许整个包子树、无需逐子包列出时使用。 -> -> ```yaml -> resi-cache: -> serializer: -> allowed-package-prefixes: -> - "io.github.davidhlp." -> - "com.example.*" # 一行搞定整棵 com.example 子树 -> ``` - -### 注解级属性(`@RedisCacheable`) - -| 属性 | 默认值 | 说明 | -|------|--------|------| -| `ttl` | 60 | 缓存 TTL(秒) | -| `cacheNullValues` | false | 是否缓存 null | -| `useBloomFilter` | false | 启用布隆过滤 | -| `expectedInsertions` | 100000 | 布隆预期插入量 | -| `falseProbability` | 0.01 | 布隆误判率 | -| `randomTtl` | false | 启用 TTL 抖动 | -| `variance` | 0.2 | TTL 抖动幅度 | -| `enableEarlyExpiration` | false | 启用提前过期 | -| `earlyExpirationThreshold` | 0.3 | 提前过期阈值(TTL 剩余比例) | -| `sync` / `syncTimeout` | false / 10 | 同步等待与超时 | - -> 注:5 大防护属性**默认全 `false`**——须在 `@RedisCacheable` 上逐个显式开启。`sync=true`(防击穿)依赖 Redisson 在 classpath,**缺失时 fail-fast**(拒绝静默降级为单 JVM 锁——跨实例无效);确需单实例/测试降级,设 `resi-cache.sync-lock.local-only=true`。 - -### 每缓存覆盖(`caches.`) +默认白名单是字面前缀 `io.github.davidhlp`,应用自己的业务包必须显式加入。 +如需 dot 边界保护的子树匹配,请使用以 `.*` 结尾的前缀;例如 +`com.example.*` 会匹配 `com.example.User` 与 `com.example.orders.Order`,但不会 +匹配 `com.exampleX.User`。 + +### 按缓存覆盖配置 ```yaml resi-cache: @@ -252,115 +341,172 @@ resi-cache: key-prefix: "users:" ``` -## 📖 工作原理 +### `@RedisCacheable` 属性 -### 缓存穿透防护 +| 属性 | 默认值 | 作用 | +|---|---:|---| +| `ttl` | `60` | 缓存 TTL,单位为秒 | +| `cacheNullValues` | `false` | 是否缓存 `null` 结果 | +| `useBloomFilter` | `false` | 是否启用布隆过滤防护 | +| `expectedInsertions` | `100000` | 布隆过滤器预期插入量 | +| `falseProbability` | `0.01` | 布隆过滤器误判率目标 | +| `randomTtl` | `false` | 是否启用 TTL 抖动 | +| `variance` | `0.2` | TTL 抖动幅度 | +| `enableEarlyExpiration` | `false` | 是否启用热点 key 提前刷新 | +| `earlyExpirationThreshold` | `0.3` | 触发刷新的剩余 TTL 比例 | +| `sync` / `syncTimeout` | `false` / `10` | 是否启用同步加载及等待超时 | -布隆过滤器在缓存层之前拦截不存在的数据请求: +## 扩展点 -``` -请求 ──→ BloomFilter ──→ 存在?──→ 是 ──→ 继续执行 - │ - └──→ 否 ──→ 直接返回 null(不查缓存) -``` +### 注解族 -### 缓存击穿防护 +公共注解族与 Spring Cache 操作相对应: -分布式锁确保同一时刻只有一个请求去加载数据: +- `@RedisCacheable`:读穿缓存以及防护属性。 +- `@RedisCachePut`:显式写入缓存。 +- `@RedisCacheEvict`:移除缓存。 +- `@RedisCaching`:在一个方法或类型上组合多个 ResiCache 操作。 -``` -请求A ──→ 获取锁 ──→ 查 DB ──→ 写入缓存 ──→ 释放锁 -请求B ──→ 获取锁 ──→ 已存在,直接从缓存获取 -``` +### 自定义 Handler -### 缓存雪崩防护 +自定义 Handler 实现公共 `CacheHandler` 契约,并使用带有 `HandlerOrder` 值的 +`@HandlerPriority`。它必须被应用自己的组件扫描发现,或作为应用 Bean 提供。 +Handler 返回类型化 `HandlerResult`,也可以参与后置处理,而不需要自行维护一个 +指向下一个节点的链表。 -TTL 随机化避免大量缓存同时过期: +只有文档明确声明的公共 seam 才适合替换。依赖公共类型或修改 Handler 行为前, +请先阅读 [STABILITY.md](STABILITY.md)。 -``` -设置 TTL = baseTtl ± variance × baseTtl (randomTtl=true 时生效) -``` +## 运行时语义 -## 🆚 与 JetCache / Caffeine / 裸 Redisson 对比 +### 缓存 I/O 失败行为 -在 Redis 之上做缓存,常见有四个选项:JetCache、Caffeine、裸 Redisson、 -ResiCache。本项目定位:**ResiCache for Redisson — Redisson 忘了做的那条 -可声明缓存防护链**。 +| 操作 | 行为 | +|---|---| +| GET | 降级为 cache miss,并记录内部失败 | +| PUT / PUT_IF_ABSENT / CLEAN | 抛出保留原始 cause 的类型化运行时异常 | +| REMOVE | 可观测的 best-effort 移除;移除失败不向上抛出 | +| `get(key, loader)` 且 loader 成功 | 即使写回失败也返回 loader 值;写回失败日志不包含原始 key | +| `get(key, loader)` 且 loader 失败 | 抛出 Spring 的 `Cache.ValueRetrievalException` | -| 能力 | JetCache | Caffeine | 裸 Redisson | **ResiCache** | -|------|:--------:|:--------:|:-----------:|:-------------:| -| 多级缓存(本地 + 远程) | ✅ | 仅本地 | — | — | -| 布隆过滤器(防穿透) | — | — | 手写 | ✅ | -| TTL 抖动(防雪崩) | — | — | 手写 | ✅ | -| 分布式击穿锁(防击穿) | — | — | 手写 | ✅ | -| null 值缓存 | — | — | 手写 | ✅ | -| 热点 Key 提前刷新 | — | — | 手写 | ✅ | -| 声明式 `@注解` 责任链 | 部分 | — | — | ✅ | -| 跨实例广播失效 | ✅ | — | — | — | - -一句话结论:**JetCache 缺的那 3 项防护,以 Redisson-native 责任链补齐** — -布隆过滤器(防穿透)、TTL 抖动(防雪崩)、分布式击穿锁(防击穿)。 -ResiCache 是补齐这 3 项空白的 Redisson 搭档;JetCache 主打多级缓存与 -跨实例广播失效。两者**作用域互补,不是直接替代**。 - -## 📦 项目结构 +`@CacheEvict(allEntries=true)` / CLEAN 是 best-effort、非原子操作,使用 SCAN 游标 +和批量删除。Bloom bit 是独立维护的 membership set,默认由成功写入回填;不会从 +数据源自动重建。清理缓存条目不会删除这些 bit。 -``` -src/main/java/io/github/davidhlp/spring/cache/redis/ -├── annotation/ # @RedisCacheable, @RedisCacheEvict, @RedisCachePut, @RedisCaching -├── cache/ # package-private runtime:AOP、责任链、防护、操作、序列化与装配 -├── chain/ # 稳定 CacheHandler/Operation/Result 契约与决策视图 -├── config/ # 自动配置、RedisProCacheProperties 与 metrics 入口 -├── protection/ # 稳定 BloomIFilter、LockManager、EarlyExpirationMode 契约 -└── serialization/ # SerializationException、迁移 operator 契约与 wire envelope +### 序列化迁移 + +ResiCache 将值存储在内部 `{version, payload}` envelope 中,与 Spring 的 +`GenericJackson2JsonRedisSerializer` 或 `JdkSerializer` 不兼容。已有应用不应 +假定直接切换序列化器是安全的,应采用有边界的 +shadow-read → dual-write → cutover 迁移流程。迁移指引见 +[COMPATIBILITY.md](COMPATIBILITY.md)。 + +## 方案对比 + +ResiCache 刻意比通用缓存框架更聚焦: + +| 能力 | JetCache | Caffeine | 裸 Redisson | **ResiCache** | +|---|:---:|:---:|:---:|:---:| +| 多级本地 + 远端缓存 | 有 | 仅本地 | — | — | +| 布隆过滤器 | — | — | 手写 | 有 | +| TTL 抖动 | — | — | 手写 | 有 | +| 分布式击穿锁 | — | — | 手写 | 有 | +| 空值缓存 | — | — | 手写 | 有 | +| 热点 key 提前刷新 | — | — | 手写 | 有 | +| 声明式防护责任链 | 部分 | — | — | 有 | +| 跨实例广播失效 | 有 | — | — | — | + +JetCache 侧重多级缓存和跨实例广播失效;ResiCache 侧重面向 Redisson 的防护 +责任链。两者解决缓存问题的不同部分,不是互相替代的同类方案。 + +## 已知限制 + +- **仍处于 1.0 之前**:小版本仍可能改变公共契约;依赖扩展点前请阅读 + [STABILITY.md](STABILITY.md)。 +- **防护默认关闭**:`@RedisCacheable` 的五类防护属性默认都是 `false`。 +- **Bloom membership 默认由写入回填**:不会扫描已有数据,缺少 bit 会短路 loader。 + 对已有 key 启用 `useBloomFilter` 前,请先通过公共 `BloomIFilter` seam 预填充或维护。 +- **当前构建线尚无产物**:Boot 4 / Java 21 构建线尚未发布到 Maven Central; + `0.0.2` 是历史上的 Boot 3 / Java 17 构建线。 +- **需要序列化迁移**:已有 Spring 原生序列化值不会自动兼容 ResiCache envelope。 +- **不支持 Reactive 缓存**:WebFlux 的 `Mono` 与 `Flux` 方法不会进入阻塞式 + ResiCache interceptor。 +- **异步缓存方法有限制**:`@Async` 方法不支持同步锁和布隆过滤增强。 +- **防护开关只在启动时解析**:修改 `resi-cache.protection.*` 后必须重启应用。 +- **Time-to-idle 读取**:底层 writer 读取路径刻意不在读时刷新 TTL,以避免额外 + 写放大。 + +完整、经过测试的边界列表请以 [COMPATIBILITY.md](COMPATIBILITY.md) 为准。 + +## 不在范围内 + +ResiCache 刻意不实现以下更适合由专业组件负责的能力: + +- **熔断与限流** → [Resilience4j](https://resilience4j.readthedocs.io/) +- **多级本地 + 远端缓存** → 使用 [Caffeine](https://github.com/ben-manes/caffeine) 作为本地层 +- **Reactive 缓存** → 当前阻塞式 interceptor 不支持 + +## 项目结构 + +```text +ResiCache/ +├── src/main/java/io/github/davidhlp/spring/cache/redis/ +│ ├── annotation/ # 公共 ResiCache 注解 +│ ├── cache/ # 内部 runtime、AOP、责任链、操作与装配 +│ ├── chain/ # 稳定的 Handler、operation、result 契约 +│ ├── config/ # 自动配置与 RedisProCacheProperties +│ ├── protection/ # 稳定的 BloomIFilter 与 LockManager seam +│ └── serialization/ # 序列化与迁移契约 +├── src/test/java/ # 单元、契约与集成测试 +├── resicache-bench/ # 独立 JMH 基准模块 +├── scripts/ci/ # CI 与仓库守卫脚本 +└── docs/adr/ # 已接受的架构决策 ``` -## ⚠️ Known Limitations / 已知限制 +## 开发与验证 -v0.0.x 当前已知限制: +### 前置条件 -- **防护默认全关**:`@RedisCacheable` 的 5 大防护属性默认 `false`,须逐个显式开启 -- **序列化信封与 Spring 原生不兼容**:ResiCache 用 `{version, payload}` 信封序列化,与 Spring 默认 `GenericJackson2JsonRedisSerializer` / `JdkSerializer` 不兼容——**存量项目接入需迁移**,否则全量缓存失效 -- **序列化白名单默认锁作者包名**:`allowed-package-prefixes` 默认仅 `io.github.davidhlp.`,自定义类型须显式配置(见上文 [序列化安全](#序列化安全)) -- **双 Advisor 风险已消除**:`nativeAnnotationMode` 默认 `SELECTIVE`——纯 `@Cacheable` 完全走 Spring 原生、不被 ResiCache 接管。需要 FULL 兼容(接管 `@Cacheable`)可显式 `resi-cache.native-annotation-mode=FULL` -- **不支持 Reactive**(WebFlux / `Mono` / `Flux`):`RedisCacheInterceptor` 是阻塞式,Reactive 方法不触发缓存 -- **缓存 I/O 失败语义**:GET 降级为 miss 并记录日志;PUT、 - PUT_IF_ABSENT、CLEAN 抛出带原始 cause 的类型化运行时异常; - REMOVE 为可观测 best-effort,不抛异常。**read-through(`get(key, loader)`)可用性优先**: - loader 成功值必定返回——写回失败仅记录(脱敏)日志、不覆盖该值; - loader 失败仍以 Spring `Cache.ValueRetrievalException` 呈现。 -- **`@CacheEvict(allEntries=true)`(CLEAN)是 best-effort、非原子**:与 Spring 原生 `RedisCache.clear`/`DefaultRedisCacheWriter.clean` 一致,用 SCAN 游标 + 批量 UNLINK/DEL,CLEAN 期间新写入的 key 可能被遗漏,大 key 集时缓存短暂处于半删状态。刻意不用 Lua/MULTI 原子化(Redis 单线程 O(keyspace) 阻塞、Cluster cross-slot)。Bloom 表示数据源可能存在;CLEAN 只清缓存、保留旧 bits,因此只允许 false-positive,不会制造阻止 loader 的 false-negative。 +- JDK 21 +- Maven 3.x,或仓库自带的 Maven Wrapper +- Docker:运行 Testcontainers Redis 与 Cluster 验证时必需 -## 🚫 Not in Scope +### 验证命令 -ResiCache **刻意不做**以下能力,避免过度膨胀——请用专业工具配合: +```bash +# 不依赖 Docker 的日常路径 +./mvnw -Punit test -B -- **熔断 / 限流** → 配 [Resilience4j](https://resilience4j.readthedocs.io/) 保护下游 -- **多级本地 + 远端缓存** → 配 [Caffeine](https://github.com/ben-manes/caffeine) 做本地层 -- **Reactive 缓存**(WebFlux)→ 不支持 +# 完整 Redis/Testcontainers 验证 +./mvnw clean verify -B -## 🔧 依赖版本 +# 独立的代码风格与测试命名检查 +./mvnw checkstyle:check -B +bash scripts/ci/check-test-names.sh -| 依赖 | 版本 | -|------|------| -| Spring Boot | 4.0.0(parent) | -| Java | 21 | -| Redisson | 3.50.0(optional) | -| Caffeine | 3.1.8 | -| Testcontainers | 1.20.6 | +# 跳过测试打包 +./mvnw clean package -DskipTests -B +``` + +`./mvnw clean verify -B` 强制 JaCoCo 覆盖率门槛:行覆盖率 70%,分支覆盖率 +40%。完整开发与贡献流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。 + +## 项目状态与支持 -完整兼容矩阵见 [COMPATIBILITY.md](COMPATIBILITY.md)。 +- **版本**:`v0.0.2`;1.0 之前只提供有限的语义化版本保证。 +- **维护**:单人维护、无 SLA、尽力支持。 +- **变更历史**:[CHANGELOG.md](CHANGELOG.md) +- **兼容性策略**:[COMPATIBILITY.md](COMPATIBILITY.md) +- **API 稳定性**:[STABILITY.md](STABILITY.md) +- **性能基线**:[PERFORMANCE.md](PERFORMANCE.md) +- **架构决策**:[ADR 索引](docs/adr/README.md) +- **贡献指南**:[CONTRIBUTING.md](CONTRIBUTING.md) -## 项目状态与维护 +## 安全 -- **版本**:v0.0.2 — 语义化版本 < 1.0,API 可能在 minor 版本变更,breaking 项在 [CHANGELOG.md](CHANGELOG.md) 标 ⚠️ -- **维护**:单人维护([DavidHLP](https://github.com/davidhlp)),**Non-SLA best-effort**——不承诺响应时间,但积极修复 issue -- **贡献指南**:欢迎 PR,流程见 [CONTRIBUTING.md](CONTRIBUTING.md) -- **性能实测基准**:JMH 实测数据与 SLO 达标表见 [PERFORMANCE.md](PERFORMANCE.md) -- **稳定性契约**:0.x 周期兼容与 1.0 毕业指标见 [STABILITY.md](STABILITY.md) -- **兼容性矩阵**:支持的 Spring Boot / Java / Redisson 版本见 [COMPATIBILITY.md](COMPATIBILITY.md) -- **安全策略**:漏洞私有报告流程见 [SECURITY.md](SECURITY.md) +疑似漏洞不要直接创建公开 issue,请按照 [SECURITY.md](SECURITY.md) 中的私有 +报告流程提交。 -## 📄 License +## 许可证 [MIT License](LICENSE) © 2026 DavidHLP From a310212a6ae99d1b3281785130bd9be711232692 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Fri, 18 Sep 2026 13:34:21 +0800 Subject: [PATCH 2/3] docs(readme): clarify integration contracts --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++------ README.zh-CN.md | 53 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a842fce4..c9c6fc95 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,23 @@ cd ResiCache The first command is the no-Docker contributor check. The second installs the current source checkout into the local Maven repository for a local consumer; it does not publish an artifact. Do not confuse that local build with the -historical `0.0.2` artifact on Maven Central. The full Redis and Redis Cluster -verification command is documented in [Development](#development). +historical `0.0.2` artifact on Maven Central. + +For a local consumer application, add the current checkout's coordinates to +that application's `pom.xml`: + +```xml + + io.github.davidhlp + ResiCache + 0.0.2 + +``` + +The dependency resolves from the local Maven repository. Continue with the +configuration and application examples below in that consumer application, +not in the ResiCache checkout. The full Redis and Redis Cluster verification +command is documented in [Development](#development). ### 2. Configure Redis @@ -125,8 +140,25 @@ spring: redis: host: localhost port: 6379 + # timeout: 2s +resi-cache: + redis: + mode: single + host: localhost + port: 6379 + database: 0 + tls-enabled: false ``` +`spring.data.redis.*` and `resi-cache.redis.*` configure separate clients. +`spring.data.redis.*` configures the Spring Data Redis connection factory used +for ResiCache cache I/O. `resi-cache.redis.*` configures the Redisson deployment +used by distributed locking and synchronization; `resi-cache.redisson.*` +controls its pool, timeout, and retry settings. In single mode, Redisson may +fall back to Spring Data Redis host, port, database, and password values when +the corresponding `resi-cache.redis.*` values are unset. Keep the effective +endpoints and credentials aligned when using `sync=true`. + ResiCache is discovered through Spring Boot auto-configuration via `RedisCacheAutoConfiguration`. It does not add `@EnableCaching` for the application; the application remains responsible for enabling Spring Cache. @@ -247,9 +279,15 @@ switch is `false`. `native-annotation-mode` controls Spring's native cache annotations: -- `SELECTIVE` (default): only converts native annotations when a ResiCache - annotation is also present, avoiding a dual-advisor path. -- `FULL`: converts all supported Spring cache annotations. +- `SELECTIVE` (default): leaves methods with no ResiCache annotation on + Spring's native path and skips a native operation when the corresponding + ResiCache operation is present. Mixed or non-corresponding annotation + combinations can still produce multiple operations or advisor interception; + do not mix annotations on one method without testing the result. +- `FULL`: converts all supported Spring cache annotations, including when + ResiCache annotations are present; mixed or non-corresponding annotations + can produce multiple operations or advisor interception, so test those + combinations. - `NONE`: ignores native Spring cache annotations in the ResiCache operation source. @@ -332,8 +370,14 @@ resi-cache: ``` The deployment validator checks mode-specific fields during configuration -binding. Credentials, when needed, belong in the application's secret -management system rather than in a committed README snippet. +binding. `resi-cache.redis.*` is the Redisson deployment path; `spring.data.redis.*` +configures the separate connection factory used for cache I/O. In single mode, +Redisson may fall back to Spring Data Redis host, port, database, and password +values when the corresponding `resi-cache.redis.*` values are unset. The +`resi-cache.redisson.*` namespace controls Redisson pool, timeout, and retry +settings. Keep the effective endpoint configurations aligned when `sync=true`. +Credentials, when needed, belong in the application's secret management system +rather than in a committed README snippet. ### Serialization safety @@ -391,6 +435,12 @@ The public annotation family mirrors Spring Cache operations: - `@RedisCaching` for grouping multiple ResiCache operations on one method or type. +`@RedisCaching` can expose operations at type level, but the protection-policy +fields inside its composed annotations are evaluated at method level. A +type-level declaration does not apply those fields to otherwise unannotated +methods; repeat the relevant `@RedisCacheable`, `@RedisCachePut`, or +`@RedisCacheEvict` at method level when the method needs that policy. + ### Custom handlers A custom handler implements the public `CacheHandler` contract and uses diff --git a/README.zh-CN.md b/README.zh-CN.md index 5b5a4b45..11add619 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -107,7 +107,20 @@ cd ResiCache 第一条命令是不依赖 Docker 的贡献者检查;第二条命令会把当前源码按 POM 版本安装到本地 Maven 仓库,供本地消费者试用,不会发布产物。不要把这个本地 -构建与 Maven Central 上历史版本的 `0.0.2` 混淆。完整的 Redis 与 Redis +构建与 Maven Central 上历史版本的 `0.0.2` 混淆。 + +对于本地消费者应用,将当前检出版本的坐标加入该应用的 `pom.xml`: + +```xml + + io.github.davidhlp + ResiCache + 0.0.2 + +``` + +该依赖从本地 Maven 仓库解析。下面的配置和应用示例都应在这个消费者应用 +中执行,而不是在 ResiCache 源码检出目录中执行。完整的 Redis 与 Redis Cluster 验证命令见[开发与验证](#开发与验证)。 ### 2. 配置 Redis @@ -118,8 +131,24 @@ spring: redis: host: localhost port: 6379 + # timeout: 2s +resi-cache: + redis: + mode: single + host: localhost + port: 6379 + database: 0 + tls-enabled: false ``` +`spring.data.redis.*` 与 `resi-cache.redis.*` 配置的是相互独立的客户端。 +`spring.data.redis.*` 配置 ResiCache 缓存 I/O 使用的 Spring Data Redis +连接工厂。`resi-cache.redis.*` 配置分布式锁和同步能力使用的 Redisson +部署;`resi-cache.redisson.*` 控制其连接池、超时和重试设置。在 single +模式下,如果对应的 `resi-cache.redis.*` 值未设置,Redisson 可能回退使用 +Spring Data Redis 的 host、port、database 和 password 值。使用 `sync=true` +时应保持最终生效的端点和凭据一致。 + ResiCache 通过 Spring Boot 自动配置入口 `RedisCacheAutoConfiguration` 被发现。 它不会替应用添加 `@EnableCaching`;是否启用 Spring Cache 仍由应用负责。 @@ -229,9 +258,13 @@ resi-cache: `native-annotation-mode` 控制 Spring 原生缓存注解: -- `SELECTIVE`(默认):仅在同时存在 ResiCache 注解时转换原生注解,避免双 - Advisor 路径。 -- `FULL`:转换所有受支持的 Spring 缓存注解。 +- `SELECTIVE`(默认):没有 ResiCache 注解的方法保留在 Spring 原生路径; + 存在 ResiCache 注解时,仅在对应 ResiCache operation 不存在时转换原生 + operation。混用或不对应的注解组合仍可能产生多个 operation 或 Advisor + 拦截;同一方法不要混用注解,除非已经验证实际结果。 +- `FULL`:即使存在 ResiCache 注解,也转换所有受支持的 Spring 原生缓存 + 注解;混用或不对应的注解可能产生多个 operation 或 Advisor 拦截,应测试 + 这些组合。 - `NONE`:在 ResiCache operation source 中忽略 Spring 原生缓存注解。 ### 全局配置 @@ -309,7 +342,12 @@ resi-cache: # sentinel-nodes: [host1:26379] ``` -部署配置会在绑定阶段校验与模式相关的字段。生产环境凭据应交由应用的密钥 +部署配置会在绑定阶段校验与模式相关的字段。`resi-cache.redis.*` 是 +Redisson 部署配置路径;`spring.data.redis.*` 配置缓存 I/O 使用的独立连接 +工厂。在 single 模式下,如果对应的 `resi-cache.redis.*` 值未设置,Redisson +可能回退使用 Spring Data Redis 的 host、port、database 和 password 值。 +`resi-cache.redisson.*` 控制 Redisson 连接池、超时和重试设置。使用 +`sync=true` 时应保持最终生效的端点配置一致。生产环境凭据应交由应用的密钥 管理系统处理,不要写入提交的 README 示例。 ### 序列化安全 @@ -367,6 +405,11 @@ resi-cache: - `@RedisCacheEvict`:移除缓存。 - `@RedisCaching`:在一个方法或类型上组合多个 ResiCache 操作。 +`@RedisCaching` 可以在类型级别暴露操作,但其中组合注解的保护策略字段按 +方法级别求值。仅有类型级声明不会把这些字段应用到未添加方法级注解的方法; +如果某个方法需要该策略,请在方法级别重复相关的 `@RedisCacheable`、 +`@RedisCachePut` 或 `@RedisCacheEvict`。 + ### 自定义 Handler 自定义 Handler 实现公共 `CacheHandler` 契约,并使用带有 `HandlerOrder` 值的 From 141816e37431243a7481a3af13babcba89655e25 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Fri, 18 Sep 2026 13:51:11 +0800 Subject: [PATCH 3/3] docs(readme): clarify advisor and redis defaults --- README.md | 46 +++++++++++++++++++++++++++------------------- README.zh-CN.md | 38 ++++++++++++++++++++++---------------- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index c9c6fc95..fdf2b9d4 100644 --- a/README.md +++ b/README.md @@ -154,10 +154,13 @@ resi-cache: `spring.data.redis.*` configures the Spring Data Redis connection factory used for ResiCache cache I/O. `resi-cache.redis.*` configures the Redisson deployment used by distributed locking and synchronization; `resi-cache.redisson.*` -controls its pool, timeout, and retry settings. In single mode, Redisson may -fall back to Spring Data Redis host, port, database, and password values when -the corresponding `resi-cache.redis.*` values are unset. Keep the effective -endpoints and credentials aligned when using `sync=true`. +controls its pool, timeout, and retry settings. The `resi-cache.redis.*` defaults +are `localhost:6379` and database `0`, so omitted endpoint fields do not inherit +`spring.data.redis.*`. In single mode, an absent `resi-cache.redis.password` may +fall back to the Spring Data Redis password. For single, cluster, or sentinel +deployments, configure matching endpoints or topology explicitly in both +namespaces when using `sync=true`; do not rely on one namespace to configure the +other. ResiCache is discovered through Spring Boot auto-configuration via `RedisCacheAutoConfiguration`. It does not add `@EnableCaching` for the @@ -279,15 +282,17 @@ switch is `false`. `native-annotation-mode` controls Spring's native cache annotations: -- `SELECTIVE` (default): leaves methods with no ResiCache annotation on - Spring's native path and skips a native operation when the corresponding - ResiCache operation is present. Mixed or non-corresponding annotation - combinations can still produce multiple operations or advisor interception; - do not mix annotations on one method without testing the result. -- `FULL`: converts all supported Spring cache annotations, including when - ResiCache annotations are present; mixed or non-corresponding annotations - can produce multiple operations or advisor interception, so test those - combinations. +- `SELECTIVE` (default): pure-native methods with no ResiCache annotation remain + on Spring's native path and are not converted by the ResiCache operation + source. When a ResiCache annotation is present, a matching native operation + is skipped by ResiCache's converter (internal conversion de-duplication). + An explicitly mixed method can still be seen by both the ResiCache advisor + and Spring's native cache advisor; do not mix annotations on one method + without testing the result. +- `FULL`: converts all supported Spring cache annotations, including plain + native-only methods. When `@EnableCaching` or another Spring native advisor + is active, those methods can also be handled by both advisors; enable `FULL` + only when that duplicate interception or execution is intentional and tested. - `NONE`: ignores native Spring cache annotations in the ResiCache operation source. @@ -371,13 +376,16 @@ resi-cache: The deployment validator checks mode-specific fields during configuration binding. `resi-cache.redis.*` is the Redisson deployment path; `spring.data.redis.*` -configures the separate connection factory used for cache I/O. In single mode, -Redisson may fall back to Spring Data Redis host, port, database, and password -values when the corresponding `resi-cache.redis.*` values are unset. The +configures the separate connection factory used for cache I/O. The +`resi-cache.redis.host`, `port`, and `database` defaults are `localhost`, `6379`, +and `0`, so omitted endpoint fields do not adopt Spring Data Redis values. In +single mode, an absent `resi-cache.redis.password` may fall back to the Spring +Data Redis password. For single, cluster, or sentinel deployments, explicitly +configure matching endpoints or topology in both namespaces when using +`sync=true`; do not rely on one namespace to configure the other. The `resi-cache.redisson.*` namespace controls Redisson pool, timeout, and retry -settings. Keep the effective endpoint configurations aligned when `sync=true`. -Credentials, when needed, belong in the application's secret management system -rather than in a committed README snippet. +settings. Credentials, when needed, belong in the application's secret +management system rather than in a committed README snippet. ### Serialization safety diff --git a/README.zh-CN.md b/README.zh-CN.md index 11add619..aee80432 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -144,10 +144,12 @@ resi-cache: `spring.data.redis.*` 与 `resi-cache.redis.*` 配置的是相互独立的客户端。 `spring.data.redis.*` 配置 ResiCache 缓存 I/O 使用的 Spring Data Redis 连接工厂。`resi-cache.redis.*` 配置分布式锁和同步能力使用的 Redisson -部署;`resi-cache.redisson.*` 控制其连接池、超时和重试设置。在 single -模式下,如果对应的 `resi-cache.redis.*` 值未设置,Redisson 可能回退使用 -Spring Data Redis 的 host、port、database 和 password 值。使用 `sync=true` -时应保持最终生效的端点和凭据一致。 +部署;`resi-cache.redisson.*` 控制其连接池、超时和重试设置。`resi-cache.redis.*` +的默认 host、port、database 分别为 `localhost`、`6379`、`0`,因此省略 +端点字段不会继承 `spring.data.redis.*`。在 single 模式下,如果 +`resi-cache.redis.password` 未设置,Redisson 可能回退使用 Spring Data Redis +密码。使用 `sync=true` 时,对于 single、cluster 或 sentinel 部署,都必须在 +两个命名空间中显式配置匹配的端点或拓扑;不要依赖一个命名空间配置另一个。 ResiCache 通过 Spring Boot 自动配置入口 `RedisCacheAutoConfiguration` 被发现。 它不会替应用添加 `@EnableCaching`;是否启用 Spring Cache 仍由应用负责。 @@ -258,13 +260,14 @@ resi-cache: `native-annotation-mode` 控制 Spring 原生缓存注解: -- `SELECTIVE`(默认):没有 ResiCache 注解的方法保留在 Spring 原生路径; - 存在 ResiCache 注解时,仅在对应 ResiCache operation 不存在时转换原生 - operation。混用或不对应的注解组合仍可能产生多个 operation 或 Advisor - 拦截;同一方法不要混用注解,除非已经验证实际结果。 -- `FULL`:即使存在 ResiCache 注解,也转换所有受支持的 Spring 原生缓存 - 注解;混用或不对应的注解可能产生多个 operation 或 Advisor 拦截,应测试 - 这些组合。 +- `SELECTIVE`(默认):没有 ResiCache 注解的纯原生方法保留在 Spring + 原生路径中,不会被 ResiCache operation source 转换。存在 ResiCache 注解 + 时,匹配的原生 operation 会被 ResiCache 转换器跳过(这是内部转换去重), + 但显式混用注解的方法仍可能同时被 ResiCache Advisor 和 Spring 原生缓存 + Advisor 看到;同一方法不要混用注解,除非已经验证实际结果。 +- `FULL`:转换所有受支持的 Spring 原生缓存注解,包括只有原生注解的方法。 + 当 `@EnableCaching` 或其他 Spring 原生 Advisor 生效时,这些方法也可能被 + 两个 Advisor 处理;只有在明确需要并测试了这种重复拦截或执行时才应启用。 - `NONE`:在 ResiCache operation source 中忽略 Spring 原生缓存注解。 ### 全局配置 @@ -344,11 +347,14 @@ resi-cache: 部署配置会在绑定阶段校验与模式相关的字段。`resi-cache.redis.*` 是 Redisson 部署配置路径;`spring.data.redis.*` 配置缓存 I/O 使用的独立连接 -工厂。在 single 模式下,如果对应的 `resi-cache.redis.*` 值未设置,Redisson -可能回退使用 Spring Data Redis 的 host、port、database 和 password 值。 -`resi-cache.redisson.*` 控制 Redisson 连接池、超时和重试设置。使用 -`sync=true` 时应保持最终生效的端点配置一致。生产环境凭据应交由应用的密钥 -管理系统处理,不要写入提交的 README 示例。 +工厂。`resi-cache.redis.host`、`port`、`database` 的默认值分别为 +`localhost`、`6379`、`0`,因此省略端点字段不会采用 Spring Data Redis 的值。 +在 single 模式下,如果 `resi-cache.redis.password` 未设置,Redisson 可能 +回退使用 Spring Data Redis 密码。使用 `sync=true` 时,对于 single、cluster +或 sentinel 部署,都必须在两个命名空间中显式配置匹配的端点或拓扑;不要 +依赖一个命名空间配置另一个。`resi-cache.redisson.*` 控制 Redisson 连接池、 +超时和重试设置。生产环境凭据应交由应用的密钥管理系统处理,不要写入提交的 +README 示例。 ### 序列化安全