php redis高并发缓存核心是防穿透、击穿、雪崩:需空值缓存+布隆过滤、随机ttl防雪崩、setnx/lua原子写;优先phpredis扩展,连接须复用并设超时。

PHP 中用 Redis 做高并发缓存,核心不是“能不能连上”,而是“怎么避免缓存穿透、击穿、雪崩,以及如何让 set 和 get 真正扛住瞬时流量”。直说结论:别裸用 redis->get() + redis->set(),必须加锁、设过期、分层兜底。
怎么连 Redis 才不掉坑(Predis vs phpredis)
PHP 有两个主流扩展:phpredis(C 扩展,性能高,推荐)和 Predis(纯 PHP,调试友好但慢 20%+)。线上高并发场景优先选 phpredis;若用 Docker 或共享主机没权限装扩展,才退到 Predis。
常见错误是用 Predis\Client 每次 new 一个实例——它默认不复用连接,QPS 上千就卡在 TCP 握手。正确做法是单例或容器内复用:
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 0.5, // 必设!否则阻塞超久
'read_write_timeout' => 0.3,
]);
用 phpredis 更简单,直接 new Redis() 后调 connect(),记得开 setOption(REDIS_OPT_PREFIX, 'myapp:') 避免 key 冲突。
立即学习“PHP免费学习笔记(深入)”;
缓存读写怎么防雪崩(key 失效时间 + 随机抖动)
所有热门 key 设相同过期时间(比如整点刷新),Redis 在那一秒集体失效,后端 DB 瞬间被打爆——这就是缓存雪崩。
解决办法不是“永不过期”,而是“主过期时间 + 随机偏移”:
$ttl = 3600 + rand(1, 300); // 基础 1 小时 + 0~5 分钟抖动- 用
set($key, $value, ['ex' => $ttl])(phpredis)或setex($key, $ttl, $value) - 如果业务允许,对关键 key 加二级缓存(如 APCu 存 1 分钟),Redis 挂了也能撑几秒
注意:别用 expire($key, $ttl) 单独设过期——它和 set 不是原子操作,中间若崩溃,key 就永久存在了。
怎么避免缓存穿透(空值也缓存 + 布隆过滤器)
恶意请求查 user_id=999999999 这种根本不存在的 ID,Redis 查不到,穿透到 DB,DB 也查不到,还无法缓存结果——大量这类请求直接拖垮 DB。
最简方案:查不到时,把 null 或特殊标记(如 "__MISS__")以短 TTL(比如 60 秒)写入 Redis:
if ($data === false) {
$redis->setex($key, 60, '__MISS__');
return null;
}
更彻底的方案是加布隆过滤器(Bloom Filter):启动时把所有合法 ID 加入过滤器,请求先过滤再查缓存。可用 redisbloom 模块或 bitSet + bitOp 自建,但要注意误判率(通常设 1%)和扩容成本。
别漏掉:前端/网关层也要做参数校验,ID 格式不对(如字母混入)直接 400 拦截,不进缓存层。
并发写缓存怎么防覆盖(setnx + Lua 脚本原子更新)
两个请求同时发现缓存失效,都去查 DB,然后都执行 set($key, $data)——后写的覆盖前写的,可能把旧数据刷回去。
标准解法是 setnx(set if not exists)抢锁:
$lockKey = "lock:{$key}";
if ($redis->setnx($lockKey, 1)) {
$redis->expire($lockKey, 10); // 锁 10 秒防死锁
$data = $db->query(...); // 查库
$redis->setex($key, 3600, json_encode($data));
$redis->del($lockKey);
} else {
// 等 100ms 后重试,或直接回源(看业务容忍度)
usleep(100000);
return $this->getCache($key);
}
但这个逻辑有竞态风险(比如 expire 失败)。更高阶做法是用 Lua 脚本保证原子性:
lua_script = "if redis.call('exists', KEYS[1]) == 0 then redis.call('setex', KEYS[1], ARGV[1], ARGV[2]); return 1 else return 0 end"
$redis->eval($lua_script, [$key, $ttl, $value], 1);
注意:Lua 脚本里不能调 DB,只能做缓存层原子操作;且脚本执行时间别超 50ms,否则阻塞其他命令。
真正难的不是连 Redis 或写 set,是设计缓存生命周期、应对节点故障时的降级策略、以及监控 redis-cli --latency 和 INFO commandstats 里的 cmdstat_get 耗时分布——这些细节,往往比语法多花三倍时间调优。











