Redis 缓存实战:从入门到生产环境最佳实践

· 阅读约需22分钟

一、为什么选择 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-server

2.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 缓存穿透

问题描述: 查询一个不存在的数据,缓存和数据库都没有,每次请求都打到数据库。

解决方案:

  1. 缓存空值:查询不到的数据也缓存一个空值,设置较短的过期时间
  2. 布隆过滤器:在缓存前加一层布隆过滤器,不存在的直接返回
// 缓存空值示例
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 过期的瞬间,大量请求同时打到数据库。

解决方案:

  1. 互斥锁:只让一个请求去查数据库并更新缓存
  2. 永不过期:逻辑过期,后台异步更新
// 互斥锁方案
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 宕机,导致所有请求都打到数据库。

解决方案:

  1. 过期时间打散:给过期时间加一个随机值
  2. 多级缓存:本地缓存 + Redis 缓存
  3. 服务降级:非核心数据直接返回默认值
  4. 高可用架构:哨兵或集群模式
// 过期时间打散
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 内存优化

  1. 使用合适的数据类型:小数据量用 Hash 比 String 更省内存
  2. 设置合理的过期时间:不用的数据及时清理
  3. 使用整数编码:数字字符串会自动优化
  4. 压缩大值:超过 10KB 的值考虑压缩
# 查看内存使用情况
INFO memory

# 查看大 key
redis-cli --bigkeys

5.3 安全配置

  1. 绑定内网 IP:不要暴露在公网
  2. 设置强密码requirepass 配置
  3. 禁用危险命令
    rename-command FLUSHDB ""
    rename-command FLUSHALL ""
    rename-command KEYS ""
    rename-command CONFIG ""
  4. 使用防火墙:限制访问 IP

5.4 监控与告警

关键监控指标:

  • 内存使用率:超过 80% 告警
  • QPS:请求量突增告警
  • 命中率:低于 90% 需要优化
  • 连接数:接近最大值告警
  • 持久化:RDB/AOF 执行失败告警
# 实时监控
redis-cli --stat

# 查看慢查询
SLOWLOG GET 10

六、常见问题与解决方案

6.1 Redis 变慢了怎么办?

排查步骤:

  1. 查看慢查询日志:SLOWLOG GET
  2. 检查内存使用:INFO memory
  3. 查看持久化状态:INFO persistence
  4. 检查大 key:redis-cli --bigkeys

常见原因:

  • 使用了 KEYS 命令(生产环境禁用)
  • 大 value 导致网络传输慢
  • 内存满了触发淘汰
  • 持久化 fork 子进程耗时
  • AOF 重写占用资源

6.2 数据一致性问题

缓存和数据库的一致性是个经典问题:

最终一致性方案(推荐):

  1. 更新数据库
  2. 删除缓存
  3. 设置合理的过期时间兜底

强一致性方案:

  1. 先删缓存
  2. 延时双删(更新数据库后,延迟一段时间再删一次)
  3. 基于 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 的使用方法。

核心要点回顾:

  1. 根据业务场景选择合适的数据类型
  2. 防范缓存穿透、击穿、雪崩三大问题
  3. 合理设计键名和过期策略
  4. 做好监控和安全配置
  5. 关注性能优化和数据一致性

希望这篇文章能帮助你在项目中更好地使用 Redis。如果有问题,欢迎在评论区交流讨论。