Redis 实战指南:从缓存到分布式锁的核心应用场景

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 分布式锁是解决并发竞争的标准手段,但实现不当容易踩坑。

正确姿势(SET NX EX + Lua)

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);
    }
}

红锁(RedLock)

高可用场景下,单机 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 存储与消息队列

Session 共享

多实例部署时,将 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 队列不适合对消息可靠性要求极高的场景。

五、Pipeline 与 Lua 脚本优化

减少客户端与 Redis 的往返次数是性能优化的关键。

Pipeline 批量操作

// 批量写入 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);
    }
});

Lua 脚本保证原子性

// 扣减库存(原子操作)
$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 等特性,可以显著提升系统性能与可靠性。关键在于:

  • 理解各场景的坑并提前防范(穿透、击穿、雪崩)
  • 用好原子操作(Lua、SET NX)解决并发问题
  • 选择合适的持久化策略和淘汰策略

推荐在正式项目中使用 predisphpredis 扩展,并结合 Laravel 自带的 Redis 门面进行开发,可以大幅降低接入成本。