Redis 的高性能和丰富的数据类型使其在各种业务场景中大放异彩。本文汇总 Redis 在实际项目中的经典应用场景与最佳实践。
一、排行榜(Leaderboard)¶
1.1 场景描述¶
典型应用: 游戏积分榜、文章点赞榜、销售排行榜、直播热度榜。
核心需求:
- 实时更新排名。
- 高并发读写(如百万用户同时刷新排行榜)。
- 支持范围查询(如 Top 100)。
1.2 技术方案:ZSet(有序集合)¶
数据结构:
ZADD leaderboard:2025 100 "user:1001"
ZADD leaderboard:2025 200 "user:1002"
ZADD leaderboard:2025 150 "user:1003"
常用操作:
更新分数(增加积分)¶
ZINCRBY leaderboard:2025 10 "user:1001"
查询 Top N(降序)¶
ZREVRANGE leaderboard:2025 0 99 WITHSCORES
# 返回前 100 名及其分数
查询用户排名¶
ZREVRANK leaderboard:2025 "user:1001"
# 返回排名(0 表示第一名)
查询用户分数¶
ZSCORE leaderboard:2025 "user:1001"
查询分数区间的用户¶
ZRANGEBYSCORE leaderboard:2025 100 200 WITHSCORES
1.3 Java 实现示例¶
@Service
public class LeaderboardService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LEADERBOARD_KEY = "leaderboard:2025";
// 增加积分
public void addScore(String userId, double score) {
redisTemplate.opsForZSet().incrementScore(LEADERBOARD_KEY, userId, score);
}
// 获取 Top N
public List<LeaderboardVO> getTopN(int topN) {
Set<ZSetOperations.TypedTuple<String>> result =
redisTemplate.opsForZSet()
.reverseRangeWithScores(LEADERBOARD_KEY, 0, topN - 1);
return result.stream()
.map(tuple -> new LeaderboardVO(
tuple.getValue(),
tuple.getScore(),
getRank(tuple.getValue()) + 1 // 排名从 1 开始
))
.collect(Collectors.toList());
}
// 获取用户排名
public Long getRank(String userId) {
return redisTemplate.opsForZSet()
.reverseRank(LEADERBOARD_KEY, userId);
}
// 获取用户分数
public Double getScore(String userId) {
return redisTemplate.opsForZSet()
.score(LEADERBOARD_KEY, userId);
}
}
1.4 性能优化¶
定时快照¶
问题: 实时排行榜压力大(如百万用户频繁查询)。
优化: 每分钟生成一次快照,用户查询快照版本。
# 定时任务(每分钟执行)
ZUNIONSTORE leaderboard:snapshot 1 leaderboard:2025
EXPIRE leaderboard:snapshot 120
二、消息队列(Message Queue)¶
2.1 方案对比¶
| 方案 | 数据结构 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|
| List + BLPOP | List | 实现简单 | 无 ACK 机制、无消费者组 | 简单任务队列 |
| Stream | Stream | 支持消费者组、ACK 确认 | Redis 5.0+ 才支持 | 推荐生产环境 |
| Pub/Sub | - | 多播、实时性高 | 不持久化、消费者离线消息丢失 | 实时通知、聊天室 |
2.2 方案 1:List + BLPOP(简单队列)¶
生产者¶
LPUSH queue:task "task1"
LPUSH queue:task "task2"
消费者(阻塞等待)¶
BRPOP queue:task 0 # 0 表示永久阻塞
Java 实现¶
// 生产者
public void sendTask(String task) {
redisTemplate.opsForList().leftPush("queue:task", task);
}
// 消费者
public String consumeTask() {
return redisTemplate.opsForList()
.rightPop("queue:task", 0, TimeUnit.SECONDS);
}
缺点:
- 无 ACK 机制:消费者崩溃,消息丢失。
- 无消费者组:多消费者会重复消费。
2.3 方案 2:Stream(推荐)¶
生产者¶
XADD stream:order * order_id 1001 user_id 2001 amount 99.99
创建消费者组¶
XGROUP CREATE stream:order group1 0
消费者读取消息¶
XREADGROUP GROUP group1 consumer1 COUNT 10 STREAMS stream:order >
ACK 确认¶
XACK stream:order group1 <message_id>
Java 实现(Spring Data Redis)¶
@Service
public class StreamMessageService {
@Autowired
private StringRedisTemplate redisTemplate;
// 生产者
public void sendOrder(Order order) {
Map<String, String> fields = Map.of(
"order_id", order.getId().toString(),
"user_id", order.getUserId().toString(),
"amount", order.getAmount().toString()
);
redisTemplate.opsForStream()
.add(StreamRecords.newRecord()
.ofMap(fields)
.withStreamKey("stream:order"));
}
// 消费者
@Scheduled(fixedDelay = 1000)
public void consumeOrder() {
List<MapRecord<String, Object, Object>> records =
redisTemplate.opsForStream()
.read(Consumer.from("group1", "consumer1"),
StreamReadOptions.empty().count(10),
StreamOffset.create("stream:order", ReadOffset.lastConsumed()));
records.forEach(record -> {
// 处理消息
System.out.println("Received: " + record.getValue());
// ACK 确认
redisTemplate.opsForStream()
.acknowledge("stream:order", "group1", record.getId());
});
}
}
三、限流(Rate Limiting)¶
3.1 方案对比¶
| 方案 | 实现 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|
| 固定窗口 | INCR + EXPIRE | 实现简单 | 窗口边界流量突刺 | 简单限流 |
| 滑动窗口 | ZSet(时间戳) | 精确限流 | 占用内存多 | 精确限流(如 API) |
| 令牌桶/漏桶 | Lua 脚本 + 时间戳 | 平滑限流 | 实现复杂 | 高并发场景 |
3.2 方案 1:固定窗口(INCR)¶
原理¶
每秒允许 100 次请求
┌─────────┬─────────┬─────────┐
│ 第 1 秒 │ 第 2 秒 │ 第 3 秒 │
│ 100 │ 0 │ 0 │
└─────────┴─────────┴─────────┘
实现¶
public boolean allowRequest(String userId) {
String key = "ratelimit:" + userId + ":" + System.currentTimeMillis() / 1000;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 1, TimeUnit.SECONDS);
}
return count <= 100; // 每秒最多 100 次
}
缺点: 窗口边界流量突刺(如第 1 秒末 50 次 + 第 2 秒初 50 次 = 100 次,但 1 秒内实际 100 次)。
3.3 方案 2:滑动窗口(ZSet)¶
原理¶
用时间戳作为 score,滑动窗口内的请求数不超过限制
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)
return 1
else
return 0
end
Java 实现¶
public boolean allowRequest(String userId) {
String key = "ratelimit:sliding:" + userId;
long now = System.currentTimeMillis();
int limit = 100; // 100 次
int window = 60; // 60 秒
String script = "..."; // Lua 脚本
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
String.valueOf(limit),
String.valueOf(window),
String.valueOf(now)
);
return result != null && result == 1;
}
3.4 方案 3:令牌桶(推荐)¶
原理¶
每秒生成 N 个令牌,请求时消耗令牌,桶满时丢弃令牌
Lua 脚本¶
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 生成速率(令牌/秒)
local requested = tonumber(ARGV[3]) -- 请求令牌数
local now = tonumber(ARGV[4])
local tokens_key = key .. ":tokens"
local timestamp_key = key .. ":timestamp"
local last_tokens = tonumber(redis.call('GET', tokens_key) or capacity)
local last_time = tonumber(redis.call('GET', timestamp_key) or now)
-- 计算新增令牌
local delta = math.max(0, now - last_time)
local new_tokens = math.min(capacity, last_tokens + delta * rate / 1000)
if new_tokens >= requested then
redis.call('SET', tokens_key, new_tokens - requested)
redis.call('SET', timestamp_key, now)
redis.call('EXPIRE', tokens_key, 60)
redis.call('EXPIRE', timestamp_key, 60)
return 1
else
return 0
end
四、分布式 ID 生成¶
4.1 方案 1:INCR(简单自增 ID)¶
public Long generateId() {
return redisTemplate.opsForValue().increment("global:id");
}
优点: 简单、高性能(单机 10 万 QPS)。
缺点:
- 单调递增,容易被爬虫(如订单 ID)。
- 单机瓶颈。
4.2 方案 2:雪花算法(Snowflake)¶
结构:
64 位 = 1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号
Java 实现(Hutool):
@Bean
public Snowflake snowflake() {
return new Snowflake(1, 1); // workerId=1, datacenterId=1
}
public long generateId() {
return snowflake.nextId();
}
优点:
- 趋势递增(按时间排序)。
- 高性能(单机百万 QPS)。
缺点:
- 依赖系统时钟(时钟回拨会重复)。
五、会话共享(Session)¶
5.1 场景描述¶
问题: 单体应用扩展为分布式集群后,Session 无法共享。
用户请求 → Nginx 负载均衡
├─ 服务器 A(Session A)
├─ 服务器 B(Session B)
└─ 服务器 C(Session C)
5.2 解决方案:Spring Session + Redis¶
Maven 依赖¶
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
配置¶
spring:
session:
store-type: redis
timeout: 30m
使用(无需修改代码)¶
@GetMapping("/login")
public String login(HttpSession session) {
session.setAttribute("userId", 1001);
return "success";
}
@GetMapping("/getUser")
public Object getUser(HttpSession session) {
return session.getAttribute("userId");
}
原理: Spring Session 拦截 HttpSession 操作,自动存储到 Redis。
六、地理位置(GEO)¶
6.1 场景描述¶
典型应用: 附近的人、附近的餐厅、打车距离计算。
6.2 核心命令¶
添加地理位置¶
GEOADD locations 116.397128 39.916527 "beijing"
GEOADD locations 121.473701 31.230416 "shanghai"
查找附近的位置¶
GEORADIUS locations 116.40 39.92 100 km WITHDIST WITHCOORD
计算两点距离¶
GEODIST locations "beijing" "shanghai" km
6.3 Java 实现¶
@Service
public class LocationService {
@Autowired
private StringRedisTemplate redisTemplate;
// 添加用户位置
public void addUserLocation(String userId, double longitude, double latitude) {
redisTemplate.opsForGeo()
.add("user:locations", new Point(longitude, latitude), userId);
}
// 查找附近的用户(5 公里内)
public List<String> getNearbyUsers(double longitude, double latitude) {
Circle circle = new Circle(new Point(longitude, latitude),
new Distance(5, Metrics.KILOMETERS));
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo()
.radius("user:locations", circle);
return results.getContent().stream()
.map(result -> result.getContent().getName())
.collect(Collectors.toList());
}
}
七、布隆过滤器(Bloom Filter)¶
7.1 场景描述¶
典型应用: 缓存穿透防护、垃圾邮件过滤、爬虫去重。
7.2 Redisson 实现¶
@Autowired
private RedissonClient redisson;
@PostConstruct
public void initBloomFilter() {
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("email:filter");
// 预期 100 万元素,误判率 0.01
bloomFilter.tryInit(1000000L, 0.01);
// 添加已注册邮箱
bloomFilter.add("alice@example.com");
bloomFilter.add("bob@example.com");
}
public boolean isEmailRegistered(String email) {
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("email:filter");
return bloomFilter.contains(email);
}
八、延时队列(Delayed Queue)¶
8.1 场景描述¶
典型应用: 订单超时取消、优惠券过期提醒、定时任务。
8.2 实现方案:ZSet(时间戳作为 score)¶
添加延时任务¶
public void addDelayedTask(String taskId, long delaySeconds) {
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
redisTemplate.opsForZSet().add("delayed:tasks", taskId, executeTime);
}
消费延时任务¶
@Scheduled(fixedDelay = 1000)
public void consumeDelayedTasks() {
long now = System.currentTimeMillis();
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore("delayed:tasks", 0, now);
tasks.forEach(taskId -> {
// 执行任务
System.out.println("Execute: " + taskId);
// 删除任务
redisTemplate.opsForZSet().remove("delayed:tasks", taskId);
});
}
九、总结与选型¶
| 场景 | Redis 数据类型 | 关键命令 | 典型应用 |
|---|---|---|---|
| 排行榜 | ZSet | ZADD、ZREVRANGE、ZRANK | 游戏积分榜、热度榜 |
| 消息队列 | Stream / List | XADD、XREADGROUP、BLPOP | 任务队列、订单处理 |
| 限流 | String / ZSet | INCR、Lua 脚本 | API 限流、防刷 |
| 分布式 ID | String | INCR、雪花算法 | 订单号、用户 ID |
| 会话共享 | Hash / String | Spring Session | 分布式 Web 应用 |
| 地理位置 | Geo | GEOADD、GEORADIUS、GEODIST | 附近的人、打车距离 |
| 布隆过滤器 | Bitmap | SETBIT、GETBIT(Redisson 封装) | 缓存穿透防护、去重 |
| 延时队列 | ZSet | ZADD、ZRANGEBYSCORE | 订单超时、定时任务 |
通过灵活运用 Redis 的数据类型与命令,可以高效解决各种业务场景的技术难题!🚀