内存数据库 redis(Redis实战:缓存设计与高频场景全解析)

内存数据库 redis(Redis实战:缓存设计与高频场景全解析)
Redis实战:缓存设计与高频场景全解析

前言

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

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

内存数据库 redis(Redis实战:缓存设计与高频场景全解析)

这篇文章分享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 jsonSerializer =            new Jackson2JsonRedisSerializer<>(Object.class);        template.setValueSerializer(jsonSerializer);        template.setHashValueSerializer(jsonSerializer);        return template;    }}

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 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
@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命令,更重要的是理解每种场景下的设计思路,在缓存一致性、内存管理、高可用之间做出合理的权衡取舍。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有