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 高可用
搞懂这三个问题,你的缓存方案才算真正入门了。