Redis 应用场景
问题
Redis 在实际项目中有哪些应用场景?如何用 Redis 实现排行榜、限流、延迟队列等功能?
答案
典型应用场景
| 场景 | 数据结构 | 说明 |
|---|---|---|
| 缓存 | String / Hash | 最常见用途,减少数据库压力 |
| 分布式锁 | String | SETNX 实现互斥 |
| 排行榜 | ZSet | score 排序 + ZREVRANGE |
| 计数器 | String | INCR 原子递增 |
| 限流 | String / ZSet | 固定窗口 / 滑动窗口 |
| Session 共享 | String / Hash | 多实例间共享登录状态 |
| 消息队列 | List / Stream | LPUSH + BRPOP / XADD + XREADGROUP |
| 延迟队列 | ZSet | score 为执行时间 |
| 签到 | Bitmap | SETBIT + BITCOUNT |
| UV 统计 | HyperLogLog | PFADD + PFCOUNT |
| 地理位置 | GEO | 附近的人 / 门店 |
| 布隆过滤器 | RedisBloom | 判断元素是否存在 |
排行榜实现
实时排行榜
# 用户得分 +10
ZINCRBY rank:game 10 user:1001
# 获取 Top 10(分数从高到低)
ZREVRANGE rank:game 0 9 WITHSCORES
# 获取用户排名(0-based)
ZREVRANK rank:game user:1001
# 获取用户分数
ZSCORE rank:game user:1001
限流实现
固定窗口限流
// 限制每分钟最多 100 次请求
public boolean isAllowed(String userId) {
String key = "rate:" + userId + ":" + (System.currentTimeMillis() / 60000);
Long count = redis.incr(key);
if (count == 1) {
redis.expire(key, 60); // 首次设置过期时间
}
return count <= 100;
}
滑动窗口限流(ZSet)
public boolean isAllowed(String userId, int maxCount, int windowSeconds) {
String key = "rate:" + userId;
long now = System.currentTimeMillis();
long windowStart = now - windowSeconds * 1000L;
// 使用 Pipeline 原子执行
Pipeline pipe = jedis.pipelined();
pipe.zremrangeByScore(key, 0, windowStart); // 移除窗口外的记录
pipe.zadd(key, now, now + ":" + UUID.randomUUID()); // 添加当前请求
pipe.zcard(key); // 统计窗口内请求数
pipe.expire(key, windowSeconds); // 设置过期时间
List<Object> results = pipe.syncAndReturnAll();
Long count = (Long) results.get(2);
return count <= maxCount;
}
延迟队列
使用 ZSet 实现:score 为任务的执行时间戳,消费者轮询取出到期任务。
延迟队列
// 生产者:添加延迟任务
public void addDelayTask(String taskId, long delayMs) {
double executeTime = System.currentTimeMillis() + delayMs;
redis.zadd("delay:queue", executeTime, taskId);
}
// 消费者:轮询取出到期任务
public void consumeDelayTasks() {
while (true) {
// 取出 score ≤ 当前时间的任务
Set<String> tasks = redis.zrangeByScore(
"delay:queue", 0, System.currentTimeMillis(), 0, 1);
if (tasks.isEmpty()) {
Thread.sleep(500); // 无任务时等待
continue;
}
String taskId = tasks.iterator().next();
// 原子性地移除并执行(防止重复消费)
Long removed = redis.zrem("delay:queue", taskId);
if (removed > 0) {
processTask(taskId);
}
}
}
分布式 Session
Spring Session + Redis
// 添加依赖后自动将 Session 存储到 Redis
// spring-session-data-redis
// application.yml
// spring:
// session:
// store-type: redis
// timeout: 30m
Session 以 Hash 结构存储在 Redis 中,key 为 spring:session:sessions:{sessionId}。
缓存策略模式
| 模式 | 读 | 写 | 适用场景 |
|---|---|---|---|
| Cache Aside | 先读缓存,miss 时读 DB 并回填 | 先写 DB,再删缓存 | 最常用,适合读多写少 |
| Read Through | 缓存层自动加载数据 | - | 缓存框架支持 |
| Write Through | - | 同步写缓存和 DB | 写一致性要求高 |
| Write Behind | - | 写缓存后异步写 DB | 写性能要求高 |
Cache Aside 模式
// 读
public Object get(String key) {
Object value = redis.get(key);
if (value == null) {
value = db.get(key);
if (value != null) {
redis.set(key, value, TTL);
}
}
return value;
}
// 写
public void update(String key, Object value) {
db.update(key, value); // 先更新数据库
redis.del(key); // 再删除缓存
}
为什么是删除缓存而不是更新?
- 避免并发问题:两个线程同时更新缓存,可能导致缓存值与最新 DB 值不一致
- 惰性计算:有些缓存值需要复杂计算,不一定每次写都需要更新缓存
- 写多读少时更高效:不必每次写都重建缓存
关于缓存一致性的详细讨论,参见前端系列 缓存与数据库一致性。
常见面试问题
Q1: Redis 做缓存,如何保证缓存和数据库一致性?
答案:
最常用的 Cache Aside 策略:读时先查缓存,miss 时查 DB 并回填缓存;写时先更新 DB,再删除缓存。
为什么不是"先删缓存再更新DB":并发时可能导致旧数据被重新写入缓存。
仍然可能不一致的场景:先更新 DB 后删缓存,如果删缓存失败怎么办?
解决方案:
- 重试机制:删缓存失败时放入消息队列,异步重试
- 延迟双删:更新 DB → 删缓存 → 延迟 N 毫秒 → 再删一次缓存
- 订阅 binlog:通过 Canal 监听 MySQL binlog,自动删除对应缓存
Q2: 如何用 Redis 实现排行榜?
答案:
使用 Sorted Set(ZSet):
- 更新分数:
ZINCRBY rank 10 user:1001(原子操作) - 查看 Top N:
ZREVRANGE rank 0 9 WITHSCORES(O(log n + m)) - 查看排名:
ZREVRANK rank user:1001(O(log n)) - 实时性:ZSet 操作都是原子的,天然支持实时排行
如果需要分时段排行(日榜、周榜),使用不同的 key:rank:daily:20240101、rank:weekly:202401W01,通过 ZUNIONSTORE 合并。
Q3: Redis 适合做消息队列吗?
答案:
Redis 可以做轻量级消息队列,但有局限:
| 方案 | 优点 | 缺点 |
|---|---|---|
| List (LPUSH + BRPOP) | 简单 | 无 ACK、无消费者组、消息消费即丢失 |
| Stream (Redis 5.0+) | 消费者组、ACK、消息回溯 | 不如专业 MQ 可靠 |
适合的场景:低延迟、消息量不大、可接受少量消息丢失。
不适合:需要高可靠消息投递、复杂路由、海量消息堆积的场景,应使用 Kafka、RocketMQ 等专业消息队列。
Q4: Redis 的 GEO 怎么使用?
答案:
Redis GEO 底层基于 ZSet + GeoHash:
# 添加位置
GEOADD shops 116.397128 39.916527 "coffee_shop_1"
GEOADD shops 116.405285 39.904989 "coffee_shop_2"
# 查找附近 3km 的店铺
GEORADIUS shops 116.397128 39.916527 3 km WITHCOORD WITHDIST COUNT 10 ASC
# Redis 6.2+ 推荐使用 GEOSEARCH
GEOSEARCH shops FROMLONLAT 116.397128 39.916527 BYRADIUS 3 km ASC COUNT 10
适合"附近的人"、"附近的门店"等 LBS 场景。