第 197 章:Redis 应用场景与最佳实践
学习目标
- 掌握 Redis 在真实业务的应用
- 实现排行榜、计数器、去重
- 学会大 Key / 热 Key 处理
- 了解 Redis 监控与运维
一、Redis 典型应用场景
二、计数器
2.1 文章阅读量
java
public void incrementView(Long articleId) {
String key = "article:view:" + articleId;
redisTemplate.opsForValue().increment(key);
}
public Long getView(Long articleId) {
return (Long) redisTemplate.opsForValue().get("article:view:" + articleId);
}2.2 定时落库
java
@Scheduled(fixedRate = 60000) // 每分钟
public void persistToDB() {
Set<String> keys = redisTemplate.keys("article:view:*");
for (String key : keys) {
Long count = (Long) redisTemplate.opsForValue().getAndDelete(key);
Long articleId = Long.parseLong(key.split(":")[2]);
articleMapper.incrementView(articleId, count);
}
}2.3 视频点赞
java
public boolean like(Long videoId, Long userId) {
// 用户点赞记录(去重)
String userLikeKey = "like:user:" + userId;
Boolean first = redisTemplate.opsForSet().add(userLikeKey, videoId) != null;
if (Boolean.TRUE.equals(first)) {
// 总数 +1
redisTemplate.opsForValue().increment("like:count:" + videoId);
}
return Boolean.TRUE.equals(first);
}
public Long getLikeCount(Long videoId) {
return (Long) redisTemplate.opsForValue().get("like:count:" + videoId);
}三、排行榜(ZSet)
3.1 游戏积分榜
java
public void addScore(String player, double score) {
redisTemplate.opsForZSet().add("rank:game:1", player, score);
}
public List<RankDTO> topN(int n) {
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores("rank:game:1", 0, n - 1);
List<RankDTO> list = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<String> t : tuples) {
list.add(new RankDTO(rank++, t.getValue(), t.getScore()));
}
return list;
}
public Long getMyRank(String player) {
return redisTemplate.opsForZSet().reverseRank("rank:game:1", player);
}
public List<String> getPlayersAroundMe(String player, int range) {
Long rank = getMyRank(player);
long start = Math.max(0, rank - range);
long end = rank + range;
return new ArrayList<>(redisTemplate.opsForZSet()
.reverseRange("rank:game:1", start, end));
}3.2 销售榜 + 多维度
java
// 总销量
redisTemplate.opsForZSet().add("rank:sales:total", productId, totalAmount);
// 日销量(每天一个 key)
String dailyKey = "rank:sales:" + LocalDate.now();
redisTemplate.opsForZSet().add(dailyKey, productId, todayAmount);
redisTemplate.expire(dailyKey, 7, TimeUnit.DAYS);
// 月榜 / 周榜同理四、去重与唯一性
4.1 用户签到(位图)
java
public boolean sign(Long userId, String date) {
int dayOfYear = LocalDate.parse(date).getDayOfYear();
String key = "sign:" + userId + ":" + LocalDate.parse(date).getYear();
Boolean first = redisTemplate.opsForValue().setBit(key, dayOfYear, true);
if (Boolean.TRUE.equals(first)) {
// 连续签到天数
redisTemplate.opsForValue().increment("sign:count:" + userId);
}
return first;
}
public boolean isSigned(Long userId, String date) {
int dayOfYear = LocalDate.parse(date).getDayOfYear();
String key = "sign:" + userId + ":" + LocalDate.parse(date).getYear();
return Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, dayOfYear));
}
public long getMonthSignCount(Long userId, int year, int month) {
String key = "sign:" + userId + ":" + year;
int start = LocalDate.of(year, month, 1).getDayOfYear();
int end = LocalDate.of(year, month, 1).with(TemporalAdjusters.lastDayOfMonth()).getDayOfYear();
return (Long) redisTemplate.execute(
(RedisCallback<Long>) conn -> conn.bitCount(key.getBytes(), start, end)
);
}4.2 UV 统计(HyperLogLog)
java
public void addUV(String page, String userId) {
String key = "uv:" + page + ":" + LocalDate.now();
redisTemplate.opsForHyperLogLog().add(key, userId);
redisTemplate.expire(key, 7, TimeUnit.DAYS);
}
public Long countUV(String page) {
String key = "uv:" + page + ":" + LocalDate.now();
return redisTemplate.opsForHyperLogLog().size(key);
}4.3 布隆过滤器(防穿透)
bash
# Redis 自带 Bloom(RedisBloom 模块)
BF.ADD user:filter user1
BF.EXISTS user:filter user1 # 1
BF.EXISTS user:filter user999 # 0(可能误判)应用:缓存穿透防护
java
public User findById(Long id) {
String bloomKey = "bloom:users";
// 1. 布隆判断(可能误判)
if (!redisTemplate.opsForValue().getOperations()
.execute((RedisCallback<Boolean>) c -> c.bfExists(bloomKey, id.toString().getBytes()))) {
return null; // 一定不存在
}
// 2. 正常查缓存
User user = redisTemplate.opsForValue().get("user:" + id);
if (user != null) return user;
// 3. 查 DB
user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set("user:" + id, user, 30, TimeUnit.MINUTES);
}
return user;
}
// 添加用户时加入过滤器
public void create(User user) {
userMapper.insert(user);
redisTemplate.execute((RedisCallback<Boolean>) c ->
c.bfAdd("bloom:users".getBytes(), user.getId().toString().getBytes()));
}五、消息通知(Stream)
见第 192 章 Stream 实战。
六、延迟队列(ZSet)
java
// zset score 是执行时间戳
public void delayTask(String taskId, long executeAt, Runnable task) {
redisTemplate.opsForZSet().add("delay:tasks", taskId, executeAt);
redisTemplate.opsForValue().set("delay:task:" + taskId, task);
}
// 定时扫描
@Scheduled(fixedRate = 1000)
public void poll() {
long now = System.currentTimeMillis();
Set<String> tasks = redisTemplate.opsForZSet().rangeByScore("delay:tasks", 0, now);
for (String taskId : tasks) {
// 原子性 pop
Long removed = redisTemplate.opsForZSet().remove("delay:tasks", taskId);
if (removed != null && removed > 0) {
// 执行任务
executor.execute((Runnable) redisTemplate.opsForValue().get("delay:task:" + taskId));
redisTemplate.delete("delay:task:" + taskId);
}
}
}七、大 Key 治理
7.1 危害
- 阻塞主线程:DEL 一个 1MB key 可能阻塞 1ms+
- 网络阻塞:大 value 占用带宽
- 集群迁移困难:slot 迁移时卡住
7.2 检测
bash
redis-cli --bigkeys # 找大 key
redis-cli --memkeys # 按内存排序
DEBUG OBJECT mykey # 单 key 信息7.3 拆分
java
// ❌ 大 key:50w 用户在一个 set
redisTemplate.opsForSet().add("all_users", ...);
// ✅ 拆成多个小 key
for (int i = 0; i < 500; i++) {
redisTemplate.opsForSet().add("users:bucket:" + i, ...);
}7.4 异步删除
bash
UNLINK bigkey # Redis 4+ 异步
DEL bigkey # 同步阻塞八、热 Key 处理
8.1 检测
bash
redis-cli --hotkeys # 7.0+ 支持
MONITOR | sort | uniq -c | sort -rn | head 208.2 解决方案
java
// 1. 客户端本地缓存(防击穿)
LoadingCache<Long, User> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(id -> userService.findById(id));
// 2. 多级缓存
// L1: Caffeine(本地)
// L2: Redis(集群)
// L3: DB
// 3. Key 分散
String key = "user:" + (id % 10) + ":" + id;九、监控与告警
9.1 关键指标
bash
INFO stats # 总请求数、连接数
INFO memory # 内存使用
INFO clients # 客户端连接
INFO replication # 主从状态
INFO commandstats # 命令调用次数9.2 慢查询
conf
# 超过 10ms 记录
slowlog-log-slower-than 10000
slowlog-max-len 128bash
SLOWLOG GET 10
SLOWLOG RESET9.3 Prometheus 监控
yaml
# prometheus.yml
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']yaml
# redis-exporter
services:
redis-exporter:
image: oliver006/redis_exporter
ports:
- "9121:9121"
environment:
- REDIS_ADDR=redis://redis:63799.4 告警规则
yaml
- alert: RedisDown
expr: redis_up == 0
for: 1m
annotations:
summary: "Redis 不可用"
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
for: 5m
annotations:
summary: "Redis 内存使用率超 85%"
- alert: RedisConnectionsHigh
expr: redis_connected_clients > 5000
for: 5m十、Redis 7.0 新特性
| 特性 | 用途 |
|---|---|
| Redis Functions | 替代 Lua,支持函数库 |
| Multi-part AOF | AOF 文件分片 |
| Client-eviction | 自动淘汰空闲连接 |
| Sharded Pub/Sub | 集群 Pub/Sub |
| Listpack 编码 | ZSet/HSet 内存优化 |
十一、本章小结
| 场景 | 数据结构 |
|---|---|
| 阅读量 | String + INCR |
| 排行榜 | ZSet |
| 签到 | BitMap |
| UV | HyperLogLog |
| 去重 | Set / BitMap |
| 延迟队列 | ZSet + 轮询 |
动手练习
- 实现一个完整的积分排行榜
- 用 BitMap 实现月度签到
- 用 HyperLogLog 实现页面 UV
- 用 Redis Bloom 防缓存穿透
推荐阅读
下一章:第 198 章:Kafka 入门与架构 →