A standalone Redis-backed cache manager (C#) with per-key subkeys and numeric arrays.
- 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.
- Each cache key is a Redis Hash (
- LRU eviction
- Implemented using a Redis Sorted Set (
ZSET) where:- member = cache key
- score = last-access timestamp (Unix milliseconds). Lower score = older;
ZRANGE 0 0returns the oldest entry.
- Implemented using a Redis Sorted Set (
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
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:
RedisCacheManager.TryGetAsyncdoes anHGETand (on hit) updates recency with a separateZADD(HashGetAsync,SortedSetAddAsync).RedisCacheManager.RemoveKeyAsyncdeletes the Hash key and removes the member from the LRUZSET(KeyDeleteAsync,SortedSetRemoveAsync).
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.
src/RedisCacheManager/C# library (StackExchange.Redis)src/RedisCacheTester/console app to exercise the cachedocker-compose.ymlbrings up Redis + tester
docker compose up --build -ddocker compose down- Compose uses
appendonly yes(AOF) withappendfsync 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
Run a benchmark against your local Redis with AOF enabled:
redis-benchmark -h localhost -p 6379 -t set,get -n 200000 -c 50 -qThen temporarily disable AOF (or switch appendfsync no) and rerun to compare p50/p95/p99 latency.