Persistence — Redis
RedisClient Jedis 5.1.0 pool 32, RedisKey 30+ keys, types STRING/HASH/ZSET/SET, member SHA256 16B score mmr, pipeline operations and circuit breaker.
Sources:
src/main/java/com/safjnest/redis/RedisClient.java,src/main/java/com/safjnest/redis/RedisKey.java,src/main/java/com/safjnest/utils/JsonCodec.java.
Redis is only ephemeral cache — no authoritative data. Everything is rebuildable from Mongo (docs/architecture/README.md:15, iodid/architecture.mdx).
RedisClient — Jedis 5.1.0 pool 32
RedisClient (src/main/java/com/safjnest/redis/RedisClient.java:23) — single JedisPool, localhost:6379, CONNECTION_TIMEOUT_MS=500 (:28).
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(32); // :36
config.setMaxIdle(32); // :37
config.setMinIdle(8); // :38
config.setTestOnBorrow(false); // :39
config.setTestOnReturn(false); // :40
config.setTestWhileIdle(true); // :41
config.setMinEvictableIdleDuration(Duration.ofMinutes(1)); // :42
config.setTimeBetweenEvictionRuns(Duration.ofSeconds(30)); // :43
config.setBlockWhenExhausted(true); // :45
pool = new JedisPool(config, "localhost", 6379, CONNECTION_TIMEOUT_MS);
for (int i=0;i<8;i++) warmup.add(pool.getResource()); // :51
SORTED_SET_BATCH_SIZE = 250(:27) — batch forzaddpipeline.TEMPORARY_TTL_SECONDS = 60(:29) — everysetuses 60s, regardless ofRedisKey.ttl.RETRY_AFTER_FAILURE_MS = 30_000(:30) — circuit breaker.
All methods catch Exception, call markUnavailable() (sets disabledUntil = now+30s :550) and return null/false/0 — never propagate to the caller. canUseRedis() (:545) checks currentTimeMillis >= disabledUntil.
Codec: JsonCodec.toJson/fromJson (JsonCodec.java:28) with FIELD ANY, JavaTimeModule.
RedisKey enum — 30+ keys
RedisKey (src/main/java/com/safjnest/redis/RedisKey.java:7):
public enum RedisKey { R4J_SUMMONER("r4j:summoner:by-id:%s:%s", Duration.ofSeconds(60)), ... }
RedisKey(String pattern, Duration ttl) { this.pattern = pattern; this.ttl = ttl; this.prefix = App.isTesting() ? "beebot_test:lol" : "beebot:lol"; }
public String of(Object... args) { return String.format(prefix + ":" + pattern, args); }
public int ttlSeconds() { return (int) ttl.toSeconds(); }
Prefix: beebot:lol prod, beebot_test:lol test (:67). ttlSeconds() returns the declared seconds, but RedisClient.set always uses TEMPORARY_TTL_SECONDS=60.
Complete table
| # | Key | Pattern | Declared TTL | Effective TTL | Type | Value |
|---|---|---|---|---|---|---|
| 1 | R4J_SUMMONER | r4j:summoner:by-id:%s:%s (region, summonerId) | 60s | 60s | STRING (JSON) | Summoner R4J |
| 2 | R4J_SUMMONER_ID | r4j:summoner:id-by-puuid:%s:%s | 60s | 60s | STRING | summoner ID |
| 3 | R4J_ACCOUNT | r4j:account:by-puuid:%s:%s | 60s | 60s | STRING | account |
| 4 | R4J_ACCOUNT_BY_NAME | r4j:account:by-name:%s:%s:%s | 60s | 60s | STRING | account by gameName#tagLine |
| 5 | R4J_USER_ID_BY_PUUID | r4j:account:user-id-by-puuid:%s:%s | 60s (orig 6h) | 60s | STRING | userId |
| 6 | R4J_LEAGUE_ENTRIES | r4j:league:entries:%s:%s | 60s (orig 6h) | 60s | STRING | league entries |
| 7 | R4J_CHAMPION_MASTERIES | r4j:league:champion-masteries:%s:%s | 60s (orig 6h) | 60s | STRING | masteries |
| 8 | R4J_SPECTATOR_CURRENT | r4j:match:current:%s:%s | 60s | 60s | STRING | LiveGame |
| 9 | R4J_MATCH_LIST | r4j:match:list:%s:%s:%s:%s | 60s (orig 1h) | 60s | STRING | match list |
| 10 | R4J_MATCH | r4j:match:by-id:%s:%s | 60s (orig persistent) | 60s | STRING | R4J match |
| 11 | R4J_SUMMONER_REFRESH_COOLDOWN | r4j:summoner:refresh:cooldown:%s:%s (puuid, shard) | 60s (orig 2m) | 60s | STRING | cooldown flag (nx + ex) |
| 12 | MATCH_DETAIL | los:%s:%s:match:%s:detail (cacheRegion, shard, gameId) | 60s (orig 6h) | 60s | STRING | Match |
| 13 | SUMMONER_DATA | los:%s:%s:summoner:%s:data | 60s (orig 1h) | 60s | STRING | summoner data |
| 14 | SUMMONER_AUTOCOMPLETE | los:%s:%s:summoner:autocomplete:%s | 60s (orig 6h) | 60s | STRING | autocomplete |
| 15 | SUMMONER_SEARCH | los:%s:%s:summoner:search:%s | 60s (orig 1h) | 60s | STRING | search |
| 16 | SUMMONER | los:%s:%s:summoner:%s (cacheRegion, shard, puuid) | 60s (orig 4h) | 60s | STRING | Summoner |
| 17 | SUMMONER_OVERVIEW | los:%s:%s:summoner:%s:overview | 60s (orig 4h) | 60s | STRING | SummonerView |
| 18 | SUMMONER_RANK | los:%s:%s:summoner:%s:rank | 60s (orig 4h) | 60s | STRING | Rank |
| 19 | SUMMONER_RANKS | los:%s:%s:summoner:%s:ranks | 60s (orig 4h) | 60s | STRING | Map<GameQueueType,Rank> |
| 20 | SUMMONER_MASTERIES | los:%s:%s:summoner:%s:masteries | 60s (orig 12h) | 60s | STRING | List<Mastery> |
| 21 | SUMMONER_STATISTICS | los:%s:%s:summoner:%s:statistics:%s (+ filter.toSummonerKey()) | 60s (orig 12h) | 60s | STRING | ProfileStatistics |
| 22 | SUMMONER_ACTIVITY | los:%s:%s:summoner:%s:activity:%s | 60s (orig 12h) | 60s | STRING | ProfileActivity |
| 23 | SUMMONER_MATCHUPS | los:%s:%s:summoner:%s:matchups:%s | 60s (orig 12h) | 60s | STRING | ProfileMatchups |
| 24 | SUMMONER_RANK_HISTORY | los:%s:%s:summoner:%s:rank-history:%s | 1d | 60s | STRING | RankHistory |
| 25 | LEADERBOARD_PAGE | los:leaderboard:page:%s:%s:%s:%s:%s:%s:%s | 60s (orig 1d) | 60s | STRING | LeaderboardPage |
| 26 | LEADERBOARD_COUNT | los:leaderboard:count:%s:%s:%s:%s:%s | 60s | 60s | STRING | count |
| 27 | LEADERBOARD_COUNT_LOCK | los:leaderboard:count-lock:%s:%s:%s:%s:%s | 1m | 60s | STRING | lock |
| 28 | LEADERBOARD_RANK_DISTRIBUTION | los:leaderboard:rank-distribution:%s:%s | 60s (orig 12h) | 60s | STRING | LeaderboardDistribution |
| 29 | LEADERBOARD_TOP_REGIONS | los:leaderboard:top-regions:%s:%s | 60s (orig 12h) | 60s | STRING | LeaderboardDistribution |
| 30 | CONTEXTUAL_LEADERBOARD_SEGMENT | los:leaderboard:%s:%s:%s | 6h | 6h/persist | ZSET | member 16B SHA256(puuid) score mmr |
| 31 | CONTEXTUAL_LEADERBOARD_COUNTS | los:leaderboard:counts:%s:%s | 0 (persist) | persist | HASH | field→count |
| 32 | CONTEXTUAL_LEADERBOARD_SEGMENTS | los:leaderboard:segments | 0 (persist) | persist | SET | segment keys |
| 33 | CONTEXTUAL_LEADERBOARD_ACCESS | los:leaderboard:access | 0 (persist) | persist | HASH | access timestamps |
| 34 | CONTEXTUAL_RECORD_SEGMENT | los:ranking:%s:%s:%s | 6h | 6h/persist | ZSET | member 16B score per metric |
| 35 | CONTEXTUAL_RECORD_SEGMENTS | los:ranking:segments | 0 (persist) | persist | SET | segment keys |
| 36 | CONTEXTUAL_RECORD_ACCESS | los:ranking:access | 0 (persist) | persist | HASH | access |
| 37 | CONTEXTUAL_LEADERBOARD_BUILD | los:leaderboard:building:%s | 10m | 10m | STRING | build lock |
| 38 | CONTEXTUAL_RANKING_BUILD | los:ranking:building:%s | 10m | 10m | STRING | build lock |
| 39 | CHAMPION_STATS | los:champion:%s:stats:%s | 60s (orig 12h) | 60s | STRING | ChampionStatistics |
| 40 | CHAMPION_PAGE | los:champion:%s:page:%s | 60s (orig 1h) | 60s | STRING | champion page |
| 41 | CHAMPION_TIER_LIST | los:champion:tier-list:%s | 60s (orig 1d) | 60s | STRING | ChampionTierList |
| 42-47 | STATUS_* | status:game-queue etc | 10d | persist | STRING/HASH | metrics (STATUS_GAME_QUEUE, GAMES_ANALYZED, TOTAL_SUMMONERS, TOTAL_MASTERIES, RANKS_BY_QUEUE, TRACKED_SUMMONERS) |
30 main keys + 6
STATUS_*+ 4CONTEXTUAL_*persistent = 40+ patterns. The prefixbeebot:lol:is always prepended (RedisKey.of:71).
Redis types
| Type | Keys | Encoding |
|---|---|---|
STRING (JSON) | all los:... except ZSET/HASH/SET | JsonCodec.toJson value |
ZSET | CONTEXTUAL_LEADERBOARD_SEGMENT, CONTEXTUAL_RECORD_SEGMENT | member = 16B SHA256(puuid) (byte[]), score = mmr (double from TierDivisionUtils.getMmr) |
HASH | CONTEXTUAL_LEADERBOARD_COUNTS, ACCESS, CONTEXTUAL_RECORD_* | field→long via hset/hgetAll/hincrBy |
SET | CONTEXTUAL_LEADERBOARD_SEGMENTS, CONTEXTUAL_RECORD_SEGMENTS | sadd/smembers |
Member SHA256 16B and mmr score
For leaderboard/record ZSETs (MongoDB.COMPETITIVE_ID_BYTES=16 MongoDB.java:103):
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash = sha256.digest(puuid.getBytes(StandardCharsets.UTF_8));
byte[] member = Arrays.copyOf(hash, 16); // 16B
double score = TierDivisionUtils.getMmr(tierDivision, lp); // mmr
// IRON_IV=0 +100/div, MASTER=10000, GM=20000, CHALL=30000 + lp
getMmr (TierDivisionUtils.java:78): UNRANKED→-1.
Pipeline operations and helpers
RedisClient exposes:
| Operation | Method | File:line | Notes |
|---|---|---|---|
GET | get(key), get(key, Class), get(key, TypeRef), get(List keys, Class) via mget | :96, :108, :120, :132 | mget pipeline for LeaderboardService |
SET | set(RedisKey, String/T, args), setCached(key,value,ttl), setPersistent | :56, :256, :299 | setex with TEMPORARY_TTL_SECONDS=60 |
CLAIM | claim(RedisKey, value, args) | :64 | set nx ex 60 — for refresh cooldown |
DELETE | delete(key), delete(List) | :149, :159 | |
EXPIRE/TTL/PERSIST | expire, ttl, setPersistent, addPersistentMember | :278, :266, :288 | |
SADD/SMEMBERS/SCARD/SREM | sadd, members, countMembers, removeMember, smembers (with del) | :183, :194, :201, :209, :235 | |
LPUSH/LRANGE/POP | rpush, lrangeAll, popList (transaction) | :175, :217, :224 | |
HGETALL/HSET/HINCRBY/HDEL | getHashLongs, replaceHash, incrementHashIfPresent, setHashLong, removeHashField | :481, :496, :510, :520, :531 | |
ZCARD/ZADD/ZREM/ZREVRANK/ZCOUNT | sortedSetCardinality, addSortedSet, removeSortedSetMember, reverseRanks, countsAbove, existingSortedSets | :457, :361, :375, :423, :440, :345 | pipeline |
BUILD ZSET | buildSortedSet(tmpKey, key, ttl, tmpTtl, source) | :397 | batch 250, rename + expire/persist |
RENAME | publishSortedSet(tmpKey, key, ttl) | :385 | |
PIPELINE EXISTS | existingSortedSets(List keys) | :345 | pipeline.exists |
DBSIZE/MEMORY | dbSize, usedMemory, memoryUsage | :309, :321, :469 | via jedis.info("memory") |
ZSET examples
// Leaderboard — reverse rank (position)
List<Long> ranks = RedisClient.reverseRanks(
List.of("beebot:lol:los:leaderboard:EUW1:RANKED_SOLO_5X5:CHALLENGER"),
List.of(member16B) // byte[]
); // zrevrank
// Count above score
List<Long> counts = RedisClient.countsAbove(
List.of("beebot:lol:los:leaderboard:EUW1:RANKED_SOLO_5X5"),
List.of(15000.0) // mmr
); // zcount (score, +inf)
// Pipeline mget for LeaderboardPage
Map<String, Summoner> map = RedisClient.get(keys, Summoner.class); // mget
// Atomic build
boolean ok = RedisClient.buildSortedSet(tmpKey, key, 60, 60, emitter -> {
for (Entry e : entries) emitter.accept(new SortedSetEntry(member16B, e.mmr()));
});
// addSortedSet → expire tmp → rename → expire/persist
Circuit breaker — 30s
private static final long RETRY_AFTER_FAILURE_MS = 30_000; // :30
private static volatile long disabledUntil; // :32
private static boolean canUseRedis() { return System.currentTimeMillis() >= disabledUntil; } // :545
private static void markUnavailable() { disabledUntil = System.currentTimeMillis() + 30_000; } // :549
private static void markAvailable() { disabledUntil = 0; } // :553
Every catch (Exception) calls markUnavailable(), every success markAvailable(). If Redis is down, for 30s all get/set return null/false without touching the pool — graceful degradation to direct Mongo.
Prefix beebot:lol
// RedisKey.java:67
this.prefix = App.isTesting() ? "beebot_test:lol" : "beebot:lol";
public String of(Object... args) { return String.format(prefix + ":" + pattern, args); }
Example prod: beebot:lol:los:EUW1:EUW1:summoner:abc-puuid:overview (SUMMONER_OVERVIEW with cacheRegion=EUROPE from LeagueShardUtils.cacheRegion :52).
Read-through pattern
// MatchService.java:65 and ProfileService.java:46
Match cached = RedisClient.get(RedisKey.MATCH_DETAIL.of(cacheRegion, shard, gameId), Match.class);
if (cached != null) return ApiResult.ready(cached);
Match stored = MongoDB.findMatch(gameId);
if (stored != null) RedisClient.set(RedisKey.MATCH_DETAIL, stored, cacheRegion, shard, gameId);
return stored;
Same for SUMMONER_OVERVIEW, SUMMONER_STATISTICS (filter.toSummonerKey()), LEADERBOARD_PAGE — always get → miss → Mongo → set.
