Redis 已经成了现代 PHP 应用的标准配置。但很多人的使用还停留在 set/get 字符串的层面,遇到复杂场景就不知道如何发挥了。今天这篇文章,我会从基础到进阶,带你全面掌握 PHP 与 Redis 的实战技巧------包括缓存设计、计数器、排行榜、分布式锁、消息队列等场景,并给出可运行的代码示例和注意事项。
一、Redis 基础回顾与选型
1.1 PHP 连接 Redis 的两种方式
-
phpredis:C 扩展,性能高,功能全,是生产环境首选。
-
Predis:纯 PHP 实现,兼容性好,适合开发环境或无法安装扩展的情况。
生产环境建议用 phpredis,安装:
bash
pecl install redis
连接示例:
php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 如果有密码
$redis->auth('password');
// 选择数据库
$redis->select(0);
1.2 数据结构速览
| 结构 | 典型场景 |
|---|---|
| String | 缓存对象、计数器、分布式锁 |
| Hash | 存储对象属性(如用户信息) |
| List | 消息队列、最新列表 |
| Set | 标签、唯一集合、共同好友 |
| Sorted Set | 排行榜、延时队列 |
| Bitmap | 签到统计、在线状态 |
| HyperLogLog | 基数统计(UV) |
| Stream | 可靠消息队列(Redis 5.0+) |
二、缓存设计:不仅仅是 set/get
2.1 缓存穿透、击穿、雪崩及应对
-
穿透:查询不存在的数据,绕过缓存直击数据库。
-
击穿:热点 key 过期瞬间,大量请求涌入数据库。
-
雪崩:大量 key 同时过期,或 Redis 宕机,导致数据库压力骤增。
解决方案:
php
// 1. 缓存空值(解决穿透)
$data = $redis->get('user:123');
if ($data === false) {
$user = DB::find(123);
if ($user) {
$redis->setex('user:123', 3600, serialize($user));
} else {
// 缓存空值,过期时间短
$redis->setex('user:123', 60, 'null');
}
return $user;
}
if ($data === 'null') {
return null;
}
return unserialize($data);
php
// 2. 使用互斥锁防止击穿
$data = $redis->get('hot_key');
if ($data === false) {
// 尝试获取锁
$lock = $redis->setnx('hot_key_lock', 1);
if ($lock) {
$redis->expire('hot_key_lock', 10);
$data = DB::getData();
$redis->setex('hot_key', 3600, $data);
$redis->del('hot_key_lock');
} else {
// 等待锁释放,或直接返回旧数据(可降级)
usleep(50000);
return $redis->get('hot_key');
}
}
php
// 3. 随机过期时间避免雪崩
$expire = 3600 + rand(0, 600);
$redis->setex('key', $expire, $value);
2.2 Hash 存储对象
Hash 可以高效存储和读取对象字段,比多次 set 更省内存。
php
// 存储用户信息
$redis->hMSet('user:123', [
'name' => '张三',
'email' => 'zhangsan@example.com',
'age' => 28
]);
// 获取某个字段
$name = $redis->hGet('user:123', 'name');
// 获取所有字段
$user = $redis->hGetAll('user:123');
// 自增年龄
$redis->hIncrBy('user:123', 'age', 1);
2.3 使用 Pipeline 批量操作
当需要批量执行多个 Redis 命令时,Pipeline 能减少网络往返,显著提升性能。
php
$redis->multi(Redis::PIPELINE);
for ($i = 0; $i < 1000; $i++) {
$redis->set("key:$i", $i);
}
$redis->exec();
三、排行榜:有序集合的经典应用
游戏积分榜、热门文章榜、产品销量榜,都可以用 Sorted Set 轻松实现。
php
// 添加用户积分
$redis->zAdd('rank:score', 100, 'user:1');
$redis->zIncrBy('rank:score', 50, 'user:1');
// 获取前十名(降序)
$top10 = $redis->zRevRange('rank:score', 0, 9, true);
// 获取用户排名(从0开始)
$rank = $redis->zRevRank('rank:score', 'user:1');
排行榜实时更新:每次用户操作时更新分数,无需定时计算,Redis 保持有序性。
四、计数器与限流
4.1 原子自增实现计数器
php
// 统计接口访问次数
$count = $redis->incr('api:count');
if ($count == 1) {
$redis->expire('api:count', 3600);
}
4.2 滑动窗口限流
防止用户短时间频繁操作,可用有序集合记录请求时间戳。
php
function isAllowed($userId, $action, $maxRequests = 10, $window = 60) {
$key = "rate_limit:{$userId}:{$action}";
$now = microtime(true);
$windowStart = $now - $window;
// 移除时间窗口外的记录
$redis->zRemRangeByScore($key, 0, $windowStart);
// 获取当前窗口内请求数
$current = $redis->zCard($key);
if ($current >= $maxRequests) {
return false;
}
// 添加当前请求
$redis->zAdd($key, $now, uniqid());
$redis->expire($key, $window);
return true;
}
更简单的令牌桶算法也可以用 Redis 实现。
五、分布式锁:确保互斥操作
在并发场景下,比如防止超卖、定时任务重复执行,需要分布式锁。
5.1 基于 SET NX EX 的锁
php
class RedisLock {
private $redis;
private $key;
private $token;
public function __construct($redis, $key) {
$this->redis = $redis;
$this->key = $key;
$this->token = uniqid();
}
public function lock($ttl = 10) {
// 使用 SET 命令实现原子性
$result = $this->redis->set($this->key, $this->token, ['nx', 'ex' => $ttl]);
return $result !== false;
}
public function unlock() {
// 使用 Lua 脚本确保原子性:只有持有锁的人才能释放
$script = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return $this->redis->eval($script, [$this->key, $this->token], 1);
}
}
使用示例:
php
$lock = new RedisLock($redis, 'order:lock:123');
if ($lock->lock(5)) {
try {
// 执行下单操作
processOrder();
} finally {
$lock->unlock();
}
} else {
// 获取锁失败,稍后重试或返回错误
}
六、消息队列:从 List 到 Stream
6.1 基于 List 的简单队列
生产者:
php
$redis->lPush('queue:tasks', json_encode(['task' => 'send_email', 'user' => 123]));
消费者(轮询):
php
while (true) {
$task = $redis->brPop('queue:tasks', 5); // 阻塞 5 秒
if ($task) {
$data = json_decode($task[1], true);
handleTask($data);
}
}
6.2 Redis Stream(Redis 5.0+)实现可靠队列
Stream 提供了更完善的消息队列特性:持久化、消费组、消息确认、重试等。
php
// 添加消息
$id = $redis->xAdd('stream:orders', '*', [
'order_id' => 123,
'user_id' => 456,
'amount' => 99.9
]);
// 消费组模式
$group = 'order_group';
$consumer = 'consumer_1';
$redis->xGroup('CREATE', 'stream:orders', $group, 0, true); // 创建消费组
// 读取未确认的消息
$messages = $redis->xReadGroup($group, $consumer, ['stream:orders' => '>'], 10, 0);
foreach ($messages['stream:orders'] as $id => $data) {
// 处理消息
handleOrder($data);
// 确认消息
$redis->xAck('stream:orders', $group, [$id]);
}
七、实战:结合 Laravel 使用 Redis
Laravel 内置了 Redis 支持,配置好 config/database.php 后,可以这样使用:
php
use Illuminate\Support\Facades\Redis;
// 缓存
Redis::set('name', 'Taylor');
$name = Redis::get('name');
// 发布/订阅(常用于实时通信)
Redis::publish('test-channel', json_encode(['foo' => 'bar']));
// 队列驱动设置 .env
QUEUE_CONNECTION=redis
// 任务推入队列
dispatch(new SendEmailJob($user));
八、监控与调优
8.1 常用 Redis 命令
-
INFO:查看服务器状态,包括内存、连接数、命中率。
-
MONITOR:实时监控命令(生产慎用,影响性能)。
-
SLOWLOG:记录慢查询,调整 slowlog-log-slower-than 配置。
8.2 PHP 侧优化建议
-
使用 connect() 而非 pconnect(),除非明确需要持久连接(避免连接数爆增)。
-
合理设置 maxmemory 和淘汰策略(如 allkeys-lru)。
-
避免 KEYS *,改用 SCAN 遍历。
-
使用 Pipeline 批量操作。
8.3 常见问题排查
-
连接过多:检查是否误用了 pconnect 导致连接泄漏,或调整 maxclients。
-
内存不足:分析大 key(redis-cli --bigkeys),删除无用数据,或升级内存。
-
慢查询:用 SLOWLOG GET 10 查看,优化对应命令或数据设计。
九、总结
Redis 在 PHP 应用中扮演着越来越重要的角色,从简单的缓存到复杂的消息队列、排行榜、分布式锁,都能优雅解决。掌握 Redis 的核心数据结构和使用场景,结合 PHP 的灵活特性,可以让我们在构建高并发、高性能系统时事半功倍。
最后,记住几个关键点:
-
缓存设计要考虑穿透、击穿、雪崩。
-
有序集合是排行榜的首选数据结构。
-
分布式锁要用原子操作和 Lua 脚本保证安全。
-
Stream 是更可靠的消息队列方案,适合生产环境。
-
监控和调优是持续的工作,不要等到出问题再去看。
希望这篇文章能帮你打开 Redis 在 PHP 中的更多玩法。如果你在实践中遇到问题,欢迎留言讨论。