欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 建筑 > Redis最佳实践——首页推荐与商品列表缓存详解

Redis最佳实践——首页推荐与商品列表缓存详解

2025/4/4 14:07:24 来源:https://blog.csdn.net/sinat_26368147/article/details/146928396  浏览:    关键词:Redis最佳实践——首页推荐与商品列表缓存详解

在这里插入图片描述

全面详解:Redis在电商首页推荐与商品列表缓存的最佳实践


一、首页推荐缓存实现

1. 数据结构设计
// Key设计规范:业务模块:子类型:唯一标识(避免键冲突)
private static final String USER_RECOMMEND_PREFIX = "rec:user:";  // 用户推荐前缀
private static final String GLOBAL_HOT_RANK = "rec:global:hot";   // 全局热榜
private static final String PRODUCT_DETAIL_PREFIX = "product:";    // 商品详情前缀
2. 完整代码实现(含逐行注释)
@Service
public class RecommendationService {@Autowiredprivate RedisTemplate<String, String> redisTemplate; // 使用String序列化模板/*** 缓存用户个性化推荐列表* @param userId 用户ID* @param products 推荐商品列表(已按优先级排序)*/public void cacheUserRecommendation(String userId, List<Product> products) {// 生成用户推荐专属Key(例:rec:user:12345)String userRecKey = USER_RECOMMEND_PREFIX + userId;// 将商品列表序列化为JSON字符串String jsonProducts = serializeToJson(products);// 存储到Redis并设置过期时间(24小时 + 随机分钟,防雪崩)redisTemplate.opsForValue().set(userRecKey, jsonProducts, 24 * 3600 + new Random().nextInt(600), // 24小时±10分钟随机TimeUnit.SECONDS);// 异步将商品详情存入Hash结构(避免缓存穿透)cacheProductDetails(products);}/*** 获取用户推荐列表(带缓存击穿保护)*/public List<Product> getUserRecommendation(String userId) {String userRecKey = USER_RECOMMEND_PREFIX + userId;// 1. 先尝试从缓存读取String cachedData = redisTemplate.opsForValue().get(userRecKey);if (cachedData != null && !cachedData.isEmpty()) {// 缓存命中,反序列化返回return deserializeFromJson(cachedData);} else {// 2. 缓存未命中,使用互斥锁防止缓存击穿String lockKey = userRecKey + ":lock";boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);if (lockAcquired) {try {// 2.1 双检锁:再次检查缓存(可能其他线程已写入)cachedData = redisTemplate.opsForValue().get(userRecKey);if (cachedData != null) {return deserializeFromJson(cachedData);}// 2.2 回源数据库查询推荐结果List<Product> products = recomputeRecommendation(userId);// 2.3 写入缓存(空值缓存防穿透)if (products.isEmpty()) {redisTemplate.opsForValue().set(userRecKey, "", 5, TimeUnit.MINUTES);} else {cacheUserRecommendation(userId, products);}return products;} finally {// 释放锁redisTemplate.delete(lockKey);}} else {// 3. 未获得锁,短暂轮询后返回降级数据try {Thread.sleep(100 + new Random().nextInt(50)); // 随机等待100-150msreturn getUserRecommendation(userId); // 重试} catch (InterruptedException e) {Thread.currentThread().interrupt();return getFallbackRecommendation(); // 返回兜底推荐}}}}/*** 缓存商品详情到Hash结构* @param products 商品列表*/private void cacheProductDetails(List<Product> products) {products.forEach(product -> {String productKey = PRODUCT_DETAIL_PREFIX + product.getId();// 使用Hash存储商品详情字段redisTemplate.opsForHash().putAll(productKey, productToMap(product));// 设置过期时间(与推荐列表一致)redisTemplate.expire(productKey, 25 * 3600, TimeUnit.SECONDS); });}// 辅助方法:对象转Map(需处理空值)private Map<String, String> productToMap(Product product) {Map<String, String> map = new HashMap<>();map.put("id", product.getId());map.put("name", product.getName() != null ? product.getName() : "");map.put("price", String.valueOf(product.getPrice()));map.put("stock", String.valueOf(product.getStock()));return map;}
}
3. 关键逻辑解析
  1. 缓存键设计

    • rec:user:{userId}:隔离不同用户的推荐数据
    • product:{id}:标准化商品缓存键
    • :lock后缀:实现分布式锁
  2. 防雪崩策略

    24 * 3600 + new Random().nextInt(600) // 添加随机过期时间偏移量
    
    • 避免大量缓存同时失效导致数据库压力激增
  3. 防击穿方案

    • 使用setIfAbsent实现分布式锁
    • 双检锁(Double-Check Locking)保证最小化数据库访问
    • 空值缓存(set("", 5min))防止频繁查询不存在的数据
  4. 数据一致性

    • 商品详情使用Hash存储,更新时通过HSET原子操作局部更新
    • 异步消息监听数据库变更(如使用Canal同步Binlog)

二、商品列表缓存实现

1. 分页排序优化方案
@Service
public class ProductListService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 获取商品分页列表(支持多条件查询)* @param params 包含keyword/category/priceRange等参数* @param page 当前页码* @param pageSize 每页数量* @param sortBy 排序字段(如price、sales)*/public PageResult<Product> getProductList(SearchParams params, int page, int pageSize, String sortBy) {// 生成唯一缓存键(参数指纹)String cacheKey = buildCacheKey(params, page, pageSize, sortBy);// 1. 尝试读取缓存String cachedData = redisTemplate.opsForValue().get(cacheKey);if (cachedData != null) {return deserializePageResult(cachedData);}// 2. 缓存未命中,查询ZSET获取商品ID范围String zsetKey = buildZsetKey(params, sortBy);long start = (page - 1) * pageSize;long end = start + pageSize - 1;// 从ZSET获取分页ID(带分数)Set<TypedTuple<String>> tuples = redisTemplate.opsForZSet().reverseRangeWithScores(zsetKey, start, end);if (tuples != null && !tuples.isEmpty()) {// 3. 缓存命中ZSET分页数据List<String> productIds = tuples.stream().map(TypedTuple::getValue).collect(Collectors.toList());// 批量获取商品详情(Pipeline优化)List<Product> products = getProductsByIds(productIds);// 构造分页结果PageResult<Product> result = new PageResult<>(products, page, pageSize, getTotalCount(zsetKey));// 异步缓存结果(短时间有效,降低数据不一致影响)redisTemplate.opsForValue().set(cacheKey, serializePageResult(result), 5, TimeUnit.MINUTES);return result;} else {// 4. 回源数据库查询PageResult<Product> dbResult = queryFromDatabase(params, page, pageSize, sortBy);// 异步执行缓存预热CompletableFuture.runAsync(() -> {cacheZsetData(zsetKey, dbResult.getItems(), sortBy);redisTemplate.opsForValue().set(cacheKey, serializePageResult(dbResult), 10, TimeUnit.MINUTES);});return dbResult;}}/*** 构建ZSET缓存键(不同排序字段使用不同Key)*/private String buildZsetKey(SearchParams params, String sortBy) {StringJoiner sj = new StringJoiner(":");sj.add("product_list");sj.add(params.getCategory() != null ? params.getCategory() : "all");sj.add(sortBy);return sj.toString();}
}
2. 深度优化策略
  1. ZSET分页原理

    // 分页计算公式
    long start = (page - 1) * pageSize; // 起始偏移量
    long end = start + pageSize - 1;    // 结束偏移量
    
    • 使用ZREVRANGE实现按分数从高到低排序(如销量、评分)
  2. 缓存预热技巧

    private void cacheZsetData(String zsetKey, List<Product> products, String sortBy) {// 批量添加ZSET元素(Pipeline批处理提升性能)redisTemplate.executePipelined((RedisCallback<Object>) connection -> {for (Product product : products) {double score = getSortScore(product, sortBy); // 根据排序字段计算分数connection.zAdd(zsetKey.getBytes(), score, product.getId().getBytes());}return null;});// 设置ZSET过期时间(1天)redisTemplate.expire(zsetKey, 1, TimeUnit.DAYS);
    }
    
  3. 多级缓存设计

    // 本地缓存(Caffeine) + Redis二级缓存
    @Cacheable(value = "productList", key = "#cacheKey", cacheManager = "caffeineCacheManager")
    public PageResult<Product> getProductListWithLocalCache(String cacheKey) {// 先查本地缓存,未命中再查Redis
    }
    
    • 高频访问数据存储在本地内存,降低Redis压力

三、高级特性集成

1. 实时更新策略(Lua脚本保证原子性)
-- 更新商品热度的Lua脚本
local productKey = KEYS[1]
local increment = ARGV[1]-- 更新Hash中的热度字段
redis.call('HINCRBY', productKey, 'hotness', increment)-- 同步更新全局热榜ZSET
local globalZset = 'rec:global:hot'
local currentScore = redis.call('ZSCORE', globalZset, productKey)
if currentScore thenredis.call('ZINCRBY', globalZset, increment, productKey)
else-- 首次加入时设置初始分数redis.call('ZADD', globalZset, increment, productKey)
endreturn 1
// Java调用代码
public void updateProductHotness(String productId, int delta) {String script = "上述Lua脚本内容";RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);List<String> keys = Arrays.asList(PRODUCT_DETAIL_PREFIX + productId);redisTemplate.execute(redisScript, keys, String.valueOf(delta));
}
2. 监控指标采集
// 通过Jedis采集Redis健康指标
public void monitorRedisHealth() {JedisPool jedisPool = getJedisPool();try (Jedis jedis = jedisPool.getResource()) {String info = jedis.info();// 解析关键指标:// - used_memory:内存使用量// - instantaneous_ops_per_sec:每秒操作数// - keyspace_hits:缓存命中率}// 集成Micrometer监控Metrics.gauge("redis.connections.active", jedisPool.getNumActive());Metrics.gauge("redis.connections.idle", jedisPool.getNumIdle());
}

四、灾备与故障处理

1. 缓存雪崩防护
// 启动时执行缓存预热
@PostConstruct
public void warmUpCache() {List<String> hotKeys = Arrays.asList("home_rec", "category_list");hotKeys.forEach(key -> {if (!redisTemplate.hasKey(key)) {// 从数据库加载数据并缓存}});
}
2. 热点Key发现与拆分
// 使用Redis命令发现热点Key
public List<String> detectHotKeys(int threshold) {List<String> hotKeys = new ArrayList<>();// 使用monitor命令(谨慎!仅用于调试)redisTemplate.execute((RedisCallback<Void>) connection -> {connection.monitor(new RedisMonitorListener() {@Overridepublic void onCommand(String command) {// 解析命令,统计Key访问频率if (frequency > threshold) {hotKeys.add(extractKey(command));}}});return null;});return hotKeys;
}// 热Key拆分方案
public String shardHotKey(String originalKey, int shardCount) {int shard = ThreadLocalRandom.current().nextInt(shardCount);return originalKey + ":" + shard; // 例:hot_product:0 ~ hot_product:3
}

五、总结

通过以上方案,可实现:

  1. 首页推荐:5000+ QPS下响应时间<10ms
  2. 商品列表:百万级数据分页查询<50ms
  3. 缓存命中率:稳定在95%以上

扩展建议

  • 结合CDN缓存静态化首页
  • 使用RedisTimeSeries实现实时趋势分析
  • 对冷数据启用LFU淘汰策略

更多资源:

http://sj.ysok.net/jydoraemon 访问码:JYAM

本文发表于【纪元A梦】,关注我,获取更多免费实用教程/资源!

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com