Skip to content

Hyshmily/hotkey

Repository files navigation

HotKey

Maven Central JitPack GitHub Release CI Coveralls Java Spring Boot Last Commit License Visitors

中文

HotKey is a configurable, high-performance, low-cost lightweight distributed cache and preheating framework, designed to solve cluster-wide distributed consistent caching problems for arbitrary sudden hotspot data at minimal cost.

Fully decouples business code from distributed coordination infrastructure via Redis and RabbitMQ.

End-to-end latency under default settings: ~300ms (P99).

Benchmarks:

  • peek ~16M ops/s (pure Caffeine lookup, no side effects)
  • get (L1 hit) ~15M ops/s (full path including TopK + Reporter)

Inspired by JD.com's hotkey project; algorithm support from Aegis.

Quick Start

1. Add Dependency

Configuration reference:

Quick Deploy YAML Templates

Local mode (App side) — just add the hotkey dependency to run; uncomment optional features as needed

hotkey:
  # local parameters all use defaults, no explicit config needed

  # —— Optional features, uncomment as needed ——

  # Cross-instance cache sync (requires spring-boot-starter-amqp + spring-boot-starter-data-redis)
  # sync:
  #   enabled: true

  # Spring Cache integration (requires spring-boot-starter-cache)
  # spring-cache:
  #   enabled: true

  # Worker decision listener (requires spring-boot-starter-amqp + spring-boot-starter-data-redis)
  # worker-listener:
  #   enabled: true
  # sync:
  #   enabled: true     # worker-listener depends on hotKeyRedisLoader Bean

  # Consistent hashing is enabled by default (dynamic Worker routing via heartbeat)
  # local:
  #   consistent-hashing:
  #     enabled: true     # (already enabled by default)

Single Worker (standalone node) — add spring-boot-starter-amqp

hotkey:
  worker:
    enabled: true
    routing:
      app-name: myapp # 【Required】Must match App-side hotkey.local.app-name
      # Consistent hashing is enabled by default; Workers register via heartbeat

# Multi-Worker example: 3 machines, same app-name
# Consistent hashing routes keys to the correct Worker via heartbeat automatically
# No static sharding config needed — just add more machines
# Recommended: deploy local App first, then start Workers

Cluster health threshold — when expected-worker-count: 0 (dynamic mode, default), min-alive-workers: 0 means 1 alive Worker is healthy. When expected-worker-count: N (fixed mode), uses majority formula N/2 + 1. Setting min-alive-workers overrides either mode. See docs/CONFIG.md for details.

All parameters: See CONFIG.md

Local Mode (App Side) — Maven Dependency

Maven Central (no extra repository needed):

<dependency>
  <groupId>io.github.hyshmily</groupId>
  <artifactId>hotkey</artifactId>
  <version>1.1.54</version>
</dependency>

JitPack:

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

<dependency>
    <groupId>io.github.hyshmily</groupId>
    <artifactId>hotkey</artifactId>
    <version>1.1.54</version>
</dependency>

GitHub Packages:

<repositories>
  <repository>
    <id>github</id>
    <name>GitHub Packages</name>
    <url>https://maven.pkg.github.com/hyshmily/hotkey</url>
  </repository>
</repositories>

<dependency>
  <groupId>io.github.hyshmily</groupId>
  <artifactId>hotkey</artifactId>
  <version>1.1.54</version>
</dependency>

Worker Node (Standalone) — JAR / Docker

Important

Prerequisites: Redis + RabbitMQ

Pre-built images are hosted on GHCR.

Pull: Log in with a GitHub PAT that has read:packages scope:

echo $PAT | docker login ghcr.io -u hyshmily --password-stdin

Full stack via docker compose (includes Redis + RabbitMQ):

docker compose -f worker/docker-compose.yml up -d

Scale multiple Worker instances:

docker compose -f worker/docker-compose.yml up -d --scale worker=3

Run standalone (external Redis + RabbitMQ):

docker run -d --name hotkey-worker -p 8080:8080 \
  -e SPRING_RABBITMQ_HOST=rabbitmq \
  -e SPRING_DATA_REDIS_HOST=redis \
  -e HOTKEY_WORKER_ENABLED=true \
  ghcr.io/hyshmily/hotkey-worker:1.1.54

Run JAR directly (no Docker):

mvn clean package -pl worker
java -jar worker/target/hotkey-worker-1.1.54.jar

2. Configuration

Default local configuration:

Feature configuration:

Feature How to Enable Description
Redis L2 Cache Add RedisTemplate Bean Two-level cache, L2 fallback
Cross-instance Sync hotkey.sync.enabled=true RabbitMQ-based cache invalidation
Worker Listener hotkey.worker-listener.enabled=true Receive HOT/COOL decisions from Worker
Worker Mode hotkey.worker.enabled=true Run a dedicated Worker node
Worker TopK Persist hotkey.worker.persistence.enabled=true Warm start from Redis after restart
Access Reporting hotkey.report.enabled=true (default) Report access counts to Worker
Reporter Self-Protection hotkey.local.reporter.enabled=true (default) BBR backpressure for Reporter flush
Spring Cache Integration hotkey.spring-cache.enabled=true @Cacheable / @CachePut / @CacheEvict fused with HotKey detection

See CONFIG.md for the full property reference.

3. Usage

Note

Serialization: HotKey internally uses StringRedisTemplate. Value serialization is entirely up to the caller. Jackson (Spring Boot default, JSON) or Kryo (binary, maximum throughput) are recommended. JDK native serialization is not recommended.

Read Operations

@Autowired
private HotKey hotKey;

// A. peek — L1 only, no hot key tracking
Optional<String> r = hotKey.peek("user:123"); // returns Optional.empty() on L1 miss

// B. computeIfAbsent — simplified get (no Optional wrapper)
String val = hotKey.computeIfAbsent("user:123", () -> redisTemplate.opsForValue().get("user:123"));

// C. get — two-level cache (Redis or any backend)
Optional<String> r = hotKey.get("user:123", () -> redisTemplate.opsForValue().get("user:123"));

// D. getWithSoftExpire — soft expiration (stale-while-revalidate)
Optional<String> r = hotKey.getWithSoftExpire("user:123", () -> redisTemplate.opsForValue().get("user:123"));

// E. Fluent read API + fallback chain
User user = hotKey
  .read("user:42")
  .withPrimary(userRepo::findById)
  .thenExecute(backupRepo::findById)
  .withHardTtl(30_000)
  .withSoftTtl(10_000)
  .allowBroadcast()
  .executeOrNull();

Write Operations

// F. putThrough — write-through + broadcast
hotKey.putThrough("user:123", newValue, () -> redisTemplate.opsForValue().set("user:123", newValue));

// G. putBeforeInvalidate — mutate then invalidate (collection types)
hotKey.putBeforeInvalidate(key, () -> redisTemplate.opsForSet().add(key, members));

// H. putLocal — local write only, no broadcast, no version bump
hotKey.putLocal("user:123", cachedValue, hardTtlMs, softTtlMs); // custom TTL

// I. evictLocal — evict from local cache only, no broadcast, no version bump
hotKey.evictLocal("user:123");                          // single key

// J. refresh — local evict then load and cache
hotKey.refresh("user:123", () -> loadUser(123), hardTtlMs, softTtlMs); // with TTL override

// K. Fluent write API
hotKey.write("user:42").withHardTtl(30_000).putThrough(newValue, dbWriter);
hotKey.write("user:42").putBeforeInvalidate(dbMutation);
hotKey.write("user:42").invalidate();

Custom per-entry TTL

HotKey uses differentiated TTLs: hot keys and normal keys have independent defaults. Per-call overrides take effect on top.

Key State Hard TTL (Caffeine eviction) Soft TTL (stale-while-revalidate)
Normal default-hard-ttl-ms (5min) default-soft-ttl-ms (30s)
Hot default-hot-hard-ttl-ms (1h) default-hot-soft-ttl-ms (5min)
// 5 min hard TTL + 30s soft TTL
Optional<String> shopJson = hotKey.get("shop:" + shopId,
    () -> redisTemplate.opsForValue().get("shop:" + shopId),
    TimeUnit.MINUTES.toMillis(5), TimeUnit.SECONDS.toMillis(30));

// 30s hard TTL, soft TTL uses default
hotKey.putThrough("weather:" + city, weatherData,
    () -> redisTemplate.opsForValue().set("weather:" + city, weatherData),
    TimeUnit.SECONDS.toMillis(30), 0);

// registerRefresh / updateRefresh — scheduled background refresh (softTtlMs = interval)
  hotKey.registerRefresh("user:123", () -> loadUser(123), 300_000L, 60_000L);  // every 60s
  hotKey.updateRefresh("user:123", () -> loadUser(123), 300_000L, 30_000L);    // change to 30s
  hotKey.unregisterRefresh("user:123");                                         // stop

Note

Cache avalanche protection: CacheExpireManager applies a uniform random offset via DelayUtil.computeTtlJitter() to every expiration timestamp (default ±5%). A 5-minute hard TTL actually expires between 4.75 ~ 5.25 minutes under the default offset. Controlled by hotkey.local.ttl-jitter-ratio (ratio, default 0.05 = ±5%, 0 to disable).

Tip

Per-call TTL semantics: passing 0 uses the configured default for that key state. For pure logical expiration (hard TTL never evicts, soft expire only): pass hardTtlMs = Long.MAX_VALUE to getWithSoftExpire(key, reader, Long.MAX_VALUE, softTtlMs) — the entry permanently resides in Caffeine. This usage is explicitly supported by Caffeine's Expiry JavaDoc: "To indicate no expiration an entry may be given an excessively long period, such as Long.MAX_VALUE." (source)

Worker Mode

Worker mode provides cluster-wide hotspot detection via dedicated nodes. App instances periodically report access counts; the Worker runs a sliding window + state machine pipeline and broadcasts HOT/COOL decisions back to all instances. State machine parameters (confirmCount, coolCount, preCoolGraceCount) can be adjusted at runtime via /actuator/hotkey/worker/state.

Mode worker.enabled Activated Beans
App-only false (default) HotKeyCache, TopK, reporter, actuator, sync
Worker-only true Worker only (no cache — get()/putThrough() throw HotKeyModeException)

Worker Cluster Health: Set hotkey.local.expected-worker-count to the expected number of Workers in production. When set >0, ClusterHealthView uses majority quorum (> expectedWorkerCount / 2) as the healthy Worker threshold; when 0 (default), the cluster is considered unhealthy until at least one heartbeat is received. This enables precise detection of partial Worker failures and graceful degradation decisions.

Worker TopK Persistence (Warm Start): When hotkey.worker.persistence.enabled=true, the Worker periodically snapshots the TopK list to Redis. On restart, TopKPersistService loads the last snapshot and replays it into the HeavyKeeper sketch, reducing warmup from hours to seconds.

Spring Cache Integration

Enable hotkey.spring-cache.enabled=true. Standard @Cacheable / @CachePut / @CacheEvict are automatically routed through HotKey's hotspot detection, soft expiration, and cross-instance sync.

Annotation Role on @Cacheable
@HotKeyCacheTTL Override hard/soft TTL
@HotKeyPreload Pre-inflate HeavyKeeper counts so known hot keys take effect immediately
@Intercept Skip method body via trigger mode (IS_LOCAL_HOT/FORCE/QPS); degrades via @Intercept.fallback(), @Fallback, or peek()
@Fallback Provide fallback value when blocked, intercepted, or on exception
@NullCaching Opt into caching null return values (default true)
@Broadcast Suppress cross-instance sync messages
@Cacheable(cacheNames = "users", key = "#id")
@HotKeyCacheTTL(softTtlMs = 1000)
@Intercept @Fallback
public User getUser(Long id) { ... }

// QPS rate-limit interception
@Cacheable(cacheNames = "products", key = "#id")
@Intercept(trigger = InterceptTrigger.QPS, QPS = 500, fallback = "'throttled'")
@Fallback
public Product getProduct(String id) { ... }

// Hot key preloading
@Cacheable(cacheNames = "flash", key = "#id")
@HotKeyPreload(keys = {"item-001", "item-002"})
@Intercept
public String getFlashItem(String id) { ... }

Requires spring-boot-starter-cache and spring-boot-starter-aop on the classpath.


Cache Sync

Enable hotkey.sync.enabled=true.

Rule System

Enable hotkey.sync.enabled=true to enable cross-instance rule synchronization. The rule system supports two actions:

Action Effect on matching keys
BLOCK get() / getWithSoftExpire() throw HotKeyBlockedException; putThrough() skips
ALLOW_NO_REPORT Process normally but skip Worker reporting (reduces noise from high-frequency keys)

Pattern Types

RuleMatcher.of(pattern, action) auto-detects the pattern:

Pattern Type Matches
"user:123" EXACT Exact key
"temp:*" PREFIX Keys starting with temp:
"order:*-detail" WILDCARD Glob-style (* / ?) match
"regex:user:\\d+" REGEX Java regex

Persistence & Broadcast

  • With Redis: Each addRule()/removeRule()/clearRules() serializes the rule list to HotKeyConstants.REDIS_KEY_RULES ("hotkey:rules"). On startup, RuleMatcher.initRules() loads from Redis. Changes are also broadcast via TYPE_RULES_SYNC — peers call RuleMatcher.syncRules() for atomic replacement without triggering secondary broadcasts (loop-free).
  • Without Redis: Same operations are broadcast to all peers via the CacheSyncPublisher fanout exchange. Each peer holds the full rule set in memory.
  • Manual broadcast: hotKey.broadcastAllLocalRulesManually() loads from Redis (if available) and re-broadcasts the current rule set to all peers.

Monitoring

HotKey provides two complementary monitoring mechanisms.

See MONITOR.md for the full response format and field descriptions.

Design Details

See CONTEXT.md for domain terminology. Architecture Decision Records (ADRs) are maintained in docs/adr/.

License

Apache License 2.0

Packages

 
 
 

Contributors

Languages