从WiFi 4到WiFi 7:你的路由器该升级了吗?一文看懂802.11n/ac/ax/be标准对实际网速的影响
2026/4/25 15:25:20
缓存与数据库的协同工作有三种经典模式:
Cache Aside(旁路缓存):由应用层负责维护缓存与数据库的一致性
Read/Write Through(读写穿透):由缓存层代理数据库读写
Write Behind Caching(写回缓存):写操作只更新缓存,由后台异步线程批量将变更写入数据库
💡 目前绝大多数系统采用
Cache Aside模型,因其简单、可控、易于调试。
在Cache Aside模型中,更新操作通常有两种顺序,但都存在并发风险:
✅为什么选方案二?
虽然仍存在“短暂旧数据返回”的可能,但不会将脏数据写回缓存,最终一致性可保障。
删除缓存失败
极端场景下的不一致
删除缓存 → 更新数据库 → sleep(100ms) → 再次删除缓存@ServicepublicclassUserService{@AutowiredprivateUserMapperuserMapper;@AutowiredprivateRedisTemplate<String,Object>redisTemplate;privatestaticfinalStringUSER_CACHE_KEY="user:";// 查询用户publicUsergetUserById(Longid){Stringkey=USER_CACHE_KEY+id;Useruser=(User)redisTemplate.opsForValue().get(key);if(user!=null){returnuser;}// 缓存未命中,查数据库user=userMapper.selectById(id);if(user!=null){// 设置随机 TTL(防雪崩)longttl=3600+newRandom().nextInt(300);// 1h ~ 1h5minredisTemplate.opsForValue().set(key,user,ttl,TimeUnit.SECONDS);}else{// 防穿透:缓存空值redisTemplate.opsForValue().set(key,"",60,TimeUnit.SECONDS);}returnuser;}// 更新用户(先更新 DB,再删缓存)@TransactionalpublicvoidupdateUser(Useruser){userMapper.updateById(user);// 1. 更新数据库Stringkey=USER_CACHE_KEY+user.getId();redisTemplate.delete(key);// 2. 删除缓存// ✅ 生产建议:若删除失败,可发消息到 MQ 重试}}即使采用正确的一致性策略,仍可能遭遇以下三类高并发场景下的缓存危机:
@ComponentpublicclassBloomFilterService{privateBloomFilter<Long>userIdBloomFilter=BloomFilter.create(Funnels.longFunnel(),1_000_000,0.01);@PostConstructpublicvoidinit(){// 启动时加载所有合法用户 IDList<Long>allUserIds=userMapper.selectAllIds();allUserIds.forEach(userIdBloomFilter::put);}publicbooleanmightExist(LonguserId){returnuserIdBloomFilter.mightContain(userId);}publicvoidaddUserToBloom(LonguserId){userIdBloomFilter.put(userId);}}// 使用示例@ServicepublicclassSafeUserService{@AutowiredprivateBloomFilterServicebloomFilterService;publicUsersafeGetUser(Longid){if(!bloomFilterService.mightExist(id)){returnnull;// 一定不存在,直接返回}returnuserService.getUserById(id);// 走正常缓存流程}}⚠️ 注意:Guava 是单机内存版。分布式环境建议使用RedisBloom 模块或自研分片布隆过滤器。
// 随机 TTL(通用)longbaseTTL=3600;longrandomTTL=baseTTL+newRandom().nextInt(300);redisTemplate.opsForValue().set(key,data,randomTTL,TimeUnit.SECONDS);// 逻辑过期封装类publicstaticclassLogicalCache<T>{privateTdata;privatelongexpireTime;// 毫秒时间戳// getter/setter}// 写入逻辑过期缓存LogicalCache<User>cache=newLogicalCache<>();cache.setData(user);cache.setExpireTime(System.currentTimeMillis()+3600_000);redisTemplate.opsForValue().set("hot:user:"+id,cache);// 读取(配合后台刷新线程)publicUsergetUserWithLogicalExpire(Longid){Stringkey="hot:user:"+id;LogicalCache<User>cache=(LogicalCache<User>)redisTemplate.opsForValue().get(key);if(cache!=null){if(System.currentTimeMillis()>cache.getExpireTime()){refreshUserCacheAsync(id);// 异步刷新}returncache.getData();// 即使过期也返回旧值}returnloadFromDBAndSetCache(id);}publicUsergetUserWithMutex(Longid){Stringkey="user:"+id;Useruser=(User)redisTemplate.opsForValue().get(key);if(user!=null)returnuser;StringlockKey="lock:user:"+id;BooleanisLocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofMillis(500));// 原子加锁,500ms超时if(Boolean.TRUE.equals(isLocked)){try{// 双重检查user=(User)redisTemplate.opsForValue().get(key);if(user!=null)returnuser;user=userMapper.selectById(id);if(user!=null){redisTemplate.opsForValue().set(key,user,3600,TimeUnit.SECONDS);}else{redisTemplate.opsForValue().set(key,"",60,TimeUnit.SECONDS);}returnuser;}finally{redisTemplate.delete(lockKey);// 释放锁}}else{// 未获取锁,短暂等待后重试try{Thread.sleep(50);returngetUserWithMutex(id);}catch(InterruptedExceptione){Thread.currentThread().interrupt();returnnull;}}}✅ 关键:
SET key value NX EX实现原子锁,必须设超时防死锁。
privatefinalCache<Long,User>localCache=Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10,TimeUnit.MINUTES).build();publicUsergetUserMultiLevel(Longid){// 1. 本地缓存Useruser=localCache.getIfPresent(id);if(user!=null&&!"".equals(user))returnuser;// 2. RedisStringredisKey="user:"+id;user=(User)redisTemplate.opsForValue().get(redisKey);if(user!=null){localCache.put(id,user);returnuser;}// 3. DBuser=userMapper.selectById(id);if(user!=null){redisTemplate.opsForValue().set(redisKey,user,3600+newRandom().nextInt(300),SECONDS);localCache.put(id,user);}else{redisTemplate.opsForValue().set(redisKey,"",60,SECONDS);localCache.put(id,newUser());// 空对象标记}returnuser;}| 问题 | 推荐方案 | Java 实现要点 |
|---|---|---|
| 缓存模型 | Cache Aside | 先 update DB → delete cache |
| 缓存穿透 | 空值缓存 + 布隆过滤器 | Guava BloomFilter(单机)或 RedisBloom |
| 缓存雪崩 | 随机 TTL / 逻辑过期 | new Random().nextInt()+LogicalCache |
| 缓存击穿 | 互斥锁 | setIfAbsent(..., Duration)+ 双重检查 |
| 高可用 | 多级缓存 | Caffeine + Redis |
💡核心思想:
缓存不是银弹,没有 100% 一致性。
所有方案都是在一致性、可用性、性能之间做权衡。
根据业务容忍度选择合适策略,才是工程之道。
作者:不会写程序的未来程序员
首发于 CSDN
版权声明:本文为原创文章,转载请注明出处。