Redis实战:缓存设计与高频场景全解析
前言
一次线上事故让我印象深刻:数据库CPU突然飙到100%,服务全面超时,排查后发现是缓存没有正确使用,大量请求直接打到了MySQL。
引入Redis并做好缓存设计之后,接口响应时间从800ms降到了20ms以内。

这篇文章分享Redis在真实项目中的实战用法。
一、Redis核心数据结构
String(字符串): 最基础的类型,值可以是字符串、数字、二进制 适用:缓存、计数器、分布式锁Hash(哈希): 类似Map,存储对象字段 适用:用户信息、商品详情等结构化对象List(列表): 双向链表,支持头尾插入/弹出 适用:消息队列、最新动态列表Set(集合): 无序不重复集合,支持交并差集操作 适用:共同好友、标签系统Sorted Set(有序集合): 每个元素附带score,按分值排序 适用:排行榜、延迟队列特殊结构: HyperLogLog → UV统计(误差0.81%,内存极省) Bitmap → 用户签到、在线状态 Stream → 持久化消息队列 二、Spring Boot集成Redis
2.1 基础配置
yaml# application.ymlspring: 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@Configurationpublic class RedisConfig { @Bean public RedisTemplate redisTemplate( RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(factory); // Key使用String序列化 template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); // Value使用JSON序列化 Jackson2JsonRedisSerializer 2.2 封装通用缓存工具类
java@Componentpublic class RedisUtil { @Autowired private RedisTemplate 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@Servicepublic 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 getTopN(int n) { Set> result = redisTemplate.opsForZSet() .reverseRangeWithScores(RANK_KEY, 0, n - 1); List rankList = new ArrayList<>(); int rank = 1; for (ZSetOperations.TypedTuple 3.3 分布式限流(滑动窗口)
java@Componentpublic class RateLimiter { @Autowired private RedisTemplate 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@Servicepublic 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) 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@Componentpublic class DistributedLock { @Autowired private RedisTemplate 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落在哪个slotredis-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命令,更重要的是理解每种场景下的设计思路,在缓存一致性、内存管理、高可用之间做出合理的权衡取舍。
文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有