Redis 数据结构操作
概述
Redis 提供 5 种基础数据结构(String、Hash、List、Set、Zset),每种结构适用于不同的业务场景。合理选择数据结构是 Redis 数据建模的关键。
选择指南
- String — 简单值、计数器、缓存对象(序列化后存储)
- Hash — 对象属性、用户信息、配置项
- List — 消息队列、最新列表、任务栈
- Set — 标签、去重、共同好友、随机抽奖
- Zset — 排行榜、评分系统、优先级队列
基础概念
数据类型对比
| 类型 | 存储特点 | 时间复杂度 | 典型场景 |
|---|---|---|---|
| String | 二进制安全,最大512MB | O(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:1Set — 去重与集合运算
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");
}