Appearance
源码:labs/java/backend-data/cache-aside-simulation/src/CacheAsideSimulation.java
- 原始路径:
labs/java/backend-data/cache-aside-simulation/src/CacheAsideSimulation.java - 类型:
java
java
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
/**
* Cache-Aside 缓存仿真:穿透 / 击穿 / 雪崩 / 双写一致性。
*
* 对应 docs 06-backend-data/07-cache-strategy-and-pitfalls.md。
* 纯 Java CLI(无外部依赖),模拟 Redis(简化 Map) + MySQL(简化 Map):
*
* 1. Cache-Aside 读路径:读 Cache → Miss → 读 DB → 写回 Cache
* 2. 缓存穿透:查不存在的 key,绕过缓存直击 DB(布隆过滤器可拦截)
* 3. 缓存击穿:热点 key 过期瞬间并发重建(互斥锁只让一个线程回源)
* 4. 缓存雪崩:批量 key 同时过期(随机 TTL 缓解)
* 5. 双写一致性:先删 Cache 后更 DB 的窗口期不一致 vs 先更 DB 后删 Cache
*
* 运行:javac -d out src/CacheAsideSimulation.java && java -cp out CacheAsideSimulation
*/
public class CacheAsideSimulation {
/** 简化 Redis:key -> value(value 可能为 null 表示已删除) */
static final Map<String, String> CACHE = new ConcurrentHashMap<>();
/** 简化 MySQL:key -> value */
static final Map<String, String> DB = new HashMap<>();
static final Random RANDOM = new Random();
static int dbHits = 0; // 统计穿透到 DB 的次数(衡量防护效果)
public static void main(String[] args) throws Exception {
// 造数据
DB.put("user:1", "Alice");
DB.put("user:2", "Bob");
CACHE.put("user:1", "Alice");
System.out.println("========== 1. Cache-Aside 读路径 ==========");
System.out.println(" get(user:1) -> " + cacheAsideGet("user:1") + "(命中缓存)");
System.out.println(" get(user:2) -> " + cacheAsideGet("user:2") + "(Miss 后回源 DB 并写回缓存)");
System.out.println();
System.out.println("========== 2. 缓存穿透:不存在的 key 每次打 DB ==========");
dbHits = 0;
for (int i = 0; i < 1000; i++) cacheAsideGet("user:not_exist");
System.out.println(" 1000 次查询不存在 key,穿透到 DB 次数 = " + dbHits + "(无防护,全量打 DB)");
System.out.println(" 修法:布隆过滤器拦截 or 缓存空值(TTL 短)");
System.out.println();
System.out.println("========== 3. 缓存击穿:热点 key 过期瞬间并发重建 ==========");
CACHE.remove("user:1"); // 模拟热点 key 过期
dbHits = 0;
// 50 个线程同时读热点 key
Thread[] threads = new Thread[50];
for (int i = 0; i < 50; i++) {
threads[i] = new Thread(() -> getWithMutex("user:1"));
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println(" 50 并发读热点 key(互斥锁防护),实际回源 DB 次数 = " + dbHits + "(应为 1)");
System.out.println();
System.out.println("========== 4. 缓存雪崩:批量 key 同时过期 vs 随机 TTL ==========");
demoAvalanche(false); // 固定 TTL
demoAvalanche(true); // 随机 TTL
System.out.println();
System.out.println("========== 5. 双写一致性:先删缓存 vs 先更 DB ==========");
demoDoubleWrite();
System.out.println();
System.out.println("结论:Cache-Aside + 互斥锁防击穿 + 随机 TTL 防雪崩 + 先更 DB 再删缓存(延迟双删)");
}
/** Cache-Aside 读路径(无任何防护,用于演示穿透) */
static String cacheAsideGet(String key) {
String cached = CACHE.get(key);
if (cached != null) return cached;
dbHits++;
String value = DB.get(key); // 回源 DB(不存在返回 null)
if (value != null) CACHE.put(key, value); // 命中才写回缓存
return value;
}
/** 带互斥锁的读:防击穿(Double-Check + 单例锁,只让一个线程回源) */
static String getWithMutex(String key) {
String cached = CACHE.get(key);
if (cached != null) return cached;
synchronized (CacheAsideSimulation.class) {
cached = CACHE.get(key); // Double-Check:别的线程可能已重建
if (cached == null) {
dbHits++;
try { Thread.sleep(10); } catch (InterruptedException ignored) {} // 模拟慢 DB
String value = DB.get(key);
if (value != null) CACHE.put(key, value);
return value;
}
}
return cached;
}
/** 雪崩演示:批量 key 用固定 TTL vs 随机 TTL,看同一时刻过期数量 */
static void demoAvalanche(boolean randomTtl) {
final int n = 1000;
int expireSameMoment = 0;
final long baseTtl = 1000;
final long[] ttls = new long[n];
for (int i = 0; i < n; i++) {
ttls[i] = randomTtl ? baseTtl + RANDOM.nextInt(500) : baseTtl; // 随机抖动 0~500ms
}
// 统计命中"同一个 TTL 时刻"的数量:固定 TTL 全部撞在同一秒
long target = baseTtl;
for (long t : ttls) if (t == target) expireSameMoment++;
System.out.println(" " + (randomTtl ? "随机 TTL" : "固定 TTL")
+ ":同时(同一时刻)过期的 key 数 = " + expireSameMoment + " / " + n
+ (randomTtl ? "(抖动打散,雪崩风险低)" : "(全部撞车,DB 被瞬间打爆)"));
}
/** 双写一致性:演示两种更新顺序的窗口期 */
static void demoDoubleWrite() {
// 场景:缓存有旧值 V1,DB 将更新为 V2
CACHE.put("goods:1", "V1_old");
DB.put("goods:1", "V2_new");
// 错误顺序:先删缓存 → 更新 DB 的间隙,读线程把旧值写回缓存
System.out.println(" [先删缓存后更 DB]");
CACHE.remove("goods:1"); // 1. 删缓存
// —— 窗口期:读线程 T2 读到 DB 旧值 V1(DB 还没更新)——
String stale = DB.get("goods:1"); // 模拟 T2 此刻回源(DB 还是 V1?不,DB 已 V2)
// 更真实的窗口:把"删缓存"和"更 DB"拆开,中间让读线程回源旧值
CACHE.put("goods:1", "V1_stale_from_reader"); // T2 写回旧值
DB.put("goods:1", "V2_new");
System.out.println(" 缓存=" + CACHE.get("goods:1") + " DB=" + DB.get("goods:1")
+ " → 不一致(旧值被读线程写回,需延迟双删兜底)");
// 正确顺序:先更 DB → 再删缓存
System.out.println(" [先更 DB 后删缓存]");
DB.put("goods:2", "V2_new");
CACHE.remove("goods:2"); // 删除缓存,下次读回源新值
System.out.println(" 缓存=" + CACHE.get("goods:2") + "(Miss)DB=" + DB.get("goods:2")
+ " → 下次读回源即新值,不一致窗口极短");
}
}