Nginx代理缓存配置
Nginx代理缓存配置
引言
反向代理每次都回源取页面,后端压力大、首屏慢。Nginx自带proxy_cache,把后端响应存到本地磁盘,命中时直接返回,既减轻源站又加快响应,适合静态资源和不常变的页面。
定义缓存区
http块里先声明缓存目录和大小:
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=mycache:10m max_size=1g inactive=7d;
- keys_zone=mycache:10m 是缓存名和索引内存;
- max_size=1g 磁盘上限;
- inactive=7d 7天没访问自动清理。
代理站点启用缓存
server或location块:
location / {
proxy_cache mycache;
proxy_cache_key $host$uri$is_args$args;
proxy_cache_valid 200 304 1h;
proxy_cache_valid 404 1m;
proxy_cache_methods GET HEAD;
proxy_pass http://backend;
}
200/304缓存1小时,404只存1分钟。
命中状态标识
加响应头方便排查:
add_header X-Cache-Status $upstream_cache_status;
HIT命中、MISS未命中、EXPIRED过期、BYPASS绕过。
绕过缓存的场景
登录后动态内容不缓存:
proxy_no_cache $http_cookie;
proxy_cache_bypass $http_cookie;
带cookie时不读不写缓存。
主动清除缓存
# 配置删除接口需要第三方模块或手动删目录
rm -rf /data/nginx/cache/*
更新内容后清对应目录,或按URL前缀匹配删除。
注意事项
- 缓存目录要先mkdir并给nginx读写权限;
- 带POST的接口不要缓存;
- 频繁变动的页面缓存时间设短或不缓存;
- 缓存键要带参数,避免不同query串内容;
- 配置改完nginx -t校验再reload;
- 大文件下载用分片缓存更稳妥。
总结
proxy_cache_path在http块定义缓存区,location里proxy_cache启用、proxy_cache_valid设有效期、proxy_cache_key拼缓存键,用X-Cache-Status看命中。静态和低频页面开缓存扛压力,动态登录内容用cookie绕过,更新后及时清理。
Redis哈希结构场景
Redis哈希结构场景
引言
Redis Hash(哈希)适合存储对象类数据,类似数据库单行记录。相比把对象序列化为JSON字符串存储,Hash可以单独读写某个字段,不用整体读取修改,适合用户资料、商品基础信息这类场景。
Hash基础命令
# 设置单个字段
HSET user:100 name "test" age 20
# 获取单个字段
HGET user:100 name
# 获取全部字段
HGETALL user:100
# 删除字段
HDEL user:100 age
PHP操作示例
<?php
$redis = new Redis();
$redis->connect('127.0.0.1',6379);
$key = 'user:100';
$redis->hMSet($key,[
'name'=>'admin',
'phone'=>'13800138000'
]);
$info = $redis->hGetAll($key);
print_r($info);
适用与不适用场景
✅ 适合:用户资料、商品基础属性,需要局部更新字段
❌ 不适合:字段数量超大、海量对象;大量独立统计计数,推荐单独key
注意事项
Redis Hash有底层ziplist编码,字段少、值小的时候内存占用极低。当字段数量或者value超过阈值,会转为hash表,内存占用上升。不要在单个hash里面存放成千上万的字段。
总结
Hash是Redis高频数据结构,适合存储结构化小对象,可以局部读写字段,避免JSON字符串每次全量读写。根据业务字段数量合理选择,不要滥用。
Redis分布式锁实战
Redis分布式锁实战
引言
多服务器部署项目时,多个服务实例同时操作同一资源,会出现并发争抢问题。Redis分布式锁可以保证同一时间只有一个请求执行临界代码,常用于订单扣库存、定时任务防重复执行场景。
实现原理
利用RedisSET key value NX EX命令:NX仅当key不存在才设置,EX设置过期时间。原子命令可以防止锁成功后服务宕机导致死锁。解锁时需要校验锁的值,防止误删除别人的锁。
PHP代码示例
<?php
$redis = new Redis();
$redis->connect('127.0.0.1',6379);
$lockKey = 'order:lock:1001';
$lockVal = uniqid();
// 获取锁,过期10秒
$ok = $redis->set($lockKey,$lockVal,['NX','EX'=>10]);
if($ok){
try{
// 临界区业务代码
echo "获取锁,执行业务";
}finally{
// lua脚本原子解锁,判断value一致才删除
$lua = 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end';
$redis->eval($lua,[$lockKey,$lockVal],1);
}
}else{
echo "获取锁失败,资源被占用";
}
注意事项
- 锁必须设置过期时间,防止程序崩溃死锁;
- 解锁必须用Lua脚本,保证判断和删除原子操作;
- 业务执行时间不能超过锁过期时间,可增加锁续期逻辑;
- 单机Redis存在单点故障,高可用场景用Redis集群或Redlock。
总结
Redis分布式锁简单易用,适合中小型Web项目并发控制。理解原子操作、锁过期、安全解锁三个关键点,避免网上简易版本带来的并发漏洞。
Redis缓存穿透、击穿、雪崩区别和解决
前言
用Redis做缓存,经常听到三个词:缓存穿透、缓存击穿、缓存雪崩。很多人搞不清区别,本文一次讲明白,附解决方法。
一、三个概念对比
| 问题 | 是什么 | 谁在访问 | 影响 |
|---|---|---|---|
| 缓存穿透 | 查一个不存在的数据,缓存没有,每次都去查数据库 | 恶意攻击/大量无效请求 | 数据库压力大 |
| 缓存击穿 | 一个热点key突然过期,大量请求同时打过去 | 正常用户 | 数据库瞬间压力大 |
| 缓存雪崩 | 大量key同时过期,或者Redis挂了 | 正常用户 | 数据库直接崩 |
二、缓存穿透
是什么
黑客故意查一个数据库里根本不存在的数据,缓存里也没有,每次请求都打到数据库。
例子: 查用户ID=-1,数据库里没有,缓存里也没有,每次都查数据库。
危害
数据库被大量无效请求打挂。
解决方法
方法1:缓存空值
数据库查不到,也往缓存里写一个空值,设个短过期时间。
// 先查缓存
$user = $redis->get("user:$id");
if ($user === false) {
// 查数据库
$user = mysqli_query("SELECT * FROM users WHERE id = $id");
if (!$user) {
// 查不到也缓存,设60秒过期
$redis->setex("user:$id", 60, "null");
} else {
$redis->setex("user:$id", 3600, $user);
}
}
方法2:布隆过滤器
在缓存前面加一层布隆过滤器,快速判断这个ID存在不存在。
不存在的直接返回,不用查数据库。
方法3:接口校验
在接口层就做参数校验,比如用户ID必须大于0,不合法的直接拒绝。
三、缓存击穿
是什么
一个热点key(比如首页热点新闻),突然过期了,这时候刚好有大量用户同时访问,所有请求都打到数据库。
和穿透的区别: 这个key本来是存在的,只是过期了。
危害
数据库瞬间压力暴增。
解决方法
方法1:互斥锁
只让一个线程去查数据库,其他线程等一下。
$lockKey = "lock:user:$id";
// 尝试加锁
if ($redis->setnx($lockKey, 1)) {
// 加锁成功,查数据库
$user = mysqli_query("SELECT * FROM users WHERE id = $id");
$redis->setex("user:$id", 3600, $user);
$redis->del($lockKey);
} else {
// 加锁失败,等一下再读缓存
sleep(100);
$user = $redis->get("user:$id");
}
方法2:热点key不过期
热点数据不设过期时间,后台定时更新。
方法3:提前预热
在热点数据快过期前,主动后台更新缓存。
四、缓存雪崩
是什么
大量key在同一时间过期,或者Redis整个挂了,所有请求都打到数据库。
和击穿的区别:
- 击穿:一个key过期
- 雪崩:大量key同时过期
危害
数据库直接被打挂,整个服务不可用。
解决方法
方法1:过期时间加随机值
不要所有key都设3600秒过期,加个随机数错开。
// 基础3600秒,加0-600秒随机
$expire = 3600 + rand(0, 600);
$redis->setex("user:$id", $expire, $data);
方法2:Redis集群
Redis挂了还有备用的,不要单点。
方法3:服务降级
数据库扛不住的时候,直接返回默认数据或者报错,不要让请求都打过去。
方法4:热点数据永不过期
核心数据不设过期,后台异步更新。
五、总结对比
| 问题 | 原因 | 解决 |
|---|---|---|
| 缓存穿透 | 查不存在的数据 | 缓存空值 + 布隆过滤器 |
| 缓存击穿 | 热点key过期 | 互斥锁 + 热点永不过期 |
| 缓存雪崩 | 大量key同时过期 | 过期时间加随机 + Redis集群 |
六、常见坑
坑1:把穿透和击穿搞混
- 穿透:数据本来就不存在
- 击穿:数据存在,但缓存过期了
坑2:缓存空值不设过期时间
如果不设过期时间,以后这个数据真的有了,缓存还是空的。
解决: 空值也要设过期时间(比如60秒)。
坑3:互斥锁死锁
加了锁没释放,其他线程一直等。
解决: 锁也要设过期时间。
坑4:随机值加太小
所有key还是差不多同时过期。
建议: 随机范围至少几百秒。
总结
记住:
- 穿透:查不存在的数据 → 缓存空值 + 布隆过滤器
- 击穿:热点key过期 → 互斥锁 + 热点永不过期
- 雪崩:大量key同时过期 → 过期时间加随机 + Redis集群
最简单的防雪崩:
$expire = 3600 + rand(0, 600);
$redis->setex($key, $expire, $data);
遇到问题加QQ23979811 协助处理
Redis 持久化机制
Redis 持久化机制
Redis 是内存数据库,断电数据就没了,所以需要持久化。
1. 为什么要持久化
- Redis 数据在内存中
- 服务器断电/重启数据丢失
- 持久化把数据存到磁盘
- 重启后可以恢复数据
2. RDB 快照
原理: 定时把内存数据全量保存到磁盘。
优点:
- 恢复速度快
- 文件小
- 适合备份
缺点:
- 可能丢失最后一次快照后的数据
- fork子进程可能阻塞
配置:
# redis.conf
save 900 1 # 900秒内1个key变化就保存
save 300 10 # 300秒内10个key变化
save 60 10000 # 60秒内10000个key变化
手动触发:
# 前台保存(阻塞)
redis-cli save
# 后台保存(不阻塞)
redis-cli bgsave
3. AOF 日志
原理: 记录每个写命令,重启时重放。
优点:
- 数据更安全(最多丢1秒)
- 日志可读
缺点:
- 文件比 RDB 大
- 恢复速度慢
配置:
# redis.conf
appendonly yes # 开启AOF
appendfilename "appendonly.aof"
# 同步策略
appendfsync everysec # 每秒同步(推荐)
# appendfsync always # 每次写都同步
# appendfsync no # 交给操作系统
AOF 重写:
# 手动触发AOF重写
redis-cli bgrewriteaof
4. RDB vs AOF 对比
| 对比项 | RDB | AOF |
|---|---|---|
| 数据安全 | 可能丢数据 | 更安全 |
| 文件大小 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 性能影响 | fork阻塞 | 较小 |
| 适用场景 | 备份/容灾 | 数据安全要求高 |
5. 混合持久化
Redis 4.0+ 支持混合持久化:
- AOF 重写时前半段用 RDB 格式
- 后半段增量用 AOF 格式
- 结合两者优点
配置:
aof-use-rdb-preamble yes
6. 生产建议
1)数据不敏感用 RDB
- 缓存场景
- 丢点数据没关系
2)数据重要用 AOF
- 每秒同步
- 最多丢1秒数据
3)生产环境推荐
- 同时开 RDB + AOF
- 混合持久化
- 定期备份 RDB 文件
7. 数据恢复
RDB 恢复:
# 把 dump.rdb 放到数据目录
# 重启 Redis 自动加载
AOF 恢复:
# 开启 appendonly yes
# 重启自动重放AOF日志
8. 常见问题
1)fork 阻塞怎么办
- 数据量大 fork 慢
- 用 AOF 代替
- 减少单实例数据量
2)AOF 文件太大
- 定期触发重写
- 设置自动重写阈值
3)恢复慢怎么办
- 用 RDB 恢复快
- 混合持久化更优
总结
Redis 持久化要点:
- RDB 是快照,恢复快但可能丢数据
- AOF 是日志,数据安全但文件大
- 生产建议同时开 RDB + AOF
- 混合持久化结合两者优点
- 缓存用 RDB,重要数据用 AOF
- 定期备份持久化文件
- fork 阻塞要关注数据量
持久化是 Redis 生产必备配置,一定要重视。
Redis 过期策略详解
Redis 过期策略详解
Redis 设置了过期时间后,怎么删除过期的 key?这就涉及过期策略。
1. 设置过期时间
# 设置过期时间(秒)
set name "张三" ex 60
# 给已有key设置过期时间
expire name 60
# 查看剩余时间
ttl name
# 移除过期时间
persist name
2. 两种过期策略
1)惰性删除
- 等到访问这个 key 的时候才检查是否过期
- 过期了就删除,返回 nil
- 优点:对 CPU 友好
- 缺点:对内存不友好,很多过期 key 没被访问就占着内存
2)定期删除
- 每隔一段时间,随机检查一些 key
- 发现过期的就删除
- 优点:定期清理,控制内存
- 缺点:随机抽查可能漏
Redis 实际是两种策略结合用。
3. 内存淘汰策略
当内存满了,写入新数据时怎么办?
# 查看当前淘汰策略
config get maxmemory-policy
常用淘汰策略:
- noeviction: 不淘汰,写入报错(默认)
- allkeys-lru: 所有 key 中淘汰最久没用的
- allkeys-random: 所有 key 中随机淘汰
- volatile-lru: 只在设置了过期时间的 key 中淘汰最久没用的
- volatile-random: 只在设置了过期时间的 key 中随机淘汰
- volatile-ttl: 淘汰即将过期的 key
生产推荐:allkeys-lru
4. LRU 和 LFU
- LRU(最近最少使用): 淘汰最久没访问的
- LFU(最不经常使用): 淘汰访问次数最少的
Redis 4.0 开始支持 LFU。
5. 实际使用建议
-
设置合理的 maxmemory
- 不要让 Redis 把内存占满
- 一般设为服务器内存的 70%
-
选择合适的淘汰策略
- 纯缓存场景:allkeys-lru
- 有持久化数据:volatile-lru
-
key 一定要设过期时间
- 重要数据不要设过期
- 缓存数据都要设过期时间
-
监控内存使用
- 用 info memory 查看
- 关注 used_memory 指标
6. 查看内存信息
# 查看内存使用
info memory
# 查看key数量
dbsize
# 查看大key
--bigkeys
总结
Redis 过期策略要点:
- 惰性删除:访问时才删
- 定期删除:定期抽查清理
- 两种策略结合使用
- 内存满了要配置淘汰策略
- 生产推荐 allkeys-lru
- 缓存数据都要设过期时间
- 监控内存使用情况
搞懂过期策略,Redis 用起来才不踩坑。
Redis 缓存穿透解决
Redis 缓存穿透解决
缓存穿透是指查询不存在的数据,请求都打到数据库,可能被攻击。
1. 什么是缓存穿透
- 缓存里没有这个数据
- 数据库里也没有
- 每次请求都查数据库
- 有人恶意攻击数据库就挂了
2. 和其他问题的区别
- 穿透: 查不存在的数据
- 击穿: 热点key过期
- 雪崩: 大量key同时过期
3. 解决方案
方案一:缓存空值
function getUser($id) {
global $redis, $db;
$key = "user:".$id;
$user = $redis->get($key);
if ($user !== false) {
return json_decode($user, true);
}
// 查数据库
$user = $db->query("SELECT * FROM users WHERE id=$id");
if ($user) {
// 存正常数据
$redis->setex($key, 3600, json_encode($user));
} else {
// 存空值,短时间过期
$redis->setex($key, 60, "null");
}
return $user;
}
方案二:布隆过滤器
- 所有存在的数据存在布隆过滤器里
- 查询前先过布隆过滤器
- 不存在直接返回,不查数据库
方案三:接口校验
- 前端做参数校验
- 比如id必须大于0
- 不合法的直接拒绝
4. 代码示例
// 简单防穿透
if ($id <= 0) {
return null;
}
// 先查缓存
$user = $redis->get("user:".$id);
if ($user === "null") {
return null;
}
if ($user) {
return json_decode($user, true);
}
// 查数据库
$user = $db->query("SELECT * FROM users WHERE id=$id");
// 存缓存
if ($user) {
$redis->setex("user:".$id, 3600, json_encode($user));
} else {
$redis->setex("user:".$id, 60, "null");
}
return $user;
5. 最佳实践
- 空值缓存,短时间过期
- 重要接口加布隆过滤器
- 前端做参数校验
- 数据库做限流保护
总结
缓存穿透解决要点:
- 查不到也存空值
- 空值过期时间短
- 用布隆过滤器
- 做好接口校验
高并发系统,缓存穿透一定要防。
Redis 缓存雪崩解决
Redis 缓存雪崩解决
缓存雪崩是指大量缓存同时失效,请求全部打到数据库,数据库直接挂掉。
1. 什么是缓存雪崩
原理:
- 大量 key 同时过期
- 请求都查数据库
- 数据库压力瞬间暴增
- 数据库扛不住挂掉
和缓存穿透的区别:
- 穿透:查询不存在的数据
- 雪崩:大量缓存同时失效
2. 产生原因
1)缓存大面积过期:
- 大量数据设置了相同过期时间
- 到时间一起失效
2)Redis 宕机:
- Redis 服务挂了
- 所有请求都走数据库
3. 解决方案
方案一:过期时间加随机值
// 不要设置相同过期时间
$expire = 3600 + rand(0, 600);
$redis->setex("key", $expire, $value);
加随机值,让 key 分散过期,不会同时失效。
方案二:缓存永不过期
- 热点数据不设置过期时间
- 更新数据时主动更新缓存
- 异步更新缓存
方案三:Redis 高可用
- 搭建 Redis 集群
- 主从复制
- 哨兵模式
- 一台挂了还有其他的
方案四:熔断降级
- 请求量太大时直接返回默认值
- 不让请求打到数据库
- 保护数据库
4. 代码示例
function getData($id) {
global $redis, $db;
$key = "data:".$id;
$data = $redis->get($key);
if ($data) {
return json_decode($data, true);
}
// 查数据库
$data = $db->query("SELECT * FROM table WHERE id=$id");
// 缓存,过期时间加随机值
$expire = 3600 + rand(0, 600);
$redis->setex($key, $expire, json_encode($data));
return $data;
}
5. 最佳实践
- 过期时间加随机值
- 搭建 Redis 集群
- 热点数据永不过期
- 加熔断降级机制
- 数据库做限流
总结
缓存雪崩解决要点:
- 过期时间加随机值,不要同时失效
- Redis 做主从高可用
- 热点数据不设置过期
- 做好降级兜底
做高并发系统,缓存雪崩一定要防。
Redis 常用数据类型
Redis 常用数据类型
Redis 不只是缓存,它有 5 种常用数据类型,用对了能解决很多问题。
1. String(字符串)
最基本的类型,一个 key 对应一个 value。
# 设置
SET name "张三"
# 读取
GET name
# 自增(计数器)
INCR page_view
# 设置过期时间(秒)
EXPIRE token 3600
# 查看过期时间
TTL token
适用场景: 缓存、计数器、分布式锁。
2. Hash(哈希)
适合存对象。
# 设置字段
HSET user:1 name "张三" age 25
# 获取字段
HGET user:1 name
# 获取所有字段
HGETALL user:1
# 删除字段
HDEL user:1 age
适用场景: 用户信息、商品信息。
3. List(列表)
有序队列,先进先出。
# 从左边插入
LPUSH messages "消息1"
LPUSH messages "消息2"
# 从右边插入
RPUSH messages "消息3"
# 从左边弹出
LPOP messages
# 查看列表
LRANGE messages 0 -1
适用场景: 消息队列、最新消息列表。
4. Set(集合)
无序不重复。
# 添加元素
SADD tags "PHP" "MySQL" "Linux"
# 查看所有元素
SMEMBERS tags
# 判断是否存在
SISMEMBER tags "PHP"
# 交集
SINTER set1 set2
适用场景: 标签、共同好友、去重。
5. ZSet(有序集合)
每个元素带分数,按分数排序。
# 添加元素
ZADD rank 100 "张三"
ZADD rank 90 "李四"
ZADD rank 80 "王五"
# 查看排行(从高到低)
ZREVRANGE rank 0 -1 WITHSCORES
# 获取排名
ZREVRANK rank "张三"
适用场景: 排行榜、热度排序。
总结
| 类型 | 特点 | 适用场景 |
|---|---|---|
| String | 简单键值 | 缓存、计数器 |
| Hash | 存对象 | 用户信息 |
| List | 有序队列 | 消息队列 |
| Set | 不重复 | 标签、去重 |
| ZSet | 带分数排序 | 排行榜 |
Redis 用熟了,能解决很多业务问题。
Redis 缓存雪崩解决
Redis 缓存雪崩解决方案
缓存雪崩是 Redis 最常见的线上事故之一,搞懂怎么预防,关键时刻能救命。
1. 什么是缓存雪崩
大量缓存同时过期,或者 Redis 整个挂了,所有请求都打到数据库,数据库瞬间压力爆炸,直接崩溃。
就像雪崩一样,一层压一层,全完了。
2. 和缓存穿透、击穿的区别
穿透:查不存在的数据,缓存挡不住,一直打数据库。
击穿:一个热点 key 突然过期,瞬间大量请求打数据库。
雪崩:大量 key 同时过期,或者 Redis 挂了,整个数据库被打垮。
3. 解决方案一:过期时间加随机值
最常用的方法。不要所有 key 都设一样的过期时间,在基础时间上加随机数:
// 不好:所有 key 都 1 小时过期
$redis->setex('product:1', 3600, $data);
// 好:基础 1 小时,加 0-600 秒随机
$ttl = 3600 + rand(0, 600);
$redis->setex('product:1', $ttl, $data);
这样过期时间分散开,不会一起失效。
4. 解决方案二:热点数据永不过期
热点数据不要主动设过期,后台异步更新:
// 商品详情这种热点数据,不设过期
$redis->set('product:1', $data);
// 后台定时任务更新
// 不依赖过期时间,由程序控制刷新
5. 解决方案三:Redis 高可用
Redis 自己要高可用,不能单点:
- 主从架构 + 哨兵,主挂了自动切从
- Redis Cluster 集群,分片存储,一个节点挂了不影响全局
# Redis Sentinel 哨兵配置
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
6. 解决方案四:熔断降级
数据库撑不住的时候,要能自动降级:
- 限流:用令牌桶、漏桶算法,限制请求量
- 降级:返回默认数据或者"系统繁忙"
- 熔断:数据库连续报错多了,直接不访问数据库
常用工具:Sentinel、Hystrix。
7. 解决方案五:多级缓存
不要只靠 Redis 一层缓存:
用户请求 → 浏览器缓存 → CDN → Nginx 本地缓存 → Redis → 数据库
多级缓存,就算 Redis 挂了,前面还有 Nginx 缓存挡着。
8. 应急处理
真出问题了怎么办:
- 先限流,别让数据库被打死
- 恢复 Redis 服务
- 重新预热热点数据
- 慢慢放开流量
总结
缓存雪崩解决方案:
- 过期时间加随机值,别一起失效
- 热点数据永不过期,后台更新
- Redis 做高可用集群
- 加熔断限流兜底
- 多级缓存层层防护
记住这几点,缓存雪崩就不怕了。
Redis 数据结构详解
Redis 五种数据结构详解
Redis 不只是缓存,它的五种数据结构各有各的用处,搞懂了才能用对地方。
1. String(字符串)
最基础的数据结构,存字符串、数字都行。
SET name "张三"
GET name
INCR count
SET token "abc123" EX 3600
应用场景:缓存用户信息、计数器、分布式锁、会话存储。
2. Hash(哈希)
存键值对,适合存对象。
HSET user:1 name "张三" age 25 city "北京"
HGET user:1 name
HGETALL user:1
应用场景:存用户信息、商品信息,比 String 存 JSON 更省空间。
3. List(列表)
有序列表,可以从头或尾加元素。
LPUSH messages "hello"
LPUSH messages "world"
LRANGE messages 0 -1
应用场景:消息队列、最新消息列表、朋友圈时间线。
4. Set(集合)
无序不重复集合。
SADD tags "PHP"
SADD tags "MySQL"
SMEMBERS tags
SISMEMBER tags "PHP"
应用场景:标签、共同好友、去重。
5. ZSet(有序集合)
每个元素带分数,按分数排序。
ZADD rank 100 "张三"
ZADD rank 90 "李四"
ZREVRANGE rank 0 9 WITHSCORES
应用场景:排行榜、计分板、优先级队列。
总结
五种数据结构各有用处:
- String:简单缓存、计数器
- Hash:存对象
- List:队列、时间线
- Set:去重、标签
- ZSet:排行榜
选对数据结构,性能才能发挥出来。
Redis 缓存穿透解决
Redis 缓存穿透解决方案
缓存穿透是指查询一个数据库里根本不存在的数据,因为缓存里也没有,所以每次请求都打到数据库。如果有人恶意攻击,数据库可能被打挂。
什么是缓存穿透
正常流程:先查缓存,缓存有就直接返回,没有再查数据库,然后写入缓存。
如果查询的数据数据库里也没有,那缓存永远都不会命中,每次请求都会查数据库,这就是缓存穿透。
解决方案一:缓存空值
最简单的办法,数据库查不到也往缓存里写一个空值,设个短过期时间。
$data = $redis->get($key);
if ($data === null) {
$data = $db->query($sql);
if ($data === null) {
// 缓存空值,60秒过期
$redis->setex($key, 60, '');
} else {
$redis->setex($key, 3600, $data);
}
}
优点:简单直接
缺点:会占用更多缓存空间,大量空值缓存会浪费内存
解决方案二:布隆过滤器
在缓存前面加一层布隆过滤器,先判断数据是否存在,不存在直接返回。
布隆过滤器的原理是用一个大的位数组和多个哈希函数,把存在的数据标记为1。查询时如果所有哈希位置都是1,说明可能存在;只要有一个位置是0,说明一定不存在。
优点:内存占用小,效率高
缺点:有一定误判率(说存在可能不存在,说不存在一定不存在)
两种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 缓存空值 | 简单 | 浪费内存 | 数据量小,穿透不严重 |
| 布隆过滤器 | 内存效率高 | 有误差 | 数据量大,穿透严重 |
总结
缓存穿透是常见的缓存问题,小项目用缓存空值就够了,大项目数据量大就上布隆过滤器。两种方案都能解决问题,根据实际场景选就行。
Redis 缓存策略:穿透、击穿、雪崩的完整解决方案
前言
Redis 缓存是高并发系统的标配,但用不好反而会出大问题。缓存穿透、击穿、雪崩这三个经典问题,几乎每个后端开发都踩过坑。本文用实战的方式,带你彻底搞懂这三个问题的原理和解决方案。
一、缓存穿透
什么是穿透
查询一个数据库里根本不存在的数据,缓存里也没有,每次请求都打到数据库上。这就是缓存穿透。
比如有人恶意请求 id = -1 的用户数据,数据库里永远查不到,缓存里也永远没有,每次请求都要查一次数据库。
解决方案
1. 缓存空值
function getUserById($id) {
$user = $redis->get("user:{$id}");
if ($user === false) {
// 数据库查询
$user = $db->query("SELECT * FROM users WHERE id = ?", [$id])->fetch();
if ($user) {
$redis->setex("user:{$id}", 3600, json_encode($user));
} else {
// 空值也缓存,短时间过期
$redis->setex("user:{$id}", 60, 'NULL');
}
}
return $user === 'NULL' ? null : $user;
}
优点:简单直接
缺点:如果恶意请求的 id 非常多,会占用大量缓存空间
2. 布隆过滤器
在缓存前面加一层布隆过滤器,先判断这个 id 是否存在,不存在直接返回。
// 初始化布隆过滤器,把所有存在的 id 放进去
$bloomFilter = new RedisBloomFilter($redis);
$bloomFilter->add($allUserIds);
function getUserById($id) {
// 先查布隆过滤器,不存在直接返回
if (!$bloomFilter->exists($id)) {
return null;
}
// 再走缓存查询...
}
优点:占用空间小,效率高
缺点:有一定误判率,实现复杂
二、缓存击穿
什么是击穿
某个热点 key 突然过期了,这一瞬间大量请求同时打到数据库上,就像被击穿了一样。
和穿透的区别:穿透是数据不存在,击穿是数据存在但缓存过期了。
解决方案
1. 互斥锁
function getHotProduct($id) {
$product = $redis->get("product:{$id}");
if ($product === false) {
// 尝试获取锁
$lockKey = "lock:product:{$id}";
$isLocked = $redis->set($lockKey, 1, ['NX', 'EX' => 10]);
if ($isLocked) {
// 拿到锁,查数据库
$product = $db->query("SELECT * FROM products WHERE id = ?", [$id])->fetch();
$redis->setex("product:{$id}", 3600, json_encode($product));
$redis->del($lockKey);
} else {
// 没拿到锁,等一会再查缓存
usleep(10000);
return getHotProduct($id);
}
}
return $product;
}
优点:保证只有一个请求查数据库
缺点:实现复杂,有死锁风险
2. 热点数据永不过期
对真正的热点数据,直接设置永不过期,由后台异步更新。
// 热点数据不设过期时间,后台定时更新
$redis->set("hot_product:{$id}", json_encode($product));
三、缓存雪崩
什么是雪崩
大量缓存 key 在同一时间集体过期,或者 Redis 整个宕机了,所有请求瞬间全部打到数据库上,数据库直接被压垮。
和击穿的区别:击穿是一个热点 key,雪崩是大量 key 同时失效。
解决方案
1. 过期时间加随机值
// 不要所有 key 都设成 3600 秒过期
$ttl = 3600 + rand(0, 600); // 在 1 小时基础上加 0-10 分钟随机值
$redis->setex("product:{$id}", $ttl, json_encode($product));
这样就不会出现大量 key 同一时间过期的情况。
2. Redis 集群高可用
部署 Redis 主从 + 哨兵,或者 Redis Cluster,确保 Redis 本身不会单点故障。
3. 服务降级限流
当数据库压力过大时,主动限流,返回兜底数据。
// 简单的降级逻辑
if ($redis->ping() === false) {
// Redis 挂了,直接返回默认数据
return getDefaultProduct();
}
四、三种问题对比
| 问题 | 原因 | 特点 | 解决方案 |
|---|---|---|---|
| 穿透 | 数据不存在 | 查不到的数据一直被请求 | 缓存空值 / 布隆过滤器 |
| 击穿 | 热点 key 过期 | 单个热点 key 瞬间失效 | 互斥锁 / 永不过期 |
| 雪崩 | 大量 key 同时过期 | 大面积缓存失效 | 随机过期时间 / 集群 / 限流 |
总结
缓存三件套的核心思路:
- 穿透:不让不存在的数据反复打数据库
- 击穿:热点数据过期时只让一个请求去查数据库
- 雪崩:让缓存失效时间错开,同时保证 Redis 高可用
搞懂这三个问题,你的缓存方案才算真正入门了。