Redis 常用命令封装
概述
Redis 支持多种数据类型的操作命令。phpredis 扩展将这些命令封装为 PHP 方法,可以直接调用。本页将按数据类型分类,介绍常用命令的 PHP 使用方式和封装技巧。
命名规则
phpredis 方法名与 Redis 命令名对应,但使用驼峰命名。例如 SET 命令对应 $redis->set(),HGETALL 对应 $redis->hGetAll()。
基础概念
命令分类概览
| 数据类型 | 常用命令 | 特点 |
|---|---|---|
| String | SET/GET/INCR/DECR/SETEX/GETSET | 最基础的数据类型 |
| Hash | HSET/HGET/HGETALL/HMSET/HDEL | 对象存储 |
| List | LPUSH/RPUSH/LPOP/RPOP/LRANGE/LLEN | 有序列表 |
| Set | SADD/SREM/SMEMBERS/SISMEMBER/SCARD | 无序集合 |
| Zset | ZADD/ZSCORE/ZRANGE/ZRANK/ZREM | 有序集合 |
| Key | DEL/EXISTS/EXPIRE/TTL/SCAN/TYPE | 键管理 |
语法与代码
String 操作
php
<?php
declare(strict_types=1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 基本读写
$redis->set('user:name', '张三');
$name = $redis->get('user:name'); // "张三"
// 设置过期时间(秒)
$redis->setex('code:12345', 300, 'ABC123'); // 5分钟后过期
// SET NX — 只在键不存在时设置(分布式锁基础)
$locked = $redis->set('lock:resource:1', 'token', [
'nx' => true, // 仅当键不存在时
'ex' => 30, // 30秒过期
]);
// INCR/DECR — 原子计数器
$redis->set('counter:page_views', 0);
$redis->incr('counter:page_views'); // 1
$redis->incrBy('counter:page_views', 10); // 11
$redis->decr('counter:page_views'); // 10
// MSET/MGET — 批量操作
$redis->mSet(['key1' => 'v1', 'key2' => 'v2', 'key3' => 'v3']);
$values = $redis->mGet(['key1', 'key2', 'key3']); // ['v1', 'v2', 'v3']
// GETSET — 原子获取并设置
$oldValue = $redis->getSet('config:version', '2.0');
// APPEND — 追加字符串
$redis->append('log:app', "错误日志\n");Hash 操作
php
<?php
declare(strict_types=1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 设置 Hash 字段
$redis->hSet('user:1001', 'name', '李四');
$redis->hSet('user:1001', 'email', 'lisi@example.com');
$redis->hSet('user:1001', 'age', 28);
// HMSET — 批量设置
$redis->hMSet('user:1001', [
'name' => '李四',
'email' => 'lisi@example.com',
'age' => 28,
'city' => '北京',
]);
// HGET — 获取单个字段
$email = $redis->hGet('user:1001', 'email');
// HGETALL — 获取所有字段
$user = $redis->hGetAll('user:1001');
// ['name' => '李四', 'email' => '...', 'age' => '28', 'city' => '北京']
// HDEL — 删除字段
$redis->hDel('user:1001', 'age');
// HEXISTS — 判断字段是否存在
$exists = $redis->hExists('user:1001', 'name'); // true
// HINCRBY — 原子递增
$redis->hIncrBy('user:1001', 'login_count', 1);
// HKEYS/HVALS — 获取所有键/值
$keys = $redis->hKeys('user:1001'); // ['name', 'email', 'city']
$values = $redis->hVals('user:1001'); // ['李四', '...', '北京']
// HLEN — 获取字段数量
$fieldCount = $redis->hLen('user:1001');List 操作
php
<?php
declare(strict_types=1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// LPUSH/RPUSH — 从左/右推入
$redis->rPush('queue:tasks', 'task1');
$redis->rPush('queue:tasks', 'task2');
$redis->rPush('queue:tasks', 'task3');
// LPOP/RPOP — 从左/右弹出
$task = $redis->lPop('queue:tasks'); // "task1"
// LRANGE — 获取范围内的元素
$tasks = $redis->lRange('queue:tasks', 0, -1); // 所有元素
$tasks = $redis->lRange('queue:tasks', 0, 9); // 前10个
// LLEN — 获取列表长度
$length = $redis->lLen('queue:tasks');
// BLPOP/BRPOP — 阻塞式弹出(队列消费)
$task = $redis->blPop('queue:tasks', 5); // 阻塞等待最多5秒
// LREM — 移除指定值
$redis->lRem('queue:tasks', 'task2', 1); // 从左移除1个
// LINDEX — 获取指定位置的元素
$first = $redis->lIndex('queue:tasks', 0);
// LTRIM — 截取列表(保留指定范围)
$redis->lTrim('queue:tasks', 0, 99); // 只保留前100个Set 操作
php
<?php
declare(strict_types=1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// SADD — 添加成员
$redis->sAdd('tags:post:1', 'php', 'redis', 'tutorial');
$redis->sAdd('tags:post:2', 'php', 'mysql', 'backend');
// SMEMBERS — 获取所有成员
$tags = $redis->sMembers('tags:post:1'); // ['php', 'redis', 'tutorial']
// SISMEMBER — 判断是否是成员
$isMember = $redis->sIsMember('tags:post:1', 'php'); // true
// SCARD — 获取成员数量
$count = $redis->sCard('tags:post:1');
// SREM — 移除成员
$redis->sRem('tags:post:1', 'tutorial');
// 集合运算
$redis->sAdd('set:a', 1, 2, 3, 4);
$redis->sAdd('set:b', 3, 4, 5, 6);
$intersection = $redis->sInter('set:a', 'set:b'); // [3, 4]
$union = $redis->sUnion('set:a', 'set:b'); // [1, 2, 3, 4, 5, 6]
$diff = $redis->sDiff('set:a', 'set:b'); // [1, 2]
// SPOP — 随机弹出
$random = $redis->sPop('tags:post:1');
// SRANDMEMBER — 随机获取(不移除)
$random = $redis->sRandMember('tags:post:1');Zset(有序集合)操作
php
<?php
declare(strict_types=1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// ZADD — 添加成员(分数)
$redis->zAdd('leaderboard', 100, 'player1');
$redis->zAdd('leaderboard', [
'player2' => 200,
'player3' => 150,
'player4' => 180,
]);
// ZSCORE — 获取分数
$score = $redis->zScore('leaderboard', 'player1'); // 100
// ZRANGE — 按分数升序
$rank = $redis->zRange('leaderboard', 0, -1); // 所有成员(升序)
// ZREVRANGE — 按分数降序
$top10 = $redis->zRevRange('leaderboard', 0, 9); // 前10名(降序)
$top10WithScore = $redis->zRevRange('leaderboard', 0, 9, true); // 带分数
// ZRANK/ZREVRANK — 获取排名(从0开始)
$rank = $redis->zRevRank('leaderboard', 'player2'); // 0(第一名)
// ZINCRBY — 增加分数
$redis->zIncrBy('leaderboard', 50, 'player1'); // 100 + 50 = 150
// ZRANGEBYSCORE — 按分数范围查询
$players = $redis->zRangeByScore('leaderboard', 100, 200);
$playersWithScores = $redis->zRangeByScore('leaderboard', 100, 200, [
'withscores' => true,
'limit' => [0, 10],
]);
// ZREM — 移除成员
$redis->zRem('leaderboard', 'player4');
// ZCARD — 获取成员数
$count = $redis->zCard('leaderboard');
// ZCOUNT — 按分数范围计数
$count = $redis->zCount('leaderboard', 100, 200);Pipeline 批量操作
php
<?php
declare(strict_types=1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Pipeline — 减少网络往返,批量执行命令
$redis->multi(Redis::PIPELINE);
$redis->set('key1', 'value1');
$redis->set('key2', 'value2');
$redis->set('key3', 'value3');
$redis->incr('counter1');
$redis->incr('counter2');
// 所有命令一次性发送到服务器
$results = $redis->exec();
// [true, true, true, 1, 2]
// Transaction(事务)— 命令按顺序执行,不会被其他客户端打断
$redis->multi(Redis::MULTI);
$redis->set('balance:1', 1000);
$redis->decrBy('balance:1', 100);
$redis->incrBy('balance:2', 100);
$results = $redis->exec();Pipeline vs Transaction
- Pipeline — 打包发送多个命令,减少网络延迟,但命令之间不是原子的
- Transaction (MULTI) — 原子执行一组命令,不会被其他客户端打断
- 两者可以结合使用:
$redis->multi(Redis::PIPELINE)在 Pipeline 中执行事务
Lua 脚本执行
php
<?php
declare(strict_types=1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 限流脚本 — 令牌桶算法
$luaScript = <<<'LUA'
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or '0')
if current >= limit then
return 0 -- 超过限制
end
redis.call('INCR', key)
if current == 0 then
redis.call('EXPIRE', key, window)
end
return 1 -- 允许
LUA;
// EVAL 执行
$allowed = $redis->eval($luaScript, ['rate:user:1', 10, 60], 1);
// 参数: 脚本, [KEYS数组 + ARGV数组], KEYS数量
// EVALSHA — 使用 SHA1 缓存(性能更好)
$sha = $redis->script('load', $luaScript);
$allowed = $redis->evalSha($sha, ['rate:user:1', 10, 60], 1);
if ($allowed) {
echo "请求允许\n";
} else {
echo "请求被限流\n";
}实战示例
分布式锁实现
php
<?php
declare(strict_types=1);
class DistributedLock
{
private Redis $redis;
private string $prefix = 'lock:';
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
/**
* 获取锁
*/
public function acquire(string $resource, int $ttl = 30, int $retry = 3, int $delay = 200): ?string
{
$token = uniqid((string) mt_rand(), true);
for ($i = 0; $i < $retry; $i++) {
$acquired = $this->redis->set(
$this->prefix . $resource,
$token,
['nx' => true, 'ex' => $ttl]
);
if ($acquired) {
return $token;
}
if ($i < $retry - 1) {
usleep($delay * 1000);
}
}
return null; // 获取失败
}
/**
* 释放锁 — 使用 Lua 脚本确保原子性
*/
public function release(string $resource, string $token): bool
{
$script = <<<'LUA'
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0
LUA;
return (bool) $this->redis->eval(
$script,
[$this->prefix . $resource, $token],
1
);
}
}
// 使用示例
$lock = new DistributedLock($redis);
$token = $lock->acquire('order:process:123', 30);
if ($token !== null) {
try {
// 执行业务逻辑
echo "获取锁成功,处理订单\n";
} finally {
$lock->release('order:process:123', $token);
}
} else {
echo "获取锁失败,资源繁忙\n";
}限流器
php
<?php
declare(strict_types=1);
class RateLimiter
{
private Redis $redis;
private string $prefix = 'rate:';
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
/**
* 滑动窗口限流
*/
public function isAllowed(string $key, int $maxRequests, int $windowSeconds): bool
{
$now = time();
$windowKey = $this->prefix . $key;
// 移除窗口外的记录
$this->redis->zRemRangeByScore($windowKey, '-inf', $now - $windowSeconds);
// 获取当前窗口内的请求数
$count = $this->redis->zCard($windowKey);
if ($count >= $maxRequests) {
return false;
}
// 添加当前请求
$this->redis->zAdd($windowKey, $now, (string) $now . ':' . mt_rand());
$this->redis->expire($windowKey, $windowSeconds);
return true;
}
/**
* 令牌桶限流
*/
public function tokenBucket(string $key, int $maxTokens, int $refillRate): bool
{
$script = <<<'LUA'
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call("HMGET", key, "tokens", "last_refill")
local tokens = tonumber(data[1] or max_tokens)
local last_refill = tonumber(data[2] or now)
-- 计算补充的令牌数
local elapsed = now - last_refill
local new_tokens = math.min(max_tokens, tokens + elapsed * refill_rate)
if new_tokens > 0 then
redis.call("HMSET", key, "tokens", new_tokens - 1, "last_refill", now)
redis.call("EXPIRE", key, math.ceil(max_tokens / refill_rate) + 10)
return 1
end
return 0
LUA;
return (bool) $this->redis->eval(
$script,
[$this->prefix . $key, $maxTokens, $refillRate, time()],
1
);
}
}注意事项
Key 管理操作
php
<?php
// SCAN — 安全的键遍历(不阻塞服务器)
$iterator = null;
$pattern = 'cache:*';
$count = 100;
while ($keys = $redis->scan($iterator, $pattern, $count)) {
foreach ($keys as $key) {
$ttl = $redis->ttl($key);
echo "{$key} (TTL: {$ttl}s)\n";
}
}
// EXPIRE / TTL — 过期时间管理
$redis->set('temp:data', 'value');
$redis->expire('temp:data', 3600); // 1小时后过期
$ttl = $redis->ttl('temp:data'); // 剩余秒数
// PERSIST — 取消过期
$redis->persist('temp:data');
// EXISTS — 判断键是否存在
$exists = $redis->exists('user:1001'); // true/false
// TYPE — 获取键的数据类型
$type = $redis->type('user:1001'); // string/hash/list/set/zset/none
// DEL — 删除键
$redis->del('key1', 'key2', 'key3'); // 返回删除数量
// UNLINK — 异步删除(不阻塞)
$redis->unlink('large:hash:key');避免 KEYS 命令
KEYS pattern 会遍历整个数据库,在大数据量下会阻塞 Redis 服务器。永远使用 SCAN 替代 KEYS。
最佳实践
1. 命令批量优化
php
<?php
// 推荐: Pipeline 批量操作
$redis->multi(Redis::PIPELINE);
for ($i = 0; $i < 1000; $i++) {
$redis->set("item:{$i}", json_encode(['id' => $i, 'data' => "item_{$i}"]));
}
$redis->exec();
// 不推荐: 逐条执行(1000次网络往返)
for ($i = 0; $i < 1000; $i++) {
$redis->set("item:{$i}", json_encode(['id' => $i, 'data' => "item_{$i}"]));
}2. 大 Key 处理
php
<?php
// 拆分大 Hash — 使用 HSCAN 分批处理
$hashKey = 'large:hash:key';
$cursor = null;
while ($fields = $redis->hScan($hashKey, $cursor, '*', 100)) {
foreach ($fields as $field => $value) {
// 逐个处理字段
}
// $cursor 在 hScan 中自动更新
if ($cursor === 0) break;
}
// 拆分策略: 将大 Hash 拆分为多个小 Hash
// 原: user:1001 → {name, email, age, ...}
// 拆: user:1001:base → {name, email}, user:1001:extra → {age, city}