Skip to content

Redis 数据结构操作

概述

Redis 提供 5 种基础数据结构(String、Hash、List、Set、Zset),每种结构适用于不同的业务场景。合理选择数据结构是 Redis 数据建模的关键。

选择指南

  • String — 简单值、计数器、缓存对象(序列化后存储)
  • Hash — 对象属性、用户信息、配置项
  • List — 消息队列、最新列表、任务栈
  • Set — 标签、去重、共同好友、随机抽奖
  • Zset — 排行榜、评分系统、优先级队列

基础概念

数据类型对比

类型存储特点时间复杂度典型场景
String二进制安全,最大512MBO(1)缓存、计数器
Hash字段-值映射,最多2^32字段O(1)对象存储
List有序链表,两端操作快O(1)/O(N)队列、栈
Set无序唯一元素O(1)去重、交集
Zset分数排序的唯一元素O(log N)排行榜

语法与代码

String — 缓存与计数器

php
<?php
declare(strict_types=1);

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

// 1. 缓存用户信息
$redis->setex('user:1001:info', 3600, json_encode([
    'id' => 1001,
    'name' => '张三',
    'email' => 'zhangsan@example.com',
]));

$user = json_decode($redis->get('user:1001:info'), true);

// 2. 页面访问计数器
$pageViews = $redis->incr('stats:page:home:views');
echo "首页访问量: {$pageViews}\n";

// 3. 分布式 ID 生成
$redis->set('id_generator:order', 10000);
$orderId = $redis->incr('id_generator:order'); // 10001
$orderId = 'ORD' . str_pad((string) $orderId, 8, '0', STR_PAD_LEFT);

// 4. 限速计数(窗口内请求数)
$key = 'rate:user:1001:' . date('YmdHi');
$count = $redis->incr($key);
if ($count === 1) {
    $redis->expire($key, 60); // 60秒窗口
}
if ($count > 30) {
    echo "请求过于频繁\n";
}

// 5. 乐观锁实现
$redis->watch('stock:product:1');
$stock = (int) $redis->get('stock:product:1');
if ($stock > 0) {
    $redis->multi();
    $redis->decr('stock:product:1');
    $redis->exec();
} else {
    $redis->unwatch();
    echo "库存不足\n";
}

Hash — 对象存储

php
<?php
declare(strict_types=1);

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

// 1. 用户对象存储
$redis->hMSet('user:1001', [
    'name'     => '张三',
    'email'    => 'zhangsan@example.com',
    'password' => password_hash('123456', PASSWORD_BCRYPT),
    'status'   => 'active',
    'created_at' => time(),
]);

// 更新单个字段
$redis->hSet('user:1001', 'last_login', time());

// 获取用户信息
$user = $redis->hGetAll('user:1001');

// 仅获取需要的字段
$name = $redis->hGet('user:1001', 'name');
$nameAndEmail = $redis->hMGet('user:1001', ['name', 'email']);

// 2. 商品库存管理
$redis->hMSet('inventory:SKU001', [
    'total'     => 1000,
    'available' => 800,
    'reserved' => 200,
    'sold'      => 0,
]);

// 库存扣减
$redis->hIncrBy('inventory:SKU001', 'available', -1);
$redis->hIncrBy('inventory:SKU001', 'sold', 1);

// 3. 配置管理
$redis->hMSet('config:app', [
    'debug'      => 'false',
    'cache_ttl'  => '3600',
    'max_upload' => '10485760',
]);

$config = $redis->hGetAll('config:app');

List — 队列与栈

php
<?php
declare(strict_types=1);

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

// 1. 简单消息队列 — FIFO
// 生产者
$redis->rPush('queue:emails', json_encode([
    'to' => 'user@example.com',
    'subject' => '欢迎注册',
    'body' => '感谢您的注册...',
]));

// 消费者
while (true) {
    // 阻塞式获取,超时5秒
    $result = $redis->blPop('queue:emails', 5);
    if ($result) {
        $email = json_decode($result[1], true);
        // 发送邮件...
        echo "发送邮件给: {$email['to']}\n";
    }
}

// 2. 最新文章列表(朋友圈时间线)
$redis->lPush('timeline:user:1', json_encode([
    'post_id' => 101,
    'content' => '今天天气真好',
    'time' => time(),
]));
$redis->lTrim('timeline:user:1', 0, 49); // 只保留最新50条

// 获取时间线
$timeline = $redis->lRange('timeline:user:1', 0, 9);

// 3. 栈 — 后进先出
$redis->lPush('stack:undo', 'action:3');
$redis->lPush('stack:undo', 'action:2');
$redis->lPush('stack:undo', 'action:1');

$lastAction = $redis->lPop('stack:undo'); // action:1

Set — 去重与集合运算

php
<?php
declare(strict_types=1);

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

// 1. 文章标签系统
$redis->sAdd('tags:post:1', 'php', 'redis', 'tutorial', 'web');
$redis->sAdd('tags:post:2', 'php', 'mysql', 'backend');

// 查找包含特定标签的文章
// 需要维护反向索引
$redis->sAdd('tag:php:posts', 'post:1', 'post:2');
$redis->sAdd('tag:redis:posts', 'post:1');

$phpPosts = $redis->sMembers('tag:php:posts');

// 2. 共同好友
$redis->sAdd('friends:user:1', 'u2', 'u3', 'u4', 'u5');
$redis->sAdd('friends:user:2', 'u3', 'u5', 'u6', 'u7');

$commonFriends = $redis->sInter('friends:user:1', 'friends:user:2');
// ['u3', 'u5']

// 3. 随机抽奖
$redis->sAdd('lottery:participants', 'user1', 'user2', 'user3', 'user4', 'user5');

// 抽取3个中奖者(不移除)
$winners = $redis->sRandMember('lottery:participants', 3);

// 抽取1个中奖者(移除)
$winner = $redis->sPop('lottery:participants');

// 4. 用户关注/粉丝
$redis->sAdd('following:user:1', 'u2', 'u3', 'u4');
$redis->sAdd('followers:user:2', 'u1', 'u5');

// 我关注的人是否也关注了某人
$isFollowing = $redis->sIsMember('followers:user:3', 'u1');

Zset — 排行榜与评分

php
<?php
declare(strict_types=1);

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

// 1. 游戏排行榜
$scores = [
    'player1' => 2500,
    'player2' => 3800,
    'player3' => 1200,
    'player4' => 4100,
    'player5' => 3000,
];

foreach ($scores as $player => $score) {
    $redis->zAdd('leaderboard:game', $score, $player);
}

// 获取前3名
$top3 = $redis->zRevRange('leaderboard:game', 0, 2, true);
// ['player4' => 4100, 'player2' => 3800, 'player5' => 3000]

// 查看某玩家排名(从0开始)
$rank = $redis->zRevRank('leaderboard:game', 'player1'); // 3

// 获取排名范围内的玩家
$page1 = $redis->zRevRange('leaderboard:game', 0, 9, true); // 第1-10名

// 更新分数
$redis->zIncrBy('leaderboard:game', 500, 'player3'); // 1200 + 500 = 1700

// 2. 限时活动排行
$todayKey = 'leaderboard:daily:' . date('Ymd');
$redis->zAdd($todayKey, 100, 'user1');
$redis->expire($todayKey, 86400 * 7); // 保留7天

// 3. 延迟队列 — 按时间排序的任务
$timestamp = time() + 300; // 5分钟后执行
$redis->zAdd('delayed:queue', $timestamp, 'task:email:send:1001');
$redis->zAdd('delayed:queue', time() + 60, 'task:sms:verify:1002');

// 获取到期的任务
$now = time();
$tasks = $redis->zRangeByScore('delayed:queue', '-inf', $now, [
    'limit' => [0, 10],
]);

实战示例

过期时间管理

php
<?php
declare(strict_types=1);

class RedisCache
{
    private Redis $redis;
    private string $prefix;
    private int $defaultTtl;

    public function __construct(Redis $redis, string $prefix = 'cache:', int $defaultTtl = 3600)
    {
        $this->redis = $redis;
        $this->prefix = $prefix;
        $this->defaultTtl = $defaultTtl;
    }

    public function get(string $key): mixed
    {
        $data = $this->redis->get($this->prefix . $key);
        return $data !== false ? json_decode($data, true) : null;
    }

    public function set(string $key, mixed $value, ?int $ttl = null): void
    {
        $ttl = $ttl ?? $this->defaultTtl;
        $this->redis->setex($this->prefix . $key, $ttl, json_encode($value));
    }

    public function remember(string $key, callable $callback, ?int $ttl = null): mixed
    {
        $value = $this->get($key);
        if ($value !== null) {
            return $value;
        }

        $value = $callback();
        $this->set($key, $value, $ttl);
        return $value;
    }

    public function forget(string $key): void
    {
        $this->redis->del($this->prefix . $key);
    }

    public function has(string $key): bool
    {
        return (bool) $this->redis->exists($this->prefix . $key);
    }

    /**
     * 清除所有带前缀的缓存
     */
    public function flush(): int
    {
        $iterator = null;
        $deleted = 0;
        while ($keys = $this->redis->scan($iterator, $this->prefix . '*', 100)) {
            $deleted += $this->redis->del(...$keys);
        }
        return $deleted;
    }
}

键命名规范

php
<?php
// 推荐的键命名规范 — 使用冒号分隔的层级结构

// 格式: object_type:identifier:field
$patterns = [
    '用户'     => 'user:{uid}',                  // user:1001
    '用户详情' => 'user:{uid}:profile',           // user:1001:profile
    '缓存'     => 'cache:{module}:{key}',         // cache:user:1001
    '锁'       => 'lock:{resource}:{identifier}', // lock:order:123
    '队列'     => 'queue:{name}',                 // queue:emails
    '限流'     => 'rate:{identifier}:{window}',   // rate:api:202401011200
    '排行榜'   => 'rank:{type}:{date}',           // rank:daily:20240101
    '集合'     => 'set:{type}:{identifier}',      // set:tags:post:1
    '计数器'   => 'counter:{type}:{id}',          // counter:views:page:home
    '会话'     => 'session:{session_id}',          // session:abc123def
];

// 过期策略建议:
// - 热点数据: 1-24小时
// - 临时数据: 5-30分钟
// - 会话数据: 30分钟-2小时
// - 排行榜: 按周期过期(日/周/月)
// - 队列: 不设过期,消费后删除

// TTL 检查与续期
function getWithRefresh(Redis $redis, string $key, int $refreshTtl): ?string
{
    $value = $redis->get($key);
    if ($value !== false) {
        // 续期
        $redis->expire($key, $refreshTtl);
        return $value;
    }
    return null;
}

注意事项

大 Key 问题

php
<?php
// 大 Key 定义: value > 10KB 或集合元素 > 5000

// 检测大 Key(redis-cli)
// redis-cli --bigkeys

// 解决方案:
// 1. 拆分 Hash — 将大 Hash 按字段分片
//    原: HSET user:1001 field1 v1 ... fieldN vN
//    拆: HSET user:1001:0 field1 v1
//        HSET user:1001:1 field2 v2

// 2. 压缩大 String
$largeData = json_encode($bigArray);
$redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZF);
$redis->set('big:data', $largeData);

// 3. 分页获取 List
$pageSize = 100;
$page = 0;
while ($items = $redis->lRange('big:list', $page * $pageSize, ($page + 1) * $pageSize - 1)) {
    foreach ($items as $item) {
        // 处理
    }
    $page++;
    if (empty($items)) break;
}

大 Key 的危害

  • 读取大 Key 会导致 Redis 阻塞,影响其他请求
  • 删除大 Key(DEL)会造成延迟,使用 UNLINK 异步删除
  • 网络传输大 Key 占用带宽,影响并发

热点 Key 问题

php
<?php
// 热点 Key: 某个 Key 被大量并发访问
// 例如: 热门文章的缓存、明星微博的点赞数

// 解决方案1: 本地缓存 + Redis 二级缓存
class HotKeyCache
{
    private Redis $redis;
    private array $localCache = [];
    private int $localTtl = 5; // 本地缓存5秒

    public function get(string $key): mixed
    {
        // 先查本地缓存
        if (isset($this->localCache[$key]) && $this->localCache[$key]['exp'] > time()) {
            return $this->localCache[$key]['value'];
        }

        // 查 Redis
        $value = $this->redis->get($key);
        if ($value !== false) {
            $this->localCache[$key] = [
                'value' => json_decode($value, true),
                'exp' => time() + $this->localTtl,
            ];
        }

        return $value !== false ? json_decode($value, true) : null;
    }
}

// 解决方案2: Key 分片
// 将热点 Key 分散到多个 Key
function getShardedKey(string $key, int $shards = 10): string
{
    $hash = crc32($key);
    $index = abs($hash) % $shards;
    return "{$key}:shard:{$index}";
}

最佳实践

1. 数据建模原则

php
<?php
// 原则1: 一个 Redis Key 对应一个业务实体
// 原则2: 避免过度设计,Redis 不是关系型数据库
// 原则3: 合理设置过期时间,避免内存泄漏
// 原则4: 使用前缀隔离不同模块/环境
// 原则5: 控制单个 Value 大小(建议 < 10KB)

2. 数据一致性

php
<?php
// Cache-Aside 模式 — 最常用
function getUser(Redis $redis, PDO $db, int $userId): array
{
    $cacheKey = "user:{$userId}:info";

    // 1. 先查缓存
    $cached = $redis->get($cacheKey);
    if ($cached !== false) {
        return json_decode($cached, true);
    }

    // 2. 缓存未命中,查数据库
    $stmt = $db->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$userId]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($user) {
        // 3. 写入缓存
        $redis->setex($cacheKey, 3600, json_encode($user));
    }

    return $user ?: [];
}

// 更新时先更新数据库,再删除缓存
function updateUser(Redis $redis, PDO $db, int $userId, array $data): void
{
    // 1. 更新数据库
    $sets = implode(', ', array_map(
        fn($col) => "{$col} = ?",
        array_keys($data)
    ));
    $stmt = $db->prepare("UPDATE users SET {$sets} WHERE id = ?");
    $stmt->execute([...array_values($data), $userId]);

    // 2. 删除缓存(下次查询时重建)
    $redis->del("user:{$userId}:info");
}

参考链接