Redis是目前最流行的内存数据库,以高性能、高并发、丰富的数据结构著称。很多人只会用Redis做简单的缓存,但是Redis的功能远不止于此,它还可以做消息队列、发布订阅、分布式锁、计数器、排行榜等。今天就来分享Redis的高级用法。

1. Redis数据结构

Redis支持多种数据结构,不同的数据结构适用于不同的场景:

  • String(字符串):最基本的数据结构,可以存储字符串、整数、浮点数。适用于缓存、计数器、分布式锁等。
  • Hash(哈希):键值对集合,适合存储对象。适用于存储用户信息、文章信息等。
  • List(列表):有序可重复的字符串列表,基于链表实现。适用于消息队列、最新列表等。
  • Set(集合):无序不可重复的字符串集合。适用于标签、共同好友、抽奖等。
  • Sorted Set(有序集合):有序不可重复的字符串集合,每个元素有一个分数。适用于排行榜、延时队列等。
  • Bitmap(位图):基于String的位操作,适用于签到、用户在线状态等。
  • HyperLogLog:基数统计,适用于UV统计等。
  • Geo:地理位置,适用于附近的人、距离计算等。

2. 缓存实战

缓存是Redis最常用的场景,合理使用缓存能大大提升系统性能,减轻数据库压力。

基本缓存:

// 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 从缓存获取数据
$key = 'article:' . $id;
$data = $redis->get($key);

if ($data === false) {
    // 缓存未命中,从数据库查询
    $data = $db->query("SELECT * FROM articles WHERE id = $id")->fetch(PDO::FETCH_ASSOC);
    // 写入缓存,设置过期时间1小时
    $redis->setex($key, 3600, json_encode($data));
} else {
    $data = json_decode($data, true);
}

缓存穿透: 查询一个不存在的数据,缓存永远不命中,请求都打到数据库。 解决方案:

  1. 缓存空值:查询结果为空也缓存,设置较短的过期时间
  2. 布隆过滤器:在缓存前加一层布隆过滤器,不存在的直接返回
// 缓存空值
$data = $db->query("SELECT * FROM articles WHERE id = $id")->fetch(PDO::FETCH_ASSOC);
if ($data) {
    $redis->setex($key, 3600, json_encode($data));
} else {
    // 缓存空值,过期时间短一些
    $redis->setex($key, 60, json_encode(null));
}

缓存雪崩: 大量缓存同时过期,请求都打到数据库。 解决方案:

  1. 过期时间加随机值:避免大量key同时过期
  2. 多级缓存:本地缓存 + Redis缓存
  3. 熔断降级:数据库压力大时返回默认值
// 过期时间加随机值,避免雪崩
$ttl = 3600 + rand(0, 300);
$redis->setex($key, $ttl, json_encode($data));

缓存击穿: 一个热点key过期,大量并发请求打到数据库。 解决方案:

  1. 互斥锁:缓存未命中时加锁,只让一个请求去查数据库
  2. 逻辑过期:不设置TTL,数据中包含过期时间,过期后异步更新
// 互斥锁方案
$data = $redis->get($key);
if ($data === false) {
    // 加锁
    $lockKey = 'lock:' . $key;
    $locked = $redis->set($lockKey, 1, ['nx', 'ex' => 10]);
    if ($locked) {
        // 获得锁,查询数据库
        $data = $db->query("SELECT * FROM articles WHERE id = $id")->fetch(PDO::FETCH_ASSOC);
        $redis->setex($key, 3600, json_encode($data));
        // 释放锁
        $redis->del($lockKey);
    } else {
        // 未获得锁,等待后重试
        usleep(100000);
        $data = $redis->get($key);
        if ($data) $data = json_decode($data, true);
    }
} else {
    $data = json_decode($data, true);
}

3. 消息队列

Redis的List结构可以用来做简单的消息队列,适用于异步任务、削峰填谷等场景。

生产者:

// 生产者:将任务推入队列
$redis->lPush('task_queue', json_encode([
    'task' => 'send_email',
    'data' => ['to' => 'user@example.com', 'subject' => 'Hello']
]));

消费者:

// 消费者:从队列取出任务处理
while (true) {
    // 阻塞式弹出,没有任务时等待
    $task = $redis->brPop('task_queue', 30);
    if ($task) {
        $data = json_decode($task[1], true);
        // 处理任务
        if ($data['task'] === 'send_email') {
            sendEmail($data['data']);
        }
    }
}

延时队列: 使用Sorted Set实现延时队列,score为执行时间戳。

// 添加延时任务,10分钟后执行
$redis->zAdd('delay_queue', time() + 600, json_encode([
    'task' => 'order_timeout',
    'order_id' => 12345
]));

// 消费者轮询
while (true) {
    // 查询到期的任务
    $tasks = $redis->zRangeByScore('delay_queue', 0, time(), ['limit' => [0, 10]]);
    foreach ($tasks as $task) {
        // 删除任务(防止重复处理)
        $removed = $redis->zRem('delay_queue', $task);
        if ($removed) {
            $data = json_decode($task, true);
            // 处理任务
            processTask($data);
        }
    }
    sleep(1);
}

4. 发布订阅

Redis支持发布订阅模式,适用于消息广播、实时通知等场景。

发布者:

// 发布消息到频道
$redis->publish('news_channel', json_encode([
    'title' => '重大新闻',
    'content' => '今天发生了一件大事...'
]));

订阅者:

// 订阅频道,接收消息
$redis->subscribe(['news_channel'], function($redis, $channel, $message) {
    $data = json_decode($message, true);
    echo "收到消息:{$data['title']}\n";
    // 处理消息
    processNews($data);
});

模式匹配订阅: 订阅匹配特定模式的频道。

// 订阅所有以news_开头的频道
$redis->psubscribe(['news_*'], function($redis, $pattern, $channel, $message) {
    echo "频道 $channel 收到消息:$message\n";
});

5. 分布式锁

在分布式系统中,需要分布式锁来保证数据一致性。Redis可以用来实现分布式锁。

加锁:

function lock($key, $timeout = 10) {
    global $redis;
    $value = uniqid(); // 唯一值,用于释放锁时验证
    // SET key value NX EX timeout
    $result = $redis->set($key, $value, ['nx', 'ex' => $timeout]);
    if ($result) {
        return $value; // 返回锁的值,用于释放锁
    }
    return false;
}

释放锁:

function unlock($key, $value) {
    global $redis;
    // 使用Lua脚本保证原子性:先判断值是否匹配,再删除
    $script = "
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
    ";
    return $redis->eval($script, [$key, $value], 1);
}

使用示例:

$lockKey = 'lock:order:' . $orderId;
$lockValue = lock($lockKey, 10);
if ($lockValue) {
    try {
        // 执行业务逻辑
        processOrder($orderId);
    } finally {
        // 释放锁
        unlock($lockKey, $lockValue);
    }
} else {
    echo "系统繁忙,请稍后重试\n";
}

6. 排行榜

使用Sorted Set实现排行榜,适用于热门文章、用户积分排行等场景。

// 增加文章阅读量
$redis->zIncrBy('article_ranking', 1, $articleId);

// 获取排行榜前10名
$ranking = $redis->zRevRange('article_ranking', 0, 9, true);
foreach ($ranking as $articleId => $views) {
    echo "文章 $articleId 阅读量 $views\n";
}

// 获取某篇文章的排名
$rank = $redis->zRevRank('article_ranking', $articleId);
echo "文章排名:" . ($rank + 1) . "\n";

7. Redis持久化

Redis支持两种持久化方式:RDB和AOF。

RDB(快照): 定时将内存数据快照保存到磁盘。

  • 优点:文件小,恢复快
  • 缺点:可能丢失最后一次快照后的数据
# redis.conf
save 900 1      # 900秒内至少1个key修改则保存
save 300 10     # 300秒内至少10个key修改则保存
save 60 10000   # 60秒内至少10000个key修改则保存
dbfilename dump.rdb
dir /var/lib/redis

AOF(追加文件): 将每条写命令追加到文件。

  • 优点:数据安全性高,最多丢失1秒数据
  • 缺点:文件大,恢复慢
# redis.conf
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec  # 每秒同步一次
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

建议: 生产环境同时开启RDB和AOF,兼顾性能和数据安全。

总结

Redis是一个功能强大的内存数据库,除了简单的缓存,还可以做消息队列、发布订阅、分布式锁、排行榜等。掌握Redis的高级用法,能让我们设计出更高性能、更可靠的系统。

希望这篇文章能帮助大家更好地理解和使用Redis。