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 实战核心:
- ✅ 分布式锁(单实例、Redlock)
- ✅ 限流算法(固定窗口、滑动窗口、令牌桶)
- ✅ 排行榜系统(实时、分段)
- ✅ 签到系统(Bitmap)
- ✅ 消息队列(List、Stream)
- ✅ 布隆过滤器(防缓存穿透)
- ✅ 秒杀系统(库存扣减)
- ✅ 延时队列(Sorted Set)
- ✅ 统计系统(UV/PV)
- ✅ LBS 应用(Geo)
最佳实践:
- 使用 Lua 脚本保证原子性
- 合理设置过期时间
- 监控内存和性能
- 处理异常情况
推荐学习:
- 阅读 Redis 源码
- 实践搭建各种系统
- 性能测试和优化