Skip to content
第 197 / 250 章中间件⏱ 12 分钟阅读

第 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 20

8.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 128
bash
SLOWLOG GET 10
SLOWLOG RESET

9.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:6379

9.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 AOFAOF 文件分片
Client-eviction自动淘汰空闲连接
Sharded Pub/Sub集群 Pub/Sub
Listpack 编码ZSet/HSet 内存优化

十一、本章小结

场景数据结构
阅读量String + INCR
排行榜ZSet
签到BitMap
UVHyperLogLog
去重Set / BitMap
延迟队列ZSet + 轮询

动手练习

  1. 实现一个完整的积分排行榜
  2. 用 BitMap 实现月度签到
  3. 用 HyperLogLog 实现页面 UV
  4. 用 Redis Bloom 防缓存穿透

推荐阅读


下一章:第 198 章:Kafka 入门与架构

本站基于 VitePress 构建 · 由 Codebook 团队维护