Skip to content

Latest commit

 

History

History
90 lines (61 loc) · 3.76 KB

File metadata and controls

90 lines (61 loc) · 3.76 KB

Redis implementation

A standalone Redis-backed cache manager (C#) with per-key subkeys and numeric arrays.

Architecture

Architecture

Requirements

  • Very large number of keys, stored in Redis (not in-process).
  • Potential Burst repeats of the same files
    • During bursts, same files can be accessed very frequently for a short while, and then never again.
  • Per key: multiple subkeys, each storing an array of numbers
    • Each cache key is a Redis Hash (HSET).
    • Each hash field is a subkey, and the field value is a serialized numeric array.
    • This keeps all subkeys for a key under one Redis entry and avoids per-subkey Redis keys.
  • LRU eviction
    • Implemented using a Redis Sorted Set (ZSET) where:
      • member = cache key
      • score = last-access timestamp (Unix milliseconds). Lower score = older; ZRANGE 0 0 returns the oldest entry.

Why not a Redis linked list?

Redis provides Lists (LPUSH/LRANGE) but they do not support the required operation efficiently:

  • we need O(1) (or close) update of a key’s position on access
  • we would need a “key -> node” pointer to move an arbitrary list element

Redis Lists do not expose stable node references/pointers, and removing/moving an arbitrary element requires scanning (LREM) which is O(n).

A ZSET provides:

  • fast recency updates (ZADD)
  • fast eviction of oldest (ZRANGE 0 0)
  • no external “dictionary -> node” structure is needed

Eviction: inside Redis (Lua)

This implementation performs write + LRU update + eviction atomically inside Redis using a Lua script (write path).

Other operations use direct Redis commands from the client:

Rationale:

  • latency: avoids a separate external eviction service/process (proxy/middleware) and its extra network hop
  • consistency: prevents races between writes and eviction
  • durability/HA: aligns with Redis durability/replication settings rather than a separate stateful component

Note: TryGet updates the LRU score after a successful HGET. If you require strictly atomic "read + touch" semantics, the read path can be moved to Lua as well.

Contents

  • src/RedisCacheManager/ C# library (StackExchange.Redis)
  • src/RedisCacheTester/ console app to exercise the cache
  • docker-compose.yml brings up Redis + tester

Run (Docker)

docker compose up --build -d
docker compose down

Redis durability / HA notes (brief)

  • Compose uses appendonly yes (AOF) with appendfsync everysec.
    • Typical impact on steady-state write latency on an SSD is often small (commonly sub-millisecond to a couple ms at p50/p95), but tail latency can increase.
    • Expect occasional p99/p99.9 spikes when the OS flush/rewrite work happens (often single-digit to tens of milliseconds on SSD; can be 50ms+ on slow/contended disks).
    • Exact numbers depend primarily on disk type (SSD vs HDD), host load, and write rate.
  • For high availability in production, consider:
    • Redis replication + Sentinel, or
    • Redis Cluster (if you need sharding), or
    • a managed Redis offering

Measuring the impact locally (recommended)

Run a benchmark against your local Redis with AOF enabled:

redis-benchmark -h localhost -p 6379 -t set,get -n 200000 -c 50 -q

Then temporarily disable AOF (or switch appendfsync no) and rerun to compare p50/p95/p99 latency.