Redis 缓存实战:从入门到生产环境最佳实践
一、为什么选择 Redis?
在现代 Web 应用架构中,缓存是提升性能的关键组件。Redis(Remote Dictionary Server)作为一款高性能的键值对存储数据库,凭借其丰富的数据结构、出色的性能和灵活的部署方式,成为了缓存领域的首选方案。
Redis 的核心优势
- 极致性能:纯内存操作,单线程模型避免了上下文切换开销,QPS 可达 10万+
- 丰富数据类型:支持 String、Hash、List、Set、Sorted Set 等多种数据结构
- 持久化支持:RDB + AOF 两种持久化方式,兼顾性能与数据安全
- 高可用架构:主从复制、哨兵模式、集群模式一应俱全
- 原子操作:所有操作都是原子性的,支持事务
二、Redis 安装与基础配置
2.1 在 Ubuntu 上安装 Redis
# 更新软件包列表
sudo apt update
# 安装 Redis
sudo apt install redis-server -y
# 启动 Redis 服务
sudo systemctl start redis-server
# 设置开机自启
sudo systemctl enable redis-server2.2 基础配置优化
编辑 /etc/redis/redis.conf 文件:
# 绑定地址,生产环境建议绑定内网IP
bind 127.0.0.1
# 设置密码
requirepass your_strong_password
# 内存限制,建议设置为物理内存的 70-80%
maxmemory 2gb
# 内存淘汰策略
maxmemory-policy allkeys-lru
# 持久化配置
save 900 1
save 300 10
save 60 10000
# AOF 持久化
appendonly yes
appendfsync everysec配置完成后重启服务:
sudo systemctl restart redis-server三、核心数据类型详解
3.1 String(字符串)
最基础的数据类型,二进制安全,可以存储任何数据。
常用命令:
# 设置值
SET user:1:name "张三"
SET user:1:age 25
# 获取值
GET user:1:name
# 自增/自减
INCR user:1:age
DECR user:1:age
# 设置过期时间
SETEX token:abc123 3600 "user_token_value"
# 批量操作
MSET user:1:name "张三" user:1:email "zhangsan@example.com"
MGET user:1:name user:1:email应用场景:
- 用户信息缓存
- Token 存储
- 计数器
- 分布式锁
3.2 Hash(哈希)
适合存储对象,一个 key 对应多个 field-value 对。
常用命令:
# 设置字段
HSET user:1 name "张三" age 25 email "zhangsan@example.com"
# 获取字段
HGET user:1 name
HGETALL user:1
# 获取所有字段名
HKEYS user:1
# 获取所有值
HVALS user:1
# 字段自增
HINCRBY user:1 age 1
# 判断字段是否存在
HEXISTS user:1 phone应用场景:
- 用户信息存储
- 商品信息
- 配置项管理
3.3 List(列表)
双向链表,可以从两端操作,适合做队列。
常用命令:
# 从左侧插入
LPUSH message:queue "消息1" "消息2" "消息3"
# 从右侧插入
RPUSH message:queue "消息4"
# 从左侧弹出
LPOP message:queue
# 从右侧弹出
RPOP message:queue
# 获取列表长度
LLEN message:queue
# 获取指定范围的元素
LRANGE message:queue 0 -1
# 阻塞式弹出(用于消息队列)
BLPOP message:queue 30应用场景:
- 消息队列
- 最新列表
- 时间线
3.4 Set(集合)
无序集合,元素唯一,支持集合运算。
常用命令:
# 添加元素
SADD tag:php "PHP" "Laravel" "Symfony"
SADD tag:java "Java" "Spring" "Laravel"
# 获取所有元素
SMEMBERS tag:php
# 判断元素是否存在
SISMEMBER tag:php "Laravel"
# 交集
SINTER tag:php tag:java
# 并集
SUNION tag:php tag:java
# 差集
SDIFF tag:php tag:java
# 集合大小
SCARD tag:php应用场景:
- 标签系统
- 共同好友
- 去重
- 抽奖系统
3.5 Sorted Set(有序集合)
每个元素都有一个分数,按分数排序。
常用命令:
# 添加元素
ZADD ranking 100 "user1" 95 "user2" 90 "user3" 85 "user4"
# 获取排名(从0开始)
ZRANK ranking "user3"
# 获取指定排名范围的元素
ZRANGE ranking 0 2 WITHSCORES
# 按分数范围获取
ZRANGEBYSCORE ranking 90 100 WITHSCORES
# 增加分数
ZINCRBY ranking 5 "user3"
# 获取元素数量
ZCARD ranking应用场景:
- 排行榜
- 带权重的任务队列
- 范围查找
四、缓存策略与设计模式
4.1 缓存穿透
问题描述: 查询一个不存在的数据,缓存和数据库都没有,每次请求都打到数据库。
解决方案:
- 缓存空值:查询不到的数据也缓存一个空值,设置较短的过期时间
- 布隆过滤器:在缓存前加一层布隆过滤器,不存在的直接返回
// 缓存空值示例
function getUser($id) {
$key = "user:{$id}";
$user = Redis::get($key);
if ($user !== null) {
return $user === '' ? null : json_decode($user, true);
}
$user = DB::table('users')->find($id);
if ($user) {
Redis::setex($key, 3600, json_encode($user));
} else {
// 缓存空值,过期时间短一些
Redis::setex($key, 60, '');
}
return $user;
}4.2 缓存击穿
问题描述: 一个热点 key 过期的瞬间,大量请求同时打到数据库。
解决方案:
- 互斥锁:只让一个请求去查数据库并更新缓存
- 永不过期:逻辑过期,后台异步更新
// 互斥锁方案
function getHotData($key) {
$data = Redis::get($key);
if ($data) {
return json_decode($data, true);
}
// 尝试获取锁
$lockKey = "lock:{$key}";
$locked = Redis::set($lockKey, 1, 'NX', 'EX', 10);
if ($locked) {
// 获取锁成功,查数据库
$data = DB::table('hot_data')->where('key', $key)->first();
if ($data) {
Redis::setex($key, 3600, json_encode($data));
}
// 释放锁
Redis::del($lockKey);
return $data;
} else {
// 没获取到锁,等待重试
usleep(50000); // 50ms
return getHotData($key);
}
}4.3 缓存雪崩
问题描述: 大量 key 在同一时间过期,或者 Redis 宕机,导致所有请求都打到数据库。
解决方案:
- 过期时间打散:给过期时间加一个随机值
- 多级缓存:本地缓存 + Redis 缓存
- 服务降级:非核心数据直接返回默认值
- 高可用架构:哨兵或集群模式
// 过期时间打散
function setCache($key, $value, $baseTTL = 3600) {
// 基础时间 + 0-300秒的随机偏移
$ttl = $baseTTL + rand(0, 300);
Redis::setex($key, $ttl, json_encode($value));
}4.4 缓存更新策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Cache Aside | 先更新数据库,再删除缓存 | 读多写少 |
| Write Through | 写缓存时同步写数据库 | 一致性要求高 |
| Write Behind | 写缓存后异步写数据库 | 写多读少,可容忍数据丢失 |
推荐使用 Cache Aside 模式:
// 更新数据
function updateUser($id, $data) {
// 1. 更新数据库
DB::table('users')->where('id', $id)->update($data);
// 2. 删除缓存(下次读取时自动加载)
Redis::del("user:{$id}");
}五、生产环境最佳实践
5.1 键名设计规范
好的键名设计能让 Redis 更易维护:
业务:模块:id:属性
示例:
- user:1:profile 用户资料
- user:1:token 用户Token
- product:100:info 商品信息
- order:2023:count 订单计数
- cache:page:home 页面缓存命名原则:
- 使用冒号
:分隔层级 - 见名知意,不要用缩写
- 控制键名长度,过长会占用更多内存
5.2 内存优化
- 使用合适的数据类型:小数据量用 Hash 比 String 更省内存
- 设置合理的过期时间:不用的数据及时清理
- 使用整数编码:数字字符串会自动优化
- 压缩大值:超过 10KB 的值考虑压缩
# 查看内存使用情况
INFO memory
# 查看大 key
redis-cli --bigkeys5.3 安全配置
- 绑定内网 IP:不要暴露在公网
- 设置强密码:
requirepass配置 - 禁用危险命令:
rename-command FLUSHDB "" rename-command FLUSHALL "" rename-command KEYS "" rename-command CONFIG "" - 使用防火墙:限制访问 IP
5.4 监控与告警
关键监控指标:
- 内存使用率:超过 80% 告警
- QPS:请求量突增告警
- 命中率:低于 90% 需要优化
- 连接数:接近最大值告警
- 持久化:RDB/AOF 执行失败告警
# 实时监控
redis-cli --stat
# 查看慢查询
SLOWLOG GET 10六、常见问题与解决方案
6.1 Redis 变慢了怎么办?
排查步骤:
- 查看慢查询日志:
SLOWLOG GET - 检查内存使用:
INFO memory - 查看持久化状态:
INFO persistence - 检查大 key:
redis-cli --bigkeys
常见原因:
- 使用了 KEYS 命令(生产环境禁用)
- 大 value 导致网络传输慢
- 内存满了触发淘汰
- 持久化 fork 子进程耗时
- AOF 重写占用资源
6.2 数据一致性问题
缓存和数据库的一致性是个经典问题:
最终一致性方案(推荐):
- 更新数据库
- 删除缓存
- 设置合理的过期时间兜底
强一致性方案:
- 先删缓存
- 延时双删(更新数据库后,延迟一段时间再删一次)
- 基于 binlog 异步更新缓存
6.3 分布式锁的正确实现
错误示例:
// 错误:先 setnx 再 expire,不是原子操作
Redis::setnx($lockKey, 1);
Redis::expire($lockKey, 10);正确示例:
// 正确:原子操作设置值和过期时间
$locked = Redis::set($lockKey, $requestId, 'NX', 'EX', 10);
// 释放锁时也要判断是不是自己加的锁
function releaseLock($lockKey, $requestId) {
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
return Redis::eval($script, 1, $lockKey, $requestId);
}七、总结
Redis 作为缓存利器,用好了能大幅提升系统性能,但也需要注意各种坑。本文从基础安装、数据类型、缓存策略到生产实践,全面介绍了 Redis 的使用方法。
核心要点回顾:
- 根据业务场景选择合适的数据类型
- 防范缓存穿透、击穿、雪崩三大问题
- 合理设计键名和过期策略
- 做好监控和安全配置
- 关注性能优化和数据一致性
希望这篇文章能帮助你在项目中更好地使用 Redis。如果有问题,欢迎在评论区交流讨论。