04-Redis实战

38 阅读4分钟

Redis 实战案例

真实场景下的 Redis 应用和最佳实践

1. 分布式锁实战

1.1 基础版分布式锁

-- lock.lua (加锁)
local key = KEYS[1]
local value = ARGV[1]
local ttl = tonumber(ARGV[2])

local result = redis.call('SET', key, value, 'NX', 'EX', ttl)
return result and 1 or 0
-- unlock.lua (解锁)
local key = KEYS[1]
local value = ARGV[1]

local current = redis.call('GET', key)
if current == value then
    redis.call('DEL', key)
    return 1
else
    return 0
end

使用示例(PHP)

class RedisLock
{
    private $redis;
    
    public function lock($resource, $ttl = 10)
    {
        $key = "lock:{$resource}";
        $value = uniqid();
        
        $script = file_get_contents('lock.lua');
        $result = $this->redis->eval($script, [$key, $value, $ttl], 1);
        
        if ($result) {
            return $value;  // 返回锁的值,用于解锁
        }
        
        return false;
    }
    
    public function unlock($resource, $value)
    {
        $key = "lock:{$resource}";
        $script = file_get_contents('unlock.lua');
        
        return $this->redis->eval($script, [$key, $value], 1);
    }
}

// 使用
$lock = new RedisLock($redis);
$lockValue = $lock->lock('order:123', 10);

if ($lockValue) {
    try {
        // 执行业务逻辑
        processOrder(123);
    } finally {
        $lock->unlock('order:123', $lockValue);
    }
}

1.2 Redlock 算法(多 Redis 实例)

class Redlock
{
    private $servers;
    private $quorum;
    
    public function __construct($servers)
    {
        $this->servers = $servers;
        $this->quorum = count($servers) / 2 + 1;
    }
    
    public function lock($resource, $ttl)
    {
        $value = uniqid();
        $startTime = microtime(true);
        $success = 0;
        
        // 在所有实例上尝试获取锁
        foreach ($this->servers as $server) {
            if ($this->lockInstance($server, $resource, $value, $ttl)) {
                $success++;
            }
        }
        
        $elapsedTime = microtime(true) - $startTime;
        $validityTime = $ttl - $elapsedTime;
        
        // 超过半数成功 且 有效时间 > 0
        if ($success >= $this->quorum && $validityTime > 0) {
            return $value;
        }
        
        // 失败,释放所有锁
        $this->unlock($resource, $value);
        return false;
    }
}

2. 限流算法实战

2.1 固定窗口限流

-- fixed_window.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = redis.call('INCR', key)

if current == 1 then
    redis.call('EXPIRE', key, window)
end

if current <= limit then
    return 1  -- 允许
else
    return 0  -- 拒绝
end

使用

# 限制每 60 秒 100 次请求
EVAL "..." 1 rate:user:123 100 60

2.2 滑动窗口限流

-- sliding_window.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 删除过期记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

-- 统计当前窗口的请求数
local count = redis.call('ZCARD', key)

if count < limit then
    -- 添加当前请求
    redis.call('ZADD', key, now, now)
    redis.call('EXPIRE', key, window + 1)
    return 1  -- 允许
else
    return 0  -- 拒绝
end

2.3 令牌桶限流

-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])  -- 桶容量
local rate = tonumber(ARGV[2])      -- 生成速率(个/秒)
local now = tonumber(ARGV[3])       -- 当前时间戳

-- 获取上次更新时间和令牌数
local last_time = tonumber(redis.call('HGET', key, 'last_time') or now)
local tokens = tonumber(redis.call('HGET', key, 'tokens') or capacity)

-- 计算新增令牌
local delta = now - last_time
local new_tokens = math.min(capacity, tokens + delta * rate)

if new_tokens >= 1 then
    -- 消耗 1 个令牌
    redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_time', now)
    redis.call('EXPIRE', key, 60)
    return 1  -- 允许
else
    return 0  -- 拒绝
end

3. 排行榜系统

3.1 实时排行榜

class Leaderboard
{
    private $redis;
    private $key = 'game:leaderboard';
    
    // 更新分数
    public function updateScore($playerId, $score)
    {
        $this->redis->zincrby($this->key, $score, $playerId);
    }
    
    // 获取前 N 名
    public function getTopN($n = 10)
    {
        return $this->redis->zrevrange($this->key, 0, $n - 1, true);
    }
    
    // 获取玩家排名
    public function getRank($playerId)
    {
        $rank = $this->redis->zrevrank($this->key, $playerId);
        return $rank !== false ? $rank + 1 : null;  // 排名从 1 开始
    }
    
    // 获取玩家分数
    public function getScore($playerId)
    {
        return $this->redis->zscore($this->key, $playerId);
    }
    
    // 获取指定范围排名
    public function getRangeByRank($start, $end)
    {
        return $this->redis->zrevrange($this->key, $start - 1, $end - 1, true);
    }
    
    // 获取我的排名及周边玩家
    public function getMyRankWithContext($playerId, $context = 5)
    {
        $rank = $this->redis->zrevrank($this->key, $playerId);
        if ($rank === false) {
            return null;
        }
        
        $start = max(0, $rank - $context);
        $end = $rank + $context;
        
        return $this->redis->zrevrange($this->key, $start, $end, true);
    }
}

3.2 分段排行榜(处理大数据量)

class SegmentedLeaderboard
{
    // 按分数段分片
    // 0-999: segment:0
    // 1000-1999: segment:1
    
    public function updateScore($playerId, $score)
    {
        $segment = intdiv($score, 1000);
        $key = "leaderboard:segment:{$segment}";
        
        $this->redis->zadd($key, $score, $playerId);
    }
    
    public function getTopN($n = 100)
    {
        // 从高分段开始查询
        $results = [];
        for ($segment = 999; $segment >= 0 && count($results) < $n; $segment--) {
            $key = "leaderboard:segment:{$segment}";
            $data = $this->redis->zrevrange($key, 0, $n - count($results) - 1, true);
            $results = array_merge($results, $data);
        }
        
        return array_slice($results, 0, $n);
    }
}

4. 签到系统

4.1 使用 Bitmap 实现

class SignInSystem
{
    private $redis;
    
    // 签到
    public function signIn($userId, $date = null)
    {
        $date = $date ?: date('Ymd');
        $key = "signin:user:{$userId}:{$date}";
        $dayOfMonth = date('j');  // 1-31
        
        // 设置对应位为 1
        $this->redis->setbit($key, $dayOfMonth, 1);
        
        // 设置过期时间(保留一年)
        $this->redis->expire($key, 365 * 86400);
    }
    
    // 检查是否签到
    public function isSignedIn($userId, $day = null)
    {
        $date = date('Ymd');
        $key = "signin:user:{$userId}:{$date}";
        $dayOfMonth = $day ?: date('j');
        
        return $this->redis->getbit($key, $dayOfMonth) == 1;
    }
    
    // 统计本月签到天数
    public function getMonthSignInCount($userId)
    {
        $date = date('Ymd');
        $key = "signin:user:{$userId}:{$date}";
        
        return $this->redis->bitcount($key);
    }
    
    // 获取连续签到天数
    public function getContinuousSignInDays($userId)
    {
        $date = date('Ymd');
        $key = "signin:user:{$userId}:{$date}";
        $today = date('j');
        $days = 0;
        
        for ($day = $today; $day >= 1; $day--) {
            if ($this->redis->getbit($key, $day) == 1) {
                $days++;
            } else {
                break;
            }
        }
        
        return $days;
    }
}

5. 消息队列实战

5.1 使用 List 实现简单队列

class SimpleQueue
{
    private $redis;
    
    // 生产者
    public function push($queue, $message)
    {
        $this->redis->lpush("queue:{$queue}", json_encode($message));
    }
    
    // 消费者(阻塞)
    public function pop($queue, $timeout = 0)
    {
        $result = $this->redis->brpop("queue:{$queue}", $timeout);
        
        if ($result) {
            return json_decode($result[1], true);
        }
        
        return null;
    }
    
    // 队列长度
    public function length($queue)
    {
        return $this->redis->llen("queue:{$queue}");
    }
}

5.2 使用 Stream 实现可靠队列

class StreamQueue
{
    private $redis;
    
    // 生产者
    public function produce($stream, $data)
    {
        return $this->redis->xadd($stream, '*', $data);
    }
    
    // 创建消费者组
    public function createGroup($stream, $group)
    {
        try {
            $this->redis->xgroup('CREATE', $stream, $group, '0');
        } catch (\Exception $e) {
            // 组已存在
        }
    }
    
    // 消费者
    public function consume($stream, $group, $consumer, $count = 10)
    {
        $messages = $this->redis->xreadgroup(
            $group,
            $consumer,
            [$stream => '>'],
            $count
        );
        
        return $messages;
    }
    
    // 确认消息
    public function ack($stream, $group, $messageId)
    {
        $this->redis->xack($stream, $group, [$messageId]);
    }
    
    // 获取 Pending 消息
    public function getPending($stream, $group)
    {
        return $this->redis->xpending($stream, $group);
    }
}

6. 布隆过滤器

6.1 使用 RedisBloom 模块

# 安装 RedisBloom
# Docker
docker run -d -p 6379:6379 redislabs/rebloom:latest

# 命令
BF.ADD bloom:users user123    # 添加
BF.EXISTS bloom:users user123 # 检查是否存在
BF.MADD bloom:users user1 user2 user3  # 批量添加

6.2 自实现布隆过滤器

class BloomFilter
{
    private $redis;
    private $key;
    private $hashCount;  // 哈希函数数量
    private $bitSize;    // 位数组大小
    
    public function __construct($redis, $key, $expectedElements = 1000000, $errorRate = 0.01)
    {
        $this->redis = $redis;
        $this->key = $key;
        
        // 计算最优参数
        $this->bitSize = ceil(-($expectedElements * log($errorRate)) / (log(2) ** 2));
        $this->hashCount = ceil(($this->bitSize / $expectedElements) * log(2));
    }
    
    // 添加元素
    public function add($element)
    {
        for ($i = 0; $i < $this->hashCount; $i++) {
            $hash = $this->hash($element, $i);
            $position = $hash % $this->bitSize;
            $this->redis->setbit($this->key, $position, 1);
        }
    }
    
    // 检查元素是否存在
    public function exists($element)
    {
        for ($i = 0; $i < $this->hashCount; $i++) {
            $hash = $this->hash($element, $i);
            $position = $hash % $this->bitSize;
            
            if (!$this->redis->getbit($this->key, $position)) {
                return false;  // 一定不存在
            }
        }
        
        return true;  // 可能存在
    }
    
    private function hash($element, $seed)
    {
        return crc32($element . $seed);
    }
}

7. 秒杀系统

7.1 库存预扣(Lua 脚本)

-- seckill.lua
local stock_key = KEYS[1]       -- 库存键
local order_key = KEYS[2]       -- 订单集合键
local user_id = ARGV[1]         -- 用户 ID
local quantity = tonumber(ARGV[2])  -- 购买数量

-- 1. 检查是否已购买
if redis.call('SISMEMBER', order_key, user_id) == 1 then
    return {-1, "Already purchased"}
end

-- 2. 获取库存
local stock = tonumber(redis.call('GET', stock_key))

if not stock or stock < quantity then
    return {0, "Out of stock"}
end

-- 3. 扣减库存
redis.call('DECRBY', stock_key, quantity)

-- 4. 记录用户已购买
redis.call('SADD', order_key, user_id)

return {1, "Success"}

PHP 实现

class SeckillService
{
    private $redis;
    
    public function seckill($productId, $userId, $quantity = 1)
    {
        $stockKey = "seckill:stock:{$productId}";
        $orderKey = "seckill:orders:{$productId}";
        
        $script = file_get_contents('seckill.lua');
        $result = $this->redis->eval($script, 
            [$stockKey, $orderKey, $userId, $quantity], 
            2
        );
        
        [$code, $message] = $result;
        
        if ($code == 1) {
            // 成功,异步创建订单
            $this->createOrderAsync($productId, $userId, $quantity);
            return ['success' => true];
        }
        
        return ['success' => false, 'message' => $message];
    }
    
    private function createOrderAsync($productId, $userId, $quantity)
    {
        // 发送消息到队列
        $this->redis->lpush('queue:orders', json_encode([
            'product_id' => $productId,
            'user_id' => $userId,
            'quantity' => $quantity,
            'created_at' => time(),
        ]));
    }
}

8. 延时队列

8.1 基于 Sorted Set 实现

class DelayQueue
{
    private $redis;
    private $key = 'delay:queue';
    
    // 添加延时任务
    public function add($taskId, $delaySeconds, $data)
    {
        $executeTime = time() + $delaySeconds;
        $taskData = json_encode([
            'id' => $taskId,
            'data' => $data,
        ]);
        
        $this->redis->zadd($this->key, $executeTime, $taskData);
    }
    
    // 获取到期任务(Lua 脚本保证原子性)
    public function getReadyTasks($limit = 100)
    {
        $script = <<<LUA
local key = KEYS[1]
local now = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])

-- 获取到期的任务
local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)

if #tasks == 0 then
    return {}
end

-- 删除已获取的任务
for i, task in ipairs(tasks) do
    redis.call('ZREM', key, task)
end

return tasks
LUA;
        
        $tasks = $this->redis->eval($script, [$this->key, time(), $limit], 1);
        
        $result = [];
        foreach ($tasks as $task) {
            $result[] = json_decode($task, true);
        }
        
        return $result;
    }
    
    // 消费者(定时执行)
    public function consume()
    {
        while (true) {
            $tasks = $this->getReadyTasks();
            
            foreach ($tasks as $task) {
                $this->processTask($task);
            }
            
            sleep(1);  // 1 秒检查一次
        }
    }
    
    private function processTask($task)
    {
        // 处理任务逻辑
        echo "Processing task: " . $task['id'] . "\n";
    }
}

9. 计数器和统计

9.1 UV 统计(HyperLogLog)

class UVCounter
{
    private $redis;
    
    // 记录访问
    public function record($page, $userId)
    {
        $key = "uv:{$page}:" . date('Ymd');
        $this->redis->pfadd($key, $userId);
        $this->redis->expire($key, 7 * 86400);  // 保留 7 天
    }
    
    // 获取当天 UV
    public function getToday($page)
    {
        $key = "uv:{$page}:" . date('Ymd');
        return $this->redis->pfcount($key);
    }
    
    // 获取本周 UV
    public function getWeek($page)
    {
        $keys = [];
        for ($i = 0; $i < 7; $i++) {
            $date = date('Ymd', strtotime("-{$i} days"));
            $keys[] = "uv:{$page}:{$date}";
        }
        
        // 合并统计
        $tempKey = "uv:{$page}:week:" . date('W');
        call_user_func_array([$this->redis, 'pfmerge'], array_merge([$tempKey], $keys));
        
        $count = $this->redis->pfcount($tempKey);
        $this->redis->expire($tempKey, 86400);
        
        return $count;
    }
}

9.2 PV 统计(String)

class PVCounter
{
    private $redis;
    
    // 记录访问
    public function record($page)
    {
        $key = "pv:{$page}:" . date('Ymd');
        $this->redis->incr($key);
        $this->redis->expire($key, 7 * 86400);
    }
    
    // 获取今日 PV
    public function getToday($page)
    {
        $key = "pv:{$page}:" . date('Ymd');
        return $this->redis->get($key) ?: 0;
    }
}

10. 附近的人(Geo)

10.1 实现

class NearbyService
{
    private $redis;
    private $key = 'locations:users';
    
    // 更新位置
    public function updateLocation($userId, $longitude, $latitude)
    {
        $this->redis->geoadd($this->key, $longitude, $latitude, $userId);
    }
    
    // 查找附近的人
    public function findNearby($longitude, $latitude, $radius = 5, $unit = 'km')
    {
        return $this->redis->georadius(
            $this->key,
            $longitude,
            $latitude,
            $radius,
            $unit,
            ['WITHCOORD', 'WITHDIST', 'COUNT' => 20]
        );
    }
    
    // 计算两个用户的距离
    public function getDistance($userId1, $userId2, $unit = 'km')
    {
        return $this->redis->geodist($this->key, $userId1, $userId2, $unit);
    }
}

11. 总结

Redis 实战核心:

  1. ✅ 分布式锁(单实例、Redlock)
  2. ✅ 限流算法(固定窗口、滑动窗口、令牌桶)
  3. ✅ 排行榜系统(实时、分段)
  4. ✅ 签到系统(Bitmap)
  5. ✅ 消息队列(List、Stream)
  6. ✅ 布隆过滤器(防缓存穿透)
  7. ✅ 秒杀系统(库存扣减)
  8. ✅ 延时队列(Sorted Set)
  9. ✅ 统计系统(UV/PV)
  10. ✅ LBS 应用(Geo)

最佳实践

  • 使用 Lua 脚本保证原子性
  • 合理设置过期时间
  • 监控内存和性能
  • 处理异常情况

推荐学习

  • 阅读 Redis 源码
  • 实践搭建各种系统
  • 性能测试和优化