Redis 作为内存级 NoSQL 数据库,早已超越了"缓存"的单一角色。在实际项目中,Redis 可以胜任分布式锁、限流器、消息队列、计数器、排行榜等多种场景。本文从工程实践出发,结合 Laravel 和原生代码示例,梳理 Redis 最常用的几个应用模式及其注意事项。
这三个问题是缓存层最经典的陷阱,理解它们的区别和应对方式是用好 Redis 的第一步。
查询一个缓存和数据库都不存在的数据,请求直接打到数据库。
解决方案:
// 1. 缓存空值(短期缓存)
function getUserById($id) {
$cacheKey = "user:{$id}";
$user = Redis::get($cacheKey);
if ($user !== null) {
return $user === false ? null : json_decode($user, true);
}
// 查数据库
$user = DB::table('users')->find($id);
// 空值也缓存,TTL 60 秒
Redis::setex($cacheKey, 60, $user ? json_encode($user) : false);
return $user;
}
// 2. 布隆过滤器(预防性拦截)
// 初始化时将所有用户 ID 加入布隆过滤器
$bloom = new BloomFilter();
$bloom->add(1);
$bloom->add(2);
// 查询前先过滤
if (!$bloom->has($id)) {
return null; // 直接返回,不查 DB
}
热点 Key 过期瞬间,大量并发请求同时打到数据库。
解决方案:
// 互斥锁(SETNX)
function getHotData() {
$cacheKey = 'hot_data';
$data = Redis::get($cacheKey);
if ($data !== null) {
return json_decode($data, true);
}
// 尝试获取锁
$lockKey = 'lock:hot_data';
$lock = Redis::set($lockKey, 1, 'EX', 5, 'NX');
if ($lock) {
// 双重检查
$data = Redis::get($cacheKey);
if ($data !== null) {
Redis::del($lockKey);
return json_decode($data, true);
}
// 查数据库重建缓存
$data = DB::table('hot_table')->get();
Redis::setex($cacheKey, 3600, json_encode($data));
Redis::del($lockKey);
return $data;
}
// 没抢到锁,短暂等待后重试
usleep(10000); // 10ms
return getHotData();
}
大量 Key 同时过期,或 Redis 宕机,导致数据库崩溃。
解决方案:
// 过期时间加随机偏移
$ttl = 3600 + rand(0, 600);
Redis::setex($cacheKey, $ttl, $value);
// 多级缓存:本地缓存 + Redis
// 本地缓存( Guzzle 或 Array)
$localCache = new ArrayCache(120);
if ($localCache->has($key)) {
return $localCache->get($key);
}
$data = Redis::get($key);
if ($data) {
$localCache->set($key, $data, 60);
return json_decode($data, true);
}
Redis 分布式锁是解决并发竞争的标准手段,但实现不当容易踩坑。
class RedisLock
{
private $prefix = 'lock:';
private $ttl = 10; // 默认 10 秒
public function acquire(string $key, string $token): bool
{
return (bool) Redis::set(
$this->prefix . $key,
$token,
'EX',
$this->ttl,
'NX'
);
}
public function release(string $key, string $token): bool
{
// Lua 脚本:确保只能释放自己的锁
$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return Redis::eval($script, 1, $this->prefix . $key, $token) === 1;
}
}
// 使用示例
$lock = new RedisLock();
$token = uniqid('', true);
if ($lock->acquire('order:123', $token)) {
try {
// 处理订单
} finally {
$lock->release('order:123', $token);
}
}
高可用场景下,单机 Redis 锁不可靠,需要使用 RedLock 算法在多个独立节点上获取锁。PHP 可使用 redlock-php 库实现。
Redis 的有序集合(ZSET)是实现排行榜的天然数据结构。
// 更新分数
Redis::zadd('leaderboard:daily', $score, $userId);
// 获取 Top 10
$top10 = Redis::zrevrange('leaderboard:daily', 0, 9, 'WITHSCORES');
// 获取用户排名
$rank = Redis::zrevrank('leaderboard:daily', $userId);
// 按时间衰减:只保留近 7 天数据
Redis::zremrangebyscore('leaderboard:daily', 0, time() - 86400 * 7);
// 文章阅读数(避免数据库写压力)
Redis::incr('article:views:' . $articleId);
// 定期持久化到 MySQL
$views = Redis::get('article:views:' . $articleId);
if ($views > 0) {
DB::table('articles')->where('id', $articleId)->increment('views', $views);
Redis::set('article:views:' . $articleId, 0);
}
// 滑动窗口限流
function isRateLimited($key, $maxRequests, $windowSeconds) {
$now = microtime(true);
$windowStart = $now - $windowSeconds;
Redis::zremrangebyscore($key, 0, $windowStart);
$count = Redis::zcard($key);
if ($count >= $maxRequests) {
return true;
}
Redis::zadd($key, $now, $now . ':' . uniqid());
Redis::expire($key, $windowSeconds);
return false;
}
多实例部署时,将 Session 存入 Redis 实现共享:
// Laravel config/session.php
'driver' => env('SESSION_DRIVER', 'redis'),
'connection' => 'session',
// 也可手动操作
Redis::connection('session')->setex(
'session:' . $sessionId,
86400,
json_encode($sessionData)
);
Redis 的 List 结构可以胜任轻量级消息队列:
// 生产者
Redis::lpush('queue:emails', json_encode([
'to' => 'user@example.com',
'subject' => 'Welcome!',
]));
// 消费者(阻塞读取)
while (true) {
$data = Redis::brpop('queue:emails', 5);
if ($data) {
$email = json_decode($data[1], true);
sendEmail($email['to'], $email['subject']);
}
}
对于高可靠场景,仍建议使用 RabbitMQ 或 Kafka,Redis 队列不适合对消息可靠性要求极高的场景。
减少客户端与 Redis 的往返次数是性能优化的关键。
// 批量写入 1000 条数据(无 Pipeline:1000 次 RTT)
Redis::pipeline(function ($pipe) use ($users) {
foreach ($users as $user) {
$pipe->hmset("user:{$user['id']}", $user);
$pipe->expire("user:{$user['id']}", 3600);
}
});
// 扣减库存(原子操作)
$script = <<<'LUA'
local stock = redis.call("GET", KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
return 0
end
redis.call("DECRBY", KEYS[1], ARGV[1])
return 1
LUA;
$result = Redis::eval($script, 1, 'stock:item:100', $quantity);
if ($result === 0) {
throw new \RuntimeException('库存不足');
}
部署 Redis 时需注意以下配置:
# redis.conf 关键配置 maxmemory 4gb # 限制最大内存 maxmemory-policy allkeys-lru # 淘汰策略 save 900 1 # RDB 持久化 appendonly yes # AOF 持久化 appendfsync everysec # AOF 刷盘策略 rename-command FLUSHALL "" # 禁用危险命令 rename-command FLUSHDB "" rename-command KEYS "" requirepass yourpassword # 设置密码
监控命令:
# 慢查询日志 SLOWLOG GET 10 # 实时监控 redis-cli --stat # 大 Key 扫描 redis-cli --bigkeys # 内存分析 redis-cli MEMORY STATS
Redis 的应用场景远超缓存。实际项目中,合理使用分布式锁、排行榜、计数器和 Pipeline 等特性,可以显著提升系统性能与可靠性。关键在于:
推荐在正式项目中使用 predis 或 phpredis 扩展,并结合 Laravel 自带的 Redis 门面进行开发,可以大幅降低接入成本。