hyperf 缓存架构方案大全
---1)缓存穿透、击穿、雪崩先分清1.1缓存穿透查一个根本不存在的数据 现象 比如一直查 user:99999999Redis没有DB也没有。每次都打DBDB被打爆。 解决1. 缓存空值短 TTL2. 布隆过滤器先拦截不可能存在的ID 代码缓存空值HyPerf?php namespace App\Service;use Hyperf\Di\Annotation\Inject;use Hyperf\Redis\RedisFactory;use Hyperf\DbConnection\Db;class UserService{#[Inject]protected RedisFactory$redisFactory;publicfunctiongetUserById(int$id): ?array{$redis$this-redisFactory-get(default);$keyuser:{$id};$cached$redis-get($key);// 命中正常数据if($cached!false$cached!__NULL__){returnjson_decode($cached,true);}// 命中空值缓存if($cached__NULL__){returnnull;}// 查DB$rowDb::table(users)-where(id,$id)-first();if(!$row){// 不存在也缓存防穿透TTL要短$redis-setex($key,60,__NULL__);returnnull;}$data(array)$row;$redis-setex($key,600, json_encode($data, JSON_UNESCAPED_UNICODE));return$data;}}---1.2缓存击穿某个热点Key刚好过期 现象 一个超热点 key比如商品详情瞬间过期大量请求一起打DB。 解决1. 互斥锁同一时刻只放一个请求去查DB并回填缓存2. 热点 key 永不过期 后台异步刷新 代码互斥锁防击穿 publicfunctiongetHotProduct(int$id): ?array{$redis$this-redisFactory-get(default);$keyproduct:{$id};$lockKeylock:product:{$id};$cached$redis-get($key);if($cached!false){returnjson_decode($cached,true);}$tokenbin2hex(random_bytes(8));$locked$redis-set($lockKey,$token,[NX,PX5000]);if($locked){try{$rowDb::table(products)-where(id,$id)-first();if(!$row){$redis-setex($key,30,__NULL__);returnnull;}$data(array)$row;$redis-setex($key,300, json_encode($data, JSON_UNESCAPED_UNICODE));return$data;}finally{$luaif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;$redis-eval($lua,[$lockKey,$token],1);}}// 没抢到锁短暂等待再读缓存避免直冲DB usleep(50000);$retry$redis-get($key);return$retryfalse? null:json_decode($retry,true);}---1.3缓存雪崩大量Key同一时间失效 or Redis挂了 现象 大量 key 同时过期流量全压到 DB或者 Redis 故障所有请求直冲下游。 解决1. TTL加随机值错峰过期2. 多级缓存本地缓存 Redis3. 限流降级兜底默认值、熔断 代码TTL错峰$baseTtl600;$randrandom_int(0,120);// 随机0-120秒$redis-setex($key,$baseTtl$rand,$value);---2)缓存一致性先删缓存还是先更DB 结论先说 在常见 Cache-Aside 模式里优先用先更新 DB再删缓存。 不要“先删缓存再更DB”容易把旧值回填进去。 推荐流程1. 更新 DB事务内2. 删除缓存3. 可选延迟二次删除处理并发读脏 代码先更DB再删缓存 延迟双删 publicfunctionupdateUserName(int$id, string$name): void{$redis$this-redisFactory-get(default);$keyuser:{$id};Db::transaction(function()use($id,$name){Db::table(users)-where(id,$id)-update([name$name]);});// 第一次删缓存$redis-del($key);// 延迟二次删除可放队列任务里 go(function()use($redis,$key){usleep(200000);// 200ms$redis-del($key);});}---3)热点 Key 问题一个Key流量太猛 现象 例如 product:1001 QPS 超高单 key 把 Redis 单线程打满。 常见办法1. 本地缓存兜一层每台应用先吃掉一部分读流量2. 热点永不过期 异步刷新3. 热点key拆分业务允许时做多副本 key4. 提前预热热点数据 代码热点多副本分摊读随机 publicfunctiongetHotValue(string$bizKey): ?string{$redis$this-redisFactory-get(default);$bucketrandom_int(0,9);//10个副本$keyhot:{$bizKey}:{$bucket};return$redis-get($key)?:$redis-get(hot:{$bizKey}:0);}publicfunctionsetHotValue(string$bizKey, string$value): void{$redis$this-redisFactory-get(default);for($i0;$i10;$i){$redis-setex(hot:{$bizKey}:{$i},300,$value);}}---4)本地缓存进程内vs 分布式缓存 本地缓存进程内 - 快内存直取 - 不走网络 - 但每个进程一份不共享一致性弱 分布式缓存Redis - 多实例共享数据统一 - 容量大 - 但有网络开销Redis可能成为瓶颈点 实战建议两级缓存 本地缓存1-3秒 Redis几十秒到几分钟 这套对热点读特别有效。 代码两级缓存简化版?php namespace App\Service;use Hyperf\Di\Annotation\Inject;use Hyperf\Redis\RedisFactory;class LocalRedisCacheService{#[Inject]protected RedisFactory$redisFactory;private array$local[];// 演示用生产可用Swoole\Table或Caffeine风格实现 publicfunctionget(string$key): ?string{$nowtime();//1)先查本地缓存if(isset($this-local[$key])$this-local[$key][expire_at]$now){return$this-local[$key][value];}//2)再查Redis$redis$this-redisFactory-get(default);$val$redis-get($key);if($valfalse){returnnull;}//3)回填本地短缓存2秒$this-local[$key][value$val,expire_at$now2,];return$val;}}--- 你可以直接按这个顺序落地空值缓存防穿透 → 互斥锁防击穿 → TTL错峰防雪崩 → 更新DB后删缓存保一致 → 本地Redis两级缓存抗热点。这套是线上最常用的一条主线。