Redis:如何在项目里面使用缓存?

发布于:2025-07-18 ⋅ 阅读:(16) ⋅ 点赞:(0)

💬 1、简述

在现代 Web 应用中,数据库往往是系统性能的瓶颈之一。为了提高查询效率、减轻数据库压力,我们通常会引入 Redis 作为缓存层。本文将介绍在实际项目中如何合理使用 Redis 缓存,并给出实战样例。

在这里插入图片描述


🧪 2、为什么使用 Redis 作为缓存?

优势 说明
超快访问速度 Redis 是内存数据库,读写性能远高于磁盘数据库
支持丰富数据结构 字符串、哈希、列表、集合、有序集合
支持过期策略 自动清除过期数据,适合缓存场景
原子操作强 适合并发环境,支持事务和 Lua 脚本

🚀 项目中缓存使用场景 :

✅ 高性能缓存层
  • 数据库查询缓存:减轻数据库压力
  • 页面/片段缓存:加速Web响应
  • 热点数据缓存:应对突发流量
✅ 分布式系统支持
  • 会话存储:分布式Session管理
  • 全局配置:动态参数配置中心
  • 分布式锁:跨进程互斥控制
✅ 特殊数据结构应用
  • 排行榜:利用ZSET实现
  • 计数器:INCR/DECR原子操作
  • 实时统计:HyperLogLog基数估算

🔄 3、基础缓存模式实践

3.1 缓存查询结果(经典Cache-Aside)

// 商品服务查询示例
public Product getProduct(Long id) {
    String cacheKey = "product:" + id;
    
    // 1. 先查缓存
    Product product = redisTemplate.opsForValue().get(cacheKey);
    if (product != null) {
        return product;
    }
    
    // 2. 查数据库
    product = productDao.findById(id);
    if (product == null) {
        return null;
    }
    
    // 3. 写入缓存(设置过期时间)
    redisTemplate.opsForValue().set(
        cacheKey, 
        product, 
        30, 
        TimeUnit.MINUTES
    );
    return product;
}

优化点

  • 设置合理的TTL(如30分钟)
  • 使用双重检查锁避免缓存击穿
  • 考虑使用布隆过滤器预防缓存穿透

3.2 缓存更新策略

写穿透(Write-Through)
@Transactional
public void updateProduct(Product product) {
    // 1. 更新数据库
    productDao.update(product);
    
    // 2. 更新缓存
    String cacheKey = "product:" + product.getId();
    redisTemplate.opsForValue().set(
        cacheKey, 
        product,
        30,
        TimeUnit.MINUTES
    );
}
延迟删除(Write-Behind)
@Transactional
public void updateProduct(Product product) {
    // 1. 只更新数据库
    productDao.update(product);
    
    // 2. 异步更新缓存(通过消息队列)
    kafkaTemplate.send("cache-refresh", 
        new CacheRefreshEvent("product", product.getId()));
}

🛠️ 4、典型应用场景实现

4.1 分布式Session管理

Spring Session配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory(
            new RedisStandaloneConfiguration("redis-host", 6379));
    }
}
使用示例
// 设置Session属性
@RequestMapping("/login")
public String login(HttpSession session) {
    session.setAttribute("user", currentUser);
    return "dashboard";
}

4.2 分布式锁实现

RedLock方案
public boolean tryLock(String lockKey, long expireSeconds) {
    String lockId = UUID.randomUUID().toString();
    
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, lockId, expireSeconds, TimeUnit.SECONDS);
    
    if (Boolean.TRUE.equals(success)) {
        // 获取锁成功
        return true;
    }
    return false;
}

public void unlock(String lockKey, String lockId) {
    // 使用Lua脚本保证原子性
    String script = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";
    
    redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        Collections.singletonList(lockKey),
        lockId
    );
}

4.3 热点商品秒杀

public boolean seckill(Long productId, Long userId) {
    String stockKey = "seckill:stock:" + productId;
    String boughtKey = "seckill:users:" + productId;
    
    // 1. 检查是否已购买
    if (redisTemplate.opsForSet().isMember(boughtKey, userId)) {
        return false;
    }
    
    // 2. 扣减库存(Lua脚本保证原子性)
    String script =
        "local stock = tonumber(redis.call('get', KEYS[1])) " +
        "if stock > 0 then " +
        "   redis.call('decr', KEYS[1]) " +
        "   redis.call('sadd', KEYS[2], ARGV[1]) " +
        "   return 1 " +
        "end " +
        "return 0";
    
    Long result = redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        Arrays.asList(stockKey, boughtKey),
        userId.toString()
    );
    
    return result == 1;
}

🌐 5、高级缓存模式

5.1 多级缓存架构

客户端
CDN
Nginx缓存
应用进程缓存
Redis集群
数据库
// 多级缓存查询
public Product getProductWithMultiCache(Long id) {
    // 1. 检查本地缓存
    Product product = localCache.get(id);
    if (product != null) {
        return product;
    }
    
    // 2. 检查Redis缓存
    product = redisTemplate.opsForValue().get("product:" + id);
    if (product != null) {
        localCache.put(id, product); // 回填本地缓存
        return product;
    }
    
    // 3. 查询数据库
    product = productDao.findById(id);
    if (product != null) {
        redisTemplate.opsForValue().set(
            "product:" + id, 
            product,
            10, 
            TimeUnit.MINUTES
        );
        localCache.put(id, product);
    }
    
    return product;
}

5.2 缓存预热策略

@Component
public class CacheWarmUp implements CommandLineRunner {

    @Autowired
    private ProductDao productDao;
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    @Override
    public void run(String... args) {
        // 加载热销商品前100名
        List<Product> hotProducts = productDao.findHotProducts(100);
        hotProducts.forEach(p -> {
            redisTemplate.opsForValue().set(
                "product:" + p.getId(),
                p,
                1, 
                TimeUnit.HOURS
            );
        });
    }
}

✍️ 6、性能优化实践

6.1 管道化(Pipeline)操作

public Map<Long, Product> batchGetProducts(List<Long> ids) {
    List<Object> results = redisTemplate.executePipelined(
        (RedisCallback<Object>) connection -> {
            ids.forEach(id -> {
                connection.stringCommands().get(("product:" + id).getBytes());
            });
            return null;
        }
    );
    
    Map<Long, Product> productMap = new HashMap<>();
    for (int i = 0; i < results.size(); i++) {
        Product p = (Product) results.get(i);
        if (p != null) {
            productMap.put(ids.get(i), p);
        }
    }
    return productMap;
}

6.2 大Key拆分方案

// 将大Hash拆分为多个小Hash
public void setLargeObject(String key, Map<String, String> bigData) {
    int segment = 0;
    Map<String, String> segmentMap = new HashMap<>();
    
    for (Map.Entry<String, String> entry : bigData.entrySet()) {
        segmentMap.put(entry.getKey(), entry.getValue());
        
        if (segmentMap.size() >= 1000) {
            redisTemplate.opsForHash().putAll(
                key + ":segment:" + segment,
                segmentMap
            );
            segment++;
            segmentMap.clear();
        }
    }
    
    if (!segmentMap.isEmpty()) {
        redisTemplate.opsForHash().putAll(
            key + ":segment:" + segment,
            segmentMap
        );
    }
}

📊 7、监控与问题排查

7.1 关键监控指标

# 内存使用
redis-cli info memory | grep used_memory_human

# 命中率
redis-cli info stats | grep keyspace_hits
redis-cli info stats | grep keyspace_misses

# 慢查询
redis-cli slowlog get 5

7.2 缓存雪崩预防

// 差异化过期时间
private int getRandomTtl() {
    return 1800 + new Random().nextInt(300); // 1800-2100秒
}

public void setWithRandomTtl(String key, Object value) {
    redisTemplate.opsForValue().set(
        key,
        value,
        getRandomTtl(),
        TimeUnit.SECONDS
    );
}

🧠 8、最佳实践

✅ 键命名规范:

  • 使用冒号分隔层级(如 service:entity:id
  • 避免特殊字符
  • 控制键长度(不超过100字节)

✅ 过期策略:

  • 所有缓存必须设置TTL
  • 热点数据永不过期需有更新机制
  • 大对象设置较短TTL

✅ 序列化选择:

// 推荐配置
@Bean
public RedisTemplate<String, Object> redisTemplate() {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
    return template;
}

✅ 集群规范:

  • 单个Value不超过10KB
  • 单个实例键数量不超过1千万
  • 避免使用KEYS命令

通过合理应用Redis缓存,可以显著提升系统性能。建议根据业务特点选择合适的缓存模式,并建立完善的监控体系。记住:缓存不是万能的,要始终保证数据最终一致性。


🔚 9、总结

在项目中合理使用 Redis 缓存,不仅能显著提升系统性能,还能提升用户体验。本文介绍了最常用的“旁路缓存”策略、代码实战以及常见问题的应对方式,适用于大多数 Web 后端项目。

🔐 使用 Redis 缓存的核心建议:

  • 把频繁访问、不常变的数据缓存起来

  • 设置合理的过期时间,避免占用内存

  • 缓存更新需和数据库同步或清理旧缓存

  • 注意并发场景下的缓存击穿/雪崩


网站公告

今日签到

点亮在社区的每一天
去签到