Redis是现在最流行的内存数据库之一它性能极高支持多种数据结构功能非常强大不仅能做缓存还能做消息队列分布式锁计数器排行榜等等很多场景都能用Redis来解决是每个后端开发者都必须掌握的技能。

我用Redis很多年了在各种项目中用过Redis的各种功能踩了一些坑也总结了一些经验今天就来分享一下Redis的实战经验从缓存到消息队列各种应用场景和最佳实践全面分享希望能帮大家更好地使用Redis提升系统性能。

一、Redis简介

先简单介绍一下Redis。

Redis全称Remote Dictionary Server远程字典服务是一个开源的使用C语言编写的高性能的键值对内存数据库它把数据存储在内存中读写速度非常快每秒能处理十几万次请求性能极高同时它也支持数据持久化能把内存中的数据保存到磁盘重启后能恢复数据。

Redis最大的特点就是支持多种数据结构不只是简单的字符串还支持列表哈希集合有序集合等等这让Redis能应用在很多场景比如缓存消息队列分布式锁计数器排行榜社交关系等等功能非常强大。

Redis的特点:

  1. 高性能: 数据存储在内存读写速度极快每秒十几万请求。
  2. 多种数据结构: 支持字符串列表哈希集合有序集合等多种数据结构。
  3. 持久化: 支持RDB和AOF两种持久化方式数据不丢失。
  4. 高可用: 支持主从复制哨兵集群高可用架构。
  5. 原子操作: 所有操作都是原子的支持事务Lua脚本。
  6. 发布订阅: 支持发布订阅模式能做消息通知。

2017年Redis已经很成熟了版本大概是3.x4.x很多公司都在用Redis做缓存和各种场景是后端开发必备的技能。

二、缓存场景

缓存是Redis最常用的场景也是最基础的用法把热点数据缓存到Redis查询的时候先查Redis有就直接返回没有再查数据库然后缓存起来这样能大大减轻数据库的压力提高系统性能和响应速度。

1. 基本缓存用法

最基本的缓存用法就是用字符串类型存储缓存数据比如用户信息商品信息等等:

// 先查缓存
$user = $redis->get('user:' . $userId);
if ($user) {
    return json_decode($user, true);
}

// 缓存没有查数据库
$user = $db->query('SELECT * FROM users WHERE id = ?', [$userId]);

// 缓存到Redis设置过期时间
$redis->setex('user:' . $userId, 3600, json_encode($user));

return $user;

这里用了setex设置缓存和过期时间1小时这样缓存不会一直存在会自动过期更新数据也能同步更新。

2. 缓存穿透缓存击穿缓存雪崩

用Redis做缓存经常会遇到三个问题:缓存穿透缓存击穿缓存雪崩这三个问题很常见也很重要必须了解和解决。

  • 缓存穿透: 查询一个不存在的数据缓存没有数据库也没有这样每次请求都会打到数据库如果被恶意攻击大量查询不存在的数据会把数据库打垮解决方法:缓存空值或者用布隆过滤器拦截不存在的查询。
  • 缓存击穿: 一个热点key在缓存过期的瞬间大量请求同时过来都打到数据库导致数据库压力过大解决方法:加互斥锁只让一个请求去查数据库其他请求等待或者热点key永不过期后台异步更新。
  • 缓存雪崩: 大量缓存在同一时间过期或者Redis宕机导致大量请求同时打到数据库导致数据库压力过大甚至宕机解决方法:过期时间加随机值避免同时过期Redis高可用集群多级缓存限流降级等等。

3. 缓存更新策略

缓存的更新策略也很重要常见的有几种:

  • Cache Aside: 最常用的读的时候先读缓存没有读数据库然后放入缓存写的时候先更新数据库然后删除缓存这种方式简单常用大部分场景都适用。
  • Write Through: 写的时候同时更新缓存和数据库缓存和数据库一致但是写的性能稍差。
  • Write Behind: 写的时候只更新缓存然后异步批量更新数据库写的性能高但是数据可能会丢失一致性差。

一般推荐用Cache Aside模式简单可靠大部分场景都够用。

三、计数器和排行榜

Redis的原子操作和有序集合很适合做计数器和排行榜比如文章阅读量点赞数评论数商品销量排行榜等等。

1. 计数器

用Redis的INCR命令能原子地增加计数很适合做计数器比如文章阅读量:

// 文章阅读量+1
$redis->incr('article:views:' . $articleId);

// 获取阅读量
$views = $redis->get('article:views:' . $articleId);

INCR是原子操作并发安全不会有计数不准的问题性能也很高比数据库计数好很多。

还能用INCRBY增加指定的数值比如点赞数+1评论数+1等等都很方便。

2. 排行榜

用Redis的有序集合(ZSet)能很方便地做排行榜比如热门文章畅销商品积分榜等等:

// 添加文章到排行榜分数是阅读量
$redis->zAdd('article:rank', $views, $articleId);

// 获取排行榜前10名(从高到低)
$rank = $redis->zRevRange('article:rank', 0, 9, true);

// 获取某篇文章的排名
$rankNum = $redis->zRevRank('article:rank', $articleId);

有序集合自动按分数排序能很方便地获取前N名获取某个元素的排名等等性能很高比数据库ORDER BY好很多特别适合做各种排行榜。

四、分布式锁

在分布式系统中经常需要分布式锁来保证并发安全比如秒杀扣库存防止重复提交等等Redis能很方便地实现分布式锁。

1. 基本分布式锁

用Redis的SETNX命令能实现基本的分布式锁:

// 加锁
$lockKey = 'lock:order:' . $orderId;
$lockValue = uniqid(); // 唯一值用于释放锁时判断
$locked = $redis->set($lockKey, $lockValue, ['nx', 'ex' => 30]); // nx=不存在才设置ex=过期时间30秒

if (!$locked) {
    throw new Exception('系统繁忙请稍后再试');
}

try {
    // 执行业务逻辑
    // ...
} finally {
    // 释放锁用Lua脚本保证原子性
    $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    $redis->eval($script, [$lockKey, $lockValue], 1);
}

这里用了SET的NX参数只有key不存在的时候才设置成功这样就能实现互斥锁EX参数设置过期时间防止锁没释放导致死锁释放锁的时候用Lua脚本保证判断和删除是原子的防止误删别人的锁。

2. Redlock算法

如果Redis是单节点分布式锁有单点故障的问题Redis作者提出了Redlock算法在多个Redis节点上加锁大部分节点加锁成功才算加锁成功这样更可靠当然Redlock也有争议一般场景单节点锁就够用了特别严格的场景才需要Redlock。

五、消息队列

Redis也能做消息队列用列表类型或者发布订阅模式能实现简单的消息队列适合一些简单的异步场景比如发送邮件短信日志记录等等。

1. 列表实现消息队列

用Redis的列表LPUSH和BRPOP能实现简单的消息队列:

// 生产者推送消息
$redis->lPush('queue:email', json_encode(['to' => 'user@example.com', 'subject' => '测试邮件']));

// 消费者阻塞获取消息
while (true) {
    $msg = $redis->brPop('queue:email', 30); // 阻塞等待超时30秒
    if ($msg) {
        $data = json_decode($msg[1], true);
        // 处理消息发送邮件
        sendEmail($data['to'], $data['subject']);
    }
}

LPUSH从左边推送消息BRPOP从右边阻塞获取消息这样就是一个简单的先进先出的消息队列BRPOP是阻塞的没有消息的时候会等待不会一直轮询性能很好。

这种方式简单好用但是不支持消息确认和重试如果消费者处理失败消息就丢了适合一些不那么重要的场景比如日志通知等等如果需要可靠的消息队列还是建议用专业的消息队列比如RabbitMQKafka等等。

2. 发布订阅模式

Redis还支持发布订阅模式一个发布者发布消息多个订阅者都能收到适合做广播通知比如实时消息系统通知等等:

// 发布者
$redis->publish('channel:news', '有新消息了');

// 订阅者
$redis->subscribe(['channel:news'], function ($redis, $channel, $msg) {
    echo "收到消息: $msg\n";
});

发布订阅模式适合广播场景但是消息不会持久化如果订阅者不在线消息就丢了而且不支持消息确认适合一些实时的不要求可靠的场景。

六、其他应用场景

除了上面的场景Redis还有很多其他的应用场景:

1. 社交关系

用集合能很方便地做社交关系比如关注粉丝共同好友等等:

// 关注
$redis->sAdd('user:follow:' . $userId, $followUserId);
$redis->sAdd('user:fans:' . $followUserId, $userId);

// 共同好友
$commonFriends = $redis->sInter('user:friends:' . $userId1, 'user:friends:' . $userId2);

集合支持交集并集差集运算很适合做社交关系的各种查询。

2. 最新列表

用列表能做最新列表比如最新文章最新评论等等用LPUSH添加LRANGE获取最新的N条很方便:

// 添加最新文章
$redis->lPush('article:latest', $articleId);
$redis->lTrim('article:latest', 0, 99); // 只保留最新的100条

// 获取最新文章
$latestArticles = $redis->lRange('article:latest', 0, 9);

3. 限流

用Redis能很方便地做接口限流比如用户每分钟只能请求100次:

$key = 'rate:limit:' . $userId . ':' . date('YmdHi');
$count = $redis->incr($key);
if ($count == 1) {
    $redis->expire($key, 60);
}
if ($count > 100) {
    throw new Exception('请求过于频繁请稍后再试');
}

这是简单的计数器限流还能用滑动窗口令牌桶等算法做更精确的限流Redis都能很方便地实现。

七、Redis最佳实践和踩坑经验

最后分享一些Redis的最佳实践和踩坑经验:

1. 不要把Redis当数据库用

Redis是内存数据库虽然支持持久化但是主要还是用来做缓存和辅助功能不要把所有数据都存在Redis里把Redis当主数据库用那样成本高也不安全重要的数据还是要存在MySQL等关系型数据库里Redis做缓存和辅助就好。

2. 设置过期时间

缓存一定要设置过期时间不要永久缓存不然数据更新了缓存还是旧的而且会占用大量内存导致内存不足一般缓存过期时间根据数据更新频率设置几分钟到几小时不等。

3. 注意大key和热key

不要存太大的key比如一个key存几MB甚至几十MB的数据那样会影响Redis性能也会导致网络阻塞大key要拆分成小key也要注意热key某个key访问量特别大会导致Redis压力过大可以用本地缓存多级缓存来缓解。

4. 批量操作提高性能

如果要操作多个key尽量用批量操作比如mgetmsetpipeline等等减少网络往返次数提高性能但是批量操作也不要一次太多key不然会阻塞Redis一般一次几百个就差不多了。

5. 持久化配置

Redis持久化有RDB和AOF两种RDB是快照恢复快但是可能会丢数据AOF是日志数据安全但是恢复慢一般建议两种都开启兼顾性能和数据安全根据业务需求调整配置。

6. 高可用架构

生产环境Redis一定要高可用不要单点用主从复制哨兵或者集群保证Redis挂了能自动切换不影响业务同时也要做好监控和告警及时发现问题。

7. 内存管理

Redis是内存数据库内存很宝贵要注意内存使用情况设置maxmemory和淘汰策略当内存满了的时候按策略淘汰旧的key不要让Redis因为内存不足而崩溃常用的淘汰策略有allkeys-lruvolatile-lru等等根据业务需求选择。

八、写在最后

Redis实战:从缓存到消息队列的应用场景与最佳实践。

Redis是一个非常强大的工具性能高功能多能应用在很多场景缓存计数器排行榜分布式锁消息队列限流社交关系等等是每个后端开发者都必须掌握的技能。

当然Redis虽然强大但是也不是银弹要根据业务场景合理使用不要滥用也要注意各种坑和最佳实践保证Redis稳定高效运行。

希望这篇分享能帮大家更好地理解和使用Redis提升系统性能和稳定性。

最后用一句话结尾:

"Redis是后端开发的瑞士军刀性能高功能强用好了能让你的系统飞起来。"

祝大家都能用好Redis系统又快又稳!