Redis实战:缓存设计与高频场景全解析

前言

一次线上事故让我印象深刻:数据库CPU突然飙到100%,服务全面超时,排查后发现是缓存没有正确使用,大量请求直接打到了MySQL。

引入Redis并做好缓存设计之后,接口响应时间从800ms降到了20ms以内。

这篇文章分享Redis在真实项目中的实战用法。


一、Redis核心数据结构

复制代码
复制代码
String(字符串):
  最基础的类型,值可以是字符串、数字、二进制
  适用:缓存、计数器、分布式锁

Hash(哈希):
  类似Map<String, String>,存储对象字段
  适用:用户信息、商品详情等结构化对象

List(列表):
  双向链表,支持头尾插入/弹出
  适用:消息队列、最新动态列表

Set(集合):
  无序不重复集合,支持交并差集操作
  适用:共同好友、标签系统

Sorted Set(有序集合):
  每个元素附带score,按分值排序
  适用:排行榜、延迟队列

特殊结构:
  HyperLogLog  → UV统计(误差0.81%,内存极省)
  Bitmap       → 用户签到、在线状态
  Stream       → 持久化消息队列

二、Spring Boot集成Redis

2.1 基础配置

复制代码

yaml

复制代码
# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password: ${REDIS_PASSWORD}
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 20      # 最大连接数
        max-idle: 10        # 最大空闲连接
        min-idle: 5         # 最小空闲连接
        max-wait: 1000ms    # 获取连接最大等待时间
复制代码

java

复制代码
// RedisConfig.java
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // Key使用String序列化
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        // Value使用JSON序列化
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
            new Jackson2JsonRedisSerializer<>(Object.class);
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        return template;
    }
}

2.2 封装通用缓存工具类

复制代码

java

复制代码
@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 设置缓存(带过期时间)
    public void set(String key, Object value, long seconds) {
        redisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
    }

    // 获取缓存
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    // 删除缓存
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    // 原子自增(计数器)
    public Long increment(String key) {
        return redisTemplate.opsForValue().increment(key);
    }

    // 判断key是否存在
    public boolean exists(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    // 设置过期时间
    public void expire(String key, long seconds) {
        redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
    }
}

三、高频业务场景实战

3.1 缓存穿透、击穿、雪崩

复制代码
复制代码
缓存穿透:
  查询一个根本不存在的数据
  请求直接穿透缓存,全部打到DB
  解决:布隆过滤器 + 空值缓存

缓存击穿:
  热点key突然过期
  大量请求同时涌入DB
  解决:互斥锁 + 逻辑过期

缓存雪崩:
  大量key同时过期
  DB瞬间承受大量请求
  解决:过期时间加随机值 + 熔断降级
复制代码

java

复制代码
// 缓存击穿解决方案:分布式互斥锁
public UserDTO getUserById(Long userId) {
    String cacheKey = "user:" + userId;

    // 1. 先查缓存
    UserDTO user = (UserDTO) redisUtil.get(cacheKey);
    if (user != null) return user;

    // 2. 缓存未命中,加分布式锁
    String lockKey = "lock:user:" + userId;
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);

    if (Boolean.TRUE.equals(locked)) {
        try {
            // 3. 双重检查(防止并发情况下重复查DB)
            user = (UserDTO) redisUtil.get(cacheKey);
            if (user != null) return user;

            // 4. 查数据库
            user = userMapper.selectById(userId);

            // 5. 空值也缓存(防止缓存穿透)
            if (user == null) {
                redisUtil.set(cacheKey, "", 60);
                return null;
            }

            // 6. 写入缓存(过期时间加随机值,防雪崩)
            long ttl = 3600 + new Random().nextInt(600);
            redisUtil.set(cacheKey, user, ttl);

        } finally {
            // 7. 释放锁
            redisUtil.delete(lockKey);
        }
    } else {
        // 未拿到锁,短暂等待后重试
        Thread.sleep(50);
        return getUserById(userId);
    }

    return user;
}

3.2 排行榜(Sorted Set)

复制代码

java

复制代码
@Service
public class LeaderboardService {

    private static final String RANK_KEY = "game:score:rank";

    // 更新分数
    public void updateScore(String userId, double score) {
        redisTemplate.opsForZSet().add(RANK_KEY, userId, score);
    }

    // 增加分数
    public void addScore(String userId, double delta) {
        redisTemplate.opsForZSet().incrementScore(RANK_KEY, userId, delta);
    }

    // 获取Top N(从高到低)
    public List<RankItem> getTopN(int n) {
        Set<ZSetOperations.TypedTuple<Object>> result =
            redisTemplate.opsForZSet()
                .reverseRangeWithScores(RANK_KEY, 0, n - 1);

        List<RankItem> rankList = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<Object> item : result) {
            rankList.add(new RankItem(
                rank++,
                (String) item.getValue(),
                item.getScore()
            ));
        }
        return rankList;
    }

    // 获取用户排名
    public Long getUserRank(String userId) {
        Long rank = redisTemplate.opsForZSet()
            .reverseRank(RANK_KEY, userId);
        return rank != null ? rank + 1 : null;
    }
}

3.3 分布式限流(滑动窗口)

复制代码

java

复制代码
@Component
public class RateLimiter {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 滑动窗口限流
     * @param key      限流Key(如 "rate:userId:api")
     * @param limit    窗口内最大请求数
     * @param windowMs 窗口大小(毫秒)
     */
    public boolean isAllowed(String key, int limit, long windowMs) {
        long now = System.currentTimeMillis();
        long windowStart = now - windowMs;

        // Lua脚本保证原子性
        String script =
            "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])\n" +
            "local count = redis.call('ZCARD', KEYS[1])\n" +
            "if count < tonumber(ARGV[2]) then\n" +
            "  redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])\n" +
            "  redis.call('PEXPIRE', KEYS[1], ARGV[4])\n" +
            "  return 1\n" +
            "end\n" +
            "return 0";

        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(key),
            String.valueOf(windowStart),
            String.valueOf(limit),
            String.valueOf(now),
            String.valueOf(windowMs)
        );

        return Long.valueOf(1).equals(result);
    }
}

// 使用示例(Controller层)
@GetMapping("/api/data")
public Result getData(HttpServletRequest request) {
    String key = "rate:" + request.getRemoteAddr() + ":data";

    // 每分钟最多请求60次
    if (!rateLimiter.isAllowed(key, 60, 60000)) {
        return Result.fail("请求过于频繁,请稍后再试");
    }

    return Result.ok(dataService.getData());
}

3.4 用户签到(Bitmap)

复制代码

java

复制代码
@Service
public class SignInService {

    // 签到
    public void signIn(Long userId) {
        String key = "sign:" + userId + ":" + 
                     LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        int dayOfMonth = LocalDate.now().getDayOfMonth();

        // 偏移量从0开始,第1天对应offset=0
        redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
        // 设置过期时间(多保留一个月)
        redisTemplate.expire(key, 60, TimeUnit.DAYS);
    }

    // 查询本月签到天数
    public Long countSignIn(Long userId) {
        String key = "sign:" + userId + ":" +
                     LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        return redisTemplate.execute(
            (RedisCallback<Long>) conn ->
                conn.bitCount(key.getBytes())
        );
    }

    // 查询某天是否签到
    public Boolean checkSignIn(Long userId, int day) {
        String key = "sign:" + userId + ":" +
                     LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        return redisTemplate.opsForValue().getBit(key, day - 1);
    }
}

四、分布式锁标准实现

复制代码

java

复制代码
@Component
public class DistributedLock {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String LOCK_PREFIX = "dlock:";

    /**
     * 加锁
     * @param lockName   锁名称
     * @param requestId  唯一请求ID(用于安全解锁)
     * @param expireMs   超时时间(毫秒)
     */
    public boolean tryLock(String lockName, String requestId, long expireMs) {
        String key = LOCK_PREFIX + lockName;
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue()
                .setIfAbsent(key, requestId, expireMs, TimeUnit.MILLISECONDS)
        );
    }

    /**
     * 安全解锁(Lua脚本保证原子性)
     * 只有持有锁的请求才能解锁,防止误删他人的锁
     */
    public boolean unlock(String lockName, String requestId) {
        String key = LOCK_PREFIX + lockName;
        String script =
            "if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
            "  return redis.call('del', KEYS[1])\n" +
            "else\n" +
            "  return 0\n" +
            "end";

        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(key),
            requestId
        );

        return Long.valueOf(1).equals(result);
    }
}

// 使用示例(扣减库存)
public boolean deductStock(Long productId, int quantity) {
    String lockName = "stock:" + productId;
    String requestId = UUID.randomUUID().toString();

    if (!distributedLock.tryLock(lockName, requestId, 5000)) {
        throw new RuntimeException("系统繁忙,请重试");
    }

    try {
        // 查询库存
        int stock = productMapper.getStock(productId);
        if (stock < quantity) {
            return false;
        }

        // 扣减库存
        productMapper.deductStock(productId, quantity);
        return true;
    } finally {
        distributedLock.unlock(lockName, requestId);
    }
}

五、Redis集群方案对比

复制代码
复制代码
主从复制(Master-Slave):
  一主多从,读写分离
  适合:读多写少,数据量不大
  缺点:主节点故障需手动切换

哨兵模式(Sentinel):
  在主从基础上增加哨兵节点
  自动故障转移
  适合:高可用要求高,数据量适中

Cluster集群:
  数据分片,横向扩展
  16384个哈希槽分布到多个节点
  适合:数据量大,高并发写入
  缺点:多key操作受限
复制代码

bash

复制代码
# 查看集群节点状态
redis-cli -c -h 127.0.0.1 -p 7001 cluster nodes

# 集群信息
redis-cli -c cluster info

# 查看key落在哪个slot
redis-cli cluster keyslot "user:10001"

六、运维监控

复制代码

bash

复制代码
# 查看内存使用
redis-cli info memory | grep used_memory_human

# 查看慢查询日志
redis-cli slowlog get 10

# 查找大key(危险操作,生产慎用)
redis-cli --bigkeys

# 实时监控命令(生产慎用)
redis-cli monitor

# 查看连接数
redis-cli info clients | grep connected_clients

# 持久化状态
redis-cli info persistence

七、团队与工具

我们在做Redis集群选型方案时,与海外架构团队进行了多次视频评审会议,大量涉及技术细节的讨论对翻译准确性要求极高。全程使用**同言翻译(Transync AI)**的实时语音翻译功能,专业术语翻译准确,帮我们顺利完成了集群方案的最终确认。


八、最佳实践检查清单

复制代码
复制代码
□ Key命名规范:业务:模块:id(如 user:profile:10001)
□ 必须设置过期时间,避免内存无限增长
□ 避免存储超过10KB的大Value
□ 禁止在生产环境使用KEYS命令(改用SCAN)
□ 缓存与DB更新保持一致性策略(先删后写 / 延迟双删)
□ 热点数据做本地缓存二级缓存
□ 连接池参数根据并发量合理配置
□ 开启持久化(AOF + RDB双保险)
□ 集群环境至少3主3从
□ 定期检查慢查询日志

总结

Redis在项目中的核心价值:

  • 极致性能:读写10万+ QPS,延迟微秒级
  • 🔒 分布式协调:分布式锁、限流、session共享
  • 📊 丰富数据结构:每种结构对应一类业务场景
  • 🛡️ 保护数据库:缓存层抵挡大量无效请求

掌握Redis不只是会几个set/get命令,更重要的是理解每种场景下的设计思路,在缓存一致性、内存管理、高可用之间做出合理的权衡取舍。

相关推荐
1688red1 小时前
基于Canal实现MySQL到Elasticsearch的数据同步
数据库·mysql·elasticsearch
m0_750580301 小时前
用Python生成艺术:分形与算法绘图
jvm·数据库·python
稻草猫.1 小时前
MyBatis进阶:动态SQL与MyBatis Generator插件使用
java·数据库·后端·spring·mvc·mybatis
华农DrLai1 小时前
什么是Prompt模板?为什么标准化的格式能提高稳定性?
数据库·人工智能·gpt·nlp·prompt
2301_819414302 小时前
Python入门:从零到一的第一个程序
jvm·数据库·python
熬夜的咕噜猫2 小时前
Nginx 安全防护与 HTTPS 部署实战
网络·数据库
我真会写代码2 小时前
从底层到实战:MySQL核心原理拆解,解锁数据库高性能密码
数据库·mysql
LF3_2 小时前
监听数据库binlog日志变化,将变动实时发送到kafka
数据库·分布式·mysql·kafka·binlog·debezium
我真会写代码2 小时前
从入门到精通:Redis实战指南,解锁高性能缓存核心能力
数据库·redis·缓存