跳到主要内容

Redis 应用场景

问题

Redis 在实际项目中有哪些应用场景?如何用 Redis 实现排行榜、限流、延迟队列等功能?

答案

典型应用场景

场景数据结构说明
缓存String / Hash最常见用途,减少数据库压力
分布式锁StringSETNX 实现互斥
排行榜ZSetscore 排序 + ZREVRANGE
计数器StringINCR 原子递增
限流String / ZSet固定窗口 / 滑动窗口
Session 共享String / Hash多实例间共享登录状态
消息队列List / StreamLPUSH + BRPOP / XADD + XREADGROUP
延迟队列ZSetscore 为执行时间
签到BitmapSETBIT + BITCOUNT
UV 统计HyperLogLogPFADD + 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); // 再删除缓存
}
为什么是删除缓存而不是更新?
  1. 避免并发问题:两个线程同时更新缓存,可能导致缓存值与最新 DB 值不一致
  2. 惰性计算:有些缓存值需要复杂计算,不一定每次写都需要更新缓存
  3. 写多读少时更高效:不必每次写都重建缓存

关于缓存一致性的详细讨论,参见前端系列 缓存与数据库一致性


常见面试问题

Q1: Redis 做缓存,如何保证缓存和数据库一致性?

答案

最常用的 Cache Aside 策略:读时先查缓存,miss 时查 DB 并回填缓存;写时先更新 DB,再删除缓存。

为什么不是"先删缓存再更新DB":并发时可能导致旧数据被重新写入缓存。

仍然可能不一致的场景:先更新 DB 后删缓存,如果删缓存失败怎么办?

解决方案:

  1. 重试机制:删缓存失败时放入消息队列,异步重试
  2. 延迟双删:更新 DB → 删缓存 → 延迟 N 毫秒 → 再删一次缓存
  3. 订阅 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:20240101rank: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 场景。

相关链接