Redis 的高性能和丰富的数据类型使其在各种业务场景中大放异彩。本文汇总 Redis 在实际项目中的经典应用场景与最佳实践。
一、排行榜(Leaderboard) 1.1 场景描述 典型应用: 游戏积分榜、文章点赞榜、销售排行榜、直播热度榜。
核心需求:
实时更新排名。
高并发读写(如百万用户同时刷新排行榜)。
支持范围查询(如 Top 100)。
1.2 技术方案:ZSet(有序集合) 数据结构:
1 2 3 ZADD leaderboard:2025 100 "user:1001" ZADD leaderboard:2025 200 "user:1002" ZADD leaderboard:2025 150 "user:1003"
常用操作:
更新分数(增加积分) 1 ZINCRBY leaderboard:2025 10 "user:1001"
查询 Top N(降序) 1 2 ZREVRANGE leaderboard:2025 0 99 WITHSCORES
查询用户排名 1 2 ZREVRANK leaderboard:2025 "user:1001"
查询用户分数 1 ZSCORE leaderboard:2025 "user:1001"
查询分数区间的用户 1 ZRANGEBYSCORE leaderboard:2025 100 200 WITHSCORES
1.3 Java 实现示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @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); } 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 )) .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 性能优化 定时快照 问题: 实时排行榜压力大(如百万用户频繁查询)。
优化: 每分钟生成一次快照,用户查询快照版本。
1 2 3 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(简单队列) 生产者 1 2 LPUSH queue:task "task1" LPUSH queue:task "task2"
消费者(阻塞等待)
Java 实现 1 2 3 4 5 6 7 8 9 10 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(推荐) 生产者 1 XADD stream:order * order_id 1001 user_id 2001 amount 99.99
创建消费者组 1 XGROUP CREATE stream:order group1 0
消费者读取消息 1 XREADGROUP GROUP group1 consumer1 COUNT 10 STREAMS stream:order >
ACK 确认 1 XACK stream:order group1 <message_id>
Java 实现(Spring Data Redis) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @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()); redisTemplate.opsForStream() .acknowledge("stream:order" , "group1" , record.getId()); }); } }
三、限流(Rate Limiting) 3.1 方案对比
方案
实现
优点
缺点
推荐场景
固定窗口
INCR + EXPIRE
实现简单
窗口边界流量突刺
简单限流
滑动窗口
ZSet(时间戳)
精确限流
占用内存多
精确限流(如 API)
令牌桶/漏桶
Lua 脚本 + 时间戳
平滑限流
实现复杂
高并发场景
3.2 方案 1:固定窗口(INCR) 原理 1 2 3 4 5 每秒允许 100 次请求 ┌─────────┬─────────┬─────────┐ │ 第 1 秒 │ 第 2 秒 │ 第 3 秒 │ │ 100 │ 0 │ 0 │ └─────────┴─────────┴─────────┘
实现 1 2 3 4 5 6 7 8 9 10 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 ; }
缺点: 窗口边界流量突刺(如第 1 秒末 50 次 + 第 2 秒初 50 次 = 100 次,但 1 秒内实际 100 次)。
3.3 方案 2:滑动窗口(ZSet) 原理 1 用时间戳作为 score,滑动窗口内的请求数不超过限制
Lua 脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public boolean allowRequest (String userId) { String key = "ratelimit:sliding:" + userId; long now = System.currentTimeMillis(); int limit = 100 ; int window = 60 ; String script = "..." ; 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:令牌桶(推荐) 原理 1 每秒生成 N 个令牌,请求时消耗令牌,桶满时丢弃令牌
Lua 脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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) 1 2 3 public Long generateId () { return redisTemplate.opsForValue().increment("global:id" ); }
优点: 简单、高性能(单机 10 万 QPS)。
缺点:
单调递增,容易被爬虫(如订单 ID)。
单机瓶颈。
4.2 方案 2:雪花算法(Snowflake) 结构:
1 64 位 = 1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号
Java 实现(Hutool):
1 2 3 4 5 6 7 8 @Bean public Snowflake snowflake () { return new Snowflake (1 , 1 ); } public long generateId () { return snowflake.nextId(); }
优点:
趋势递增(按时间排序)。
高性能(单机百万 QPS)。
缺点:
五、会话共享(Session) 5.1 场景描述 问题: 单体应用扩展为分布式集群后,Session 无法共享。
1 2 3 4 用户请求 → Nginx 负载均衡 ├─ 服务器 A(Session A) ├─ 服务器 B(Session B) └─ 服务器 C(Session C)
5.2 解决方案:Spring Session + Redis Maven 依赖 1 2 3 4 <dependency > <groupId > org.springframework.session</groupId > <artifactId > spring-session-data-redis</artifactId > </dependency >
配置 1 2 3 4 spring: session: store-type: redis timeout: 30m
使用(无需修改代码) 1 2 3 4 5 6 7 8 9 10 @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 核心命令 添加地理位置 1 2 GEOADD locations 116.397128 39.916527 "beijing" GEOADD locations 121.473701 31.230416 "shanghai"
查找附近的位置 1 GEORADIUS locations 116.40 39.92 100 km WITHDIST WITHCOORD
计算两点距离 1 GEODIST locations "beijing" "shanghai" km
6.3 Java 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @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); } 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 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Autowired private RedissonClient redisson;@PostConstruct public void initBloomFilter () { RBloomFilter<String> bloomFilter = redisson.getBloomFilter("email:filter" ); 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) 添加延时任务 1 2 3 4 public void addDelayedTask (String taskId, long delaySeconds) { long executeTime = System.currentTimeMillis() + delaySeconds * 1000 ; redisTemplate.opsForZSet().add("delayed:tasks" , taskId, executeTime); }
消费延时任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @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 的数据类型与命令,可以高效解决各种业务场景的技术难题!🚀