12 Critical Issues in Custom Redis Distributed Session Design — Solved
This article details 12 critical technical challenges when building a custom Redis-based distributed session system, covering token security, dual-index design, atomic Lua writes, expiration strategies, multi-device limits, renewal optimization, ThreadLocal cleanup, HA degradation, framework comparison, anomaly detection, and Redis Cluster hash-tag routing.
Interview Self-Test Questions
The article presents 12 interview-style questions with reference answers, each mapped to a chapter in the companion deep-dive article "自研Redis分布式Session全解析".
Q1 : Why does default Servlet Session fail in multi-instance deployment? What are the solution paths? (Chapter 三)
Q2 : Why can't Token use MD5(userId)? How to generate? What does 128-bit entropy mean? (Chapter 五、6.4)
Q3 : Why does a custom solution need token→data and uid→set dual indexes? (Chapter 7.5)
Q4 : Why use Lua scripts when writing token and uid set at login? What's wrong with two-step writes? (Chapter 7.3)
Q5 : Pros/cons of fixed expiry, sliding expiry, dual expiry? How to choose in production? (Chapter 十)
Q6 : How to implement "max N devices per account, evict oldest"? How to implement same-device-type mutual kick? (Chapter 十一)
Q7 : What performance issues arise from EXPIRE on every request? What is the renewal threshold? How to optimize? (Chapter 9.3)
Q8 : Why must ThreadLocal be cleared in afterCompletion? What happens if not? (Chapter 9.2)
Q9 : What to do when Redis crashes? How to degrade? Is data loss during master-replica failover acceptable? (Chapter 十五)
Q10 : How to choose among custom Redis Session, Sa-Token, JWT, Spring Session? (Chapter 十九)
Q11 : How to detect anomalous login (IP change, device change)? How to handle false positives? (Chapter 十二)
Q12 : In Redis Cluster, token and uid set may land in different slots causing Lua CROSSSLOT error. How to solve? (Chapter 15.4)
Reference Answers
Q1: Why does default Servlet Session fail in multi-instance deployment? What are the solution paths?
Root cause: Servlet containers (e.g., Tomcat) store Session in process memory ( StandardManager maintains a ConcurrentHashMap<String, Session>). In multi-instance deployment, load balancing disperses requests to different machines. Login creates session on machine A; subsequent request hits machine B, which lacks the sessionId, treats user as unauthenticated, redirects to login.
Two solution paths:
Sticky Session : Load balancer hashes by sessionId/IP, same user always hits same machine. Pros: Simple. Cons: Machine down loses sessions, scaling uneven, rolling deploy blocked.
Shared Storage : Move session to Redis, all instances share. Pros: Any instance handles any request, true horizontal scaling. Cons: Introduces network I/O.
Why sticky session is a band-aid:
Machine down = immediate session loss — all users pinned to that machine go offline
Scaling uneven — ip_hash distribution fixed, new machines get no old-user traffic
Rolling deploy blocked — restarting machine kicks its users
Traffic imbalance — some IP segments overload their assigned machine
Production choice: shared storage , Redis preferred (in-memory, TTL, rich data structures, mature HA).
Reference: 自研Redis分布式Session全解析 - Chapter 3, Background
Q2: Why can't Token use MD5(userId)? How to generate? What does 128-bit entropy mean?
Three fatal flaws of MD5(userId):
Predictable: userId often auto-increment integer; attacker computes own token, enumerates nearby IDs to steal others' tokens
Same user always same token: Cannot support multi-device login, kick old device — new device login yields same token, old device remains valid
Cannot actively invalidate: Token derived from userId; unless userId changes, token forever valid
Correct generation: SecureRandom + Hex
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static String generate() {
byte[] bytes = new byte[16]; // 128 bits
SECURE_RANDOM.nextBytes(bytes);
// convert to 32-char hex
...
}Why SecureRandom not Random: Random uses linear congruential algorithm; given seed, future sequence predictable. SecureRandom uses system entropy source ( /dev/urandom), cryptographically unpredictable. Session token is a credential; predictability enables enumeration impersonation.
128-bit entropy meaning: Even at 1 trillion attempts/sec (10^12), brute-forcing 2^128 space takes 2^128 / 10^12 ≈ 3.4×10^26 seconds ≈ 10^19 years — far exceeding universe age. 32-char hex token security is sufficient. Contrast 64-bit token (2^64 ≈ 1.8×10^19) which has theoretical risk under distributed attack; production practice starts at 128 bits.
Reference: 自研Redis分布式Session全解析 - 6.4 Entropy & Brute-Force Analysis
Q3: Why does a custom solution need token→data and uid→set dual indexes?
token → session data (forward index) for validation: request carries token, lookup Redis by token to get session (uid, permissions, expiry), complete auth. Core validation path.
uid → token set (reverse index) for multi-device management, solving four needs:
Kick user offline: Know uid, find all tokens, delete one by one — admin ban, password change invalidates all sessions
Limit login devices: Check set size before login, evict oldest if over limit
User-facing session list: Show "currently logged in devices" for user management
Permission change refresh: On permission change, iterate set, batch-update permission field in all user sessions
Why single index insufficient: Only token→session, cannot find all tokens by uid — kick operation doesn't know which tokens to delete, multi-device management impossible. Reverse index is the entry point for "manage sessions by user".
Reference: 自研Redis分布式Session全解析 - 7.5 Why Reverse Index Needed
Q4: Why use Lua scripts when writing token and uid set at login? What's wrong with two-step writes?
Two-step write problem:
// Counterexample: two non-atomic steps
redisson.getBucket("login:token:" + token).set(session, TTL);
// If crash or Redis failure here, token written but uid set not recorded
redisson.getSet("login:uid:" + uid).add(token);Crash in between leaves dirty data: token works (validation passes), but uid set missing it. Consequences:
Kick misses: kickOut iterates uid set to delete tokens, misses this unrecorded token, session stays valid
Multi-device count inaccurate: Device count off by one, limit check wrong
Permission refresh misses: Permission change iterates set, misses this session
Lua script guarantees atomicity:
redis.call('HSET', KEYS[1], ...) -- write token→session
redis.call('EXPIRE', KEYS[1], ARGV[7]) -- set TTL
redis.call('SADD', KEYS[2], ARGV[8]) -- write uid→set
return 1Lua atomicity principle: Redis single-threaded command processing; Lua script executes without interruption by other commands — all commands in script either fully execute or none, no other client requests interleaved. Key to dual-index consistency.
Note: Lua scripts must be short to avoid blocking other requests. Session scripts are tiny (few HSET/SADD), no impact.
Reference: 自研Redis分布式Session全解析 - 7.3 Lua Script Atomic Write
Q5: Pros/cons of fixed expiry, sliding expiry, dual expiry? How to choose in production?
Fixed Expiry : Set TTL=7 days at login, no renewal. Pros: Simple, deterministic lifecycle. Cons: Active users forced logout.
Sliding Expiry : Reset TTL=7 days on every access. Pros: Active users seamless. Cons: Leaked token + continuous access = infinite renewal.
Dual Expiry : Sliding renewal + absolute expiry ceiling. Pros: Balances UX and security. Cons: Slightly more complex.
Production choice: dual expiry :
Sliding renewal ensures active user experience (7 days seamless)
Absolute expiry ceiling (e.g., 30 days) safety net — leaked token usable max 30 days, not forever
Why not sliding only: Leaked token, attacker continuously accesses, TTL keeps resetting, token never expires = permanent credential. Absolute expiry is security floor.
Financial/sensitive scenarios stricter: Sensitive ops (payment, password change) require "30 min inactivity expiry" with no sliding renewal — fixed short TTL. This distinguishes "sensitive session" from "normal session".
Renewal performance optimization: Dual expiry + renewal threshold (renew only when remaining TTL < 1 day), 99% requests read-only, Redis write pressure drops two orders of magnitude.
Reference: 自研Redis分布式Session全解析 - Chapter 10, Core 5: Renewal Strategy Deep Dive
Q6: How to implement "max N devices per account, evict oldest"? How to implement same-device-type mutual kick?
Max N devices (any type): After login, check uid→token set; if over limit, sort by login time, evict oldest:
public void evictIfExceed(Long uid, int max) {
RSet<String> uidSet = redisson.getSet(UID_PREFIX + uid);
if (uidSet.size() <= max) return;
List<SessionInfo> sessions = uidSet.readAll().stream()
.map(t -> redisson.<SessionInfo>getBucket(TOKEN_PREFIX + t).get())
.filter(Objects::nonNull)
.sorted(Comparator.comparing(SessionInfo::getLoginTime))
.toList();
int toEvict = sessions.size() - max;
for (int i = 0; i < toEvict; i++) {
logout(uid, sessions.get(i).getToken());
}
}Read all sessions, sort by loginTime, delete earliest (size - max) entries.
Same-device-type mutual kick: uid→token set bucketed by device (key login:uid:{uid}:{device}), on same-device login, clear old tokens for that device:
String deviceKey = UID_PREFIX + uid + ":" + device;
RSet<String> deviceSet = redisson.getSet(deviceKey);
for (String oldToken : deviceSet.readAll()) {
redisson.getBucket(TOKEN_PREFIX + oldToken).delete();
}
deviceSet.delete();
deviceSet.add(token);Effect: User can have only one session on PC (new PC login kicks old PC), but PC and APP independent, can be online simultaneously.
Strategy selection:
Max N devices (any type) : General, limit total sessions
Same-device-type mutual kick : Strict, "one per type" (e.g., WeChat PC only one)
Combine both : Limit both total and per-type
Reference: 自研Redis分布式Session全解析 - Chapter 11, Multi-Device Kick & Coexistence
Q7: What performance issues from EXPIRE on every request? What is renewal threshold? How to optimize?
Performance issue: Sliding expiry with EXPIRE on every request makes Redis write pressure scale linearly with QPS. 10k QPS = 10k EXPIRE writes/sec. Redis fast but writes add up; EXPIRE updates key expiry metadata, internal overhead.
Renewal threshold optimization: Renew only when remaining TTL below threshold (e.g., 1 day), else skip:
long remainTtl = bucket.remainTimeToLive();
if (remainTtl < Duration.ofDays(1).toMillis()) {
refreshWithAbsolute(token, session, bucket);
}Effect: 99% requests (TTL ample) only GET session, no write; only requests with TTL < 1 day trigger EXPIRE. Redis write pressure drops two orders of magnitude.
Other optimizations:
Pipeline batch renewal: Renew multiple tokens together, reduce network round-trips (batch scenarios)
Async renewal: Renewal doesn't block main request, request thread doesn't wait for EXPIRE return (trade slight consistency for latency)
Key insight: "Renew on every request" is conceptual; engineering adds threshold — don't renew every time, only when "about to expire". Balance of performance and correctness.
Reference: 自研Redis分布式Session全解析 - 9.3 Renewal Threshold Optimization
Q8: Why must ThreadLocal be cleared in afterCompletion? What happens if not?
Consequence of not clearing — session leakage: Thread pool reuses threads. Thread finishes user A's request, ThreadLocal retains A's session. Same thread reused for user B; if interceptor doesn't re-set (e.g., exception path), business reads A's session — user B sees user A's data, severe security incident.
// Correct approach
@Override
public void afterCompletion(...) {
UserContext.clear(); // Must clear
}Use remove() not set(null) : set(null) only sets value to null, Entry still references ThreadLocalMap; thread pool scenario still leaks memory. remove() deletes Entry entirely.
ThreadLocal memory leak principle: ThreadLocalMap Entry = weak-ref key + strong-ref value. Thread pool threads long-lived; without remove, value strongly referenced, never GC'd. Even if key GC'd (weak ref), value remains — classic thread pool + ThreadLocal leak.
Complete guard: afterCompletion calls remove() (normal path) try-finally ensures cleanup on exception paths
Thread pool uses ThreadLocal.withInitial for default value, avoid NPE
Reference: 自研Redis分布式Session全解析 - 9.2 ThreadLocal Memory Leak Prevention
Q9: What to do when Redis crashes? How to degrade? Is data loss during master-replica failover acceptable?
Redis HA deployment:
Master-Replica + Sentinel: Master writes, replicas read; master down triggers Sentinel auto-failover. Sufficient for small/medium scale
Cluster: Sharding + per-shard master-replica. Large-scale horizontal scaling
Master-replica failover data loss acceptable: Redis replication asynchronous — master acknowledges write immediately, doesn't wait for replica. Failover may lose few recent un-replicated keys. For sessions:
Lose a just-logged-in token: user re-logs in
Lose a just-renewed TTL: token expires early, user re-logs in
Consequences acceptable, so session scenario doesn't need strong consistency; default async replication + Sentinel failover sufficient. Forcing strong consistency with WAIT command adds significant latency, not worth it.
Full Redis outage degradation:
Reject writes, keep reads : Login fails, logged-in users still work. Cost: New users cannot login
Local cache fallback : Caffeine L1 cache, read local when Redis down. Cost: No cross-instance consistency
Direct service refusal : Return 503. Cost: Large impact but simple
Practical advice: Session is core dependency, Redis must be HA. Degradation is "insurance", not "normal"; don't overcomplicate architecture for degradation.
Reference: 自研Redis分布式Session全解析 - Chapter 15, High Availability
Q10: How to choose among custom Redis Session, Sa-Token, JWT, Spring Session?
Implementation Cost : Custom (Medium), Sa-Token (Low), JWT (Low), Spring Session (Low)
State : Custom (Redis stateful), Sa-Token (Redis stateful), JWT (Token stateless), Spring Session (Redis stateful)
Active Kick : Custom (Delete key), Sa-Token (Built-in), JWT (Default impossible), Spring Session (Built-in)
Multi-Device Mgmt : Custom (Self-implement), Sa-Token (Built-in), JWT (Need extension), Spring Session (Built-in)
Validation Cost : Custom (Query Redis), Sa-Token (Query Redis), JWT (Local verify), Spring Session (Query Redis)
Customization : Custom (Highest), Sa-Token (Medium), JWT (Medium), Spring Session (Medium)
Selection guide:
Standard needs, want convenience → Sa-Token, it packages the custom approach into easier API
Need stateless, cross-service, no shared storage → JWT (microservices, cross-domain)
Spring ecosystem, minimal config → Spring Session
Strong customization needs (special renewal, risk control integration, multi-tenant session isolation, custom storage structures) → Custom, frameworks become constraints
Key insight: Sa-Token internals essentially implement this article's approach (dual-index, Redis storage, kick) as engineered wrapper. Understanding custom design makes Sa-Token source code trivial. Custom vs framework = "customization flexibility" vs "development efficiency".
Reference: 自研Redis分布式Session全解析 - Chapter 19, Comparative Analysis
Q11: How to detect anomalous login (IP change, device change)? How to handle false positives?
Detection dimensions:
IP Change : Current request IP vs session.loginIp cross-province/country → 2FA or logout
Device Fingerprint Change : User-Agent/fingerprint differs from login → Degrade or notify
Short-term High Frequency : Same token >100 req/sec → Rate limit
Geo Simultaneous Login : Same uid appears in two locations → Risk alert
IP change detection implementation: Use IP library to determine province/city; cross-province triggers alert:
public boolean isIpAbnormal(SessionInfo session, String currentIp) {
if (session.getLoginIp().equals(currentIp)) return false;
Region loginRegion = ipRegionService.locate(session.getLoginIp());
Region currentRegion = ipRegionService.locate(currentIp);
return !loginRegion.getProvince().equals(currentRegion.getProvince());
}False positive principle — lenient not strict: Misjudging normal user as anomalous and force-logout destroys UX. Real products usually only "geo-login notification" (SMS/in-app), not direct kick.
Risk grading:
Low : Cross-city → Log only
Medium : Cross-province → SMS notify
High : Cross-country + device change → 2FA
Critical : Cross-country + high freq + abnormal UA → Force logout
Only clear high-risk triggers force logout, avoid killing normal users (e.g., business trip cross-province shouldn't kick).
Reference: 自研Redis分布式Session全解析 - Chapter 12, Anomalous Login Detection
Q12: In Redis Cluster, token and uid set may land in different slots, Lua reports CROSSSLOT. Solution?
Problem: Redis Cluster distributes keys by CRC16 hash slot. login:token:{token} slot determined by token; login:uid:{uid} slot by uid — likely different slots. Lua requires all keys in same slot, else CROSSSLOT error.
Solution: hash tag. Wrap part of key in {}; Cluster uses only content inside {} for slot calculation:
login:token:{uid}.abc123 // {uid} is hash tag
login:uid:{uid}Both contain {uid}, same slot, same shard, Lua executes normally.
Redisson config: Redisson supports hash tag natively; construct keys as above:
String tokenKey = "login:token:{" + uid + "}." + token; // hash tag {uid}
String uidKey = "login:uid:{" + uid + "}";Trade-off: All tokens for a user land on same shard — that user's session ops concentrated on single shard, no cross-shard parallelism. But per-user session count tiny (few tokens), impact negligible.
Why not WAIT /cross-slot transactions: Cluster mode doesn't support multi-key transactions or Lua across slots. Hash tag is standard Cluster solution for multi-key atomic ops.
Non-Cluster mode: Master-Replica + Sentinel puts all keys on single instance, no CROSSSLOT, Lua works directly. Small systems use Sentinel for simplicity; large scale uses Cluster with hash tag handling.
Reference: 自研Redis分布式Session全解析 - 15.4 Redis Cluster Token Routing
References
自研Redis分布式Session全解析 — companion deep-dive article
JWT深度解析与实战全解析 — JWT stateless approach comparison
Sa-Token认证框架全解析 — engineered wrapper of custom approach
OAuth2与OIDC全解析 — third-party login & SSO
认证授权架构选型全解析 — selection decisions
API接口安全与签名防重放全解析 — token transport security
Redis Official Documentation — Key Expiry, Cluster, Lua, Hash Tag
OWASP Session Management Cheat Sheet — session security best practices
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodeSmart Hoops
A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
