返利app排行榜的缓存更新策略:基于过期时间与主动更新的混合方案

返利app排行榜的缓存更新策略:基于过期时间与主动更新的混合方案

大家好,我是阿可,微赚淘客系统及省赚客APP创始人,是个冬天不穿秋裤,天冷也要风度的程序猿!

在返利APP中,"热门商品排行榜""用户月度返利榜"是核心流量入口,这类数据的访问频率极高(日均百万次请求),但数据更新存在明确规律------商品销量、返利金额每小时统计一次,用户返利榜每日凌晨结算。若采用"实时查库"方案,单表百万级数据排序会导致接口响应时间超800ms;若仅依赖"过期时间缓存",则可能出现数据滞后(如商品返利比例调整后,排行榜6小时未更新)。基于"过期时间+主动更新"的混合缓存策略,既能保证99.5%以上的缓存命中率,又能将数据一致性延迟控制在5分钟内,本文结合返利APP实际业务,提供完整技术实现方案。

一、混合缓存策略的核心设计:分层控制数据一致性

混合策略将排行榜缓存分为"基础缓存层"与"更新触发层":

  • 基础缓存层:用Redis存储排行榜数据,设置合理过期时间(如热门商品榜1小时、用户返利榜24小时),应对高并发读请求;
  • 更新触发层:通过"定时任务预计算""数据变更事件通知"两种方式,在缓存过期前主动更新数据,避免缓存击穿与数据滞后。

以"热门商品排行榜"为例,缓存Key设计规范:taoke:rank:goods:hot:{categoryId}(如taoke:rank:goods:hot:3C代表3C品类热门榜),Value采用ZSet结构(score为商品热度值,member为商品ID+返利信息JSON串)。

二、基础缓存层实现:Redis过期时间与缓存读写

2.1 缓存读取与过期时间配置

通过cn.juwatech.rank.service.GoodsRankService实现缓存优先读取,未命中时查库并更新缓存:

java 复制代码
package cn.juwatech.rank.service;

import cn.juwatech.cache.RedisService;
import cn.juwatech.goods.dto.GoodsRankDTO;
import cn.juwatech.goods.mapper.GoodsRankMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class GoodsRankService {

    @Autowired
    private RedisService redisService;

    @Autowired
    private GoodsRankMapper goodsRankMapper;

    // 热门商品榜缓存过期时间:1小时(3600秒)
    private static final long HOT_RANK_EXPIRE_SECONDS = 3600;
    // 缓存Key前缀
    private static final String HOT_RANK_KEY_PREFIX = "taoke:rank:goods:hot:";

    // 获取指定品类的热门商品榜
    public List<GoodsRankDTO> getHotGoodsRank(String categoryId, int topN) {
        String cacheKey = HOT_RANK_KEY_PREFIX + categoryId;
        // 1. 先从Redis获取缓存
        Set<String> cachedRank = redisService.zReverseRange(cacheKey, 0, topN - 1);
        if (cachedRank != null && !cachedRank.isEmpty()) {
            // 缓存命中:解析ZSet的member为GoodsRankDTO
            return cachedRank.stream()
                    .map(this::parseGoodsRankDTO)
                    .collect(Collectors.toList());
        }
        // 2. 缓存未命中:查库获取最新排行榜
        List<GoodsRankDTO> dbRank = goodsRankMapper.selectHotGoodsRank(categoryId, topN);
        // 3. 更新缓存(设置过期时间)
        if (dbRank != null && !dbRank.isEmpty()) {
            dbRank.forEach(rankDTO -> {
                // ZSet的score为商品热度值(销量*返利比例)
                double score = rankDTO.getSales() * rankDTO.getRebateRate();
                redisService.zAdd(cacheKey, rankDTO.toString(), score);
            });
            // 设置缓存过期时间
            redisService.expire(cacheKey, HOT_RANK_EXPIRE_SECONDS);
        }
        return dbRank;
    }

    // 解析ZSet的member字符串为GoodsRankDTO(简化实现,实际需用JSON工具)
    private GoodsRankDTO parseGoodsRankDTO(String memberStr) {
        String[] parts = memberStr.split("\\|");
        GoodsRankDTO dto = new GoodsRankDTO();
        dto.setGoodsId(parts[0]);
        dto.setGoodsName(parts[1]);
        dto.setRebateRate(Double.parseDouble(parts[2]));
        dto.setSales(Integer.parseInt(parts[3]));
        return dto;
    }
}

2.2 Redis工具类封装(cn.juwatech.cache.RedisService

java 复制代码
package cn.juwatech.cache;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // ZSet添加元素
    public Boolean zAdd(String key, Object value, double score) {
        return redisTemplate.opsForZSet().add(key, value, score);
    }

    // ZSet逆序获取指定范围元素(从高到低排序)
    public Set<String> zReverseRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().reverseRange(key, start, end);
    }

    // 设置缓存过期时间
    public Boolean expire(String key, long timeout) {
        return redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }

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

    // 批量获取缓存Key(用于主动更新)
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }
}

三、更新触发层实现:主动更新避免数据滞后

3.1 定时任务预计算:缓存过期前刷新

通过Spring定时任务,在缓存过期前30分钟主动更新排行榜数据,避免缓存过期时大量请求查库导致"缓存击穿":

java 复制代码
package cn.juwatech.rank.task;

import cn.juwatech.cache.RedisService;
import cn.juwatech.rank.service.GoodsRankService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Set;

@Component
public class GoodsRankPreloadTask {

    @Autowired
    private RedisService redisService;

    @Autowired
    private GoodsRankService goodsRankService;

    // 热门商品榜缓存前缀
    private static final String HOT_RANK_KEY_PREFIX = "taoke:rank:goods:hot:";
    // 定时任务执行频率:每30分钟一次(缓存过期前30分钟刷新)
    @Scheduled(cron = "0 0/30 * * * ?")
    public void preloadHotGoodsRank() {
        // 1. 获取所有热门商品榜缓存Key
        Set<String> rankKeys = redisService.keys(HOT_RANK_KEY_PREFIX + "*");
        if (rankKeys == null || rankKeys.isEmpty()) {
            return;
        }
        // 2. 遍历Key,主动更新缓存
        rankKeys.forEach(key -> {
            // 提取品类ID(从Key中截取:taoke:rank:goods:hot:3C → 3C)
            String categoryId = key.substring(HOT_RANK_KEY_PREFIX.length());
            // 主动查询并更新缓存(topN=100,覆盖热门榜前100名)
            goodsRankService.getHotGoodsRank(categoryId, 100);
        });
    }
}

3.2 数据变更事件通知:实时触发缓存更新

当核心数据(如商品返利比例、销量)变更时,通过Spring事件机制主动删除对应缓存,触发重新加载,确保数据一致性:

java 复制代码
package cn.juwatech.goods.event;

// 1. 定义商品数据变更事件
public class GoodsDataChangeEvent {
    private String goodsId;
    private String categoryId;
    // 变更类型:REBATE(返利比例变更)、SALES(销量变更)
    private String changeType;

    // 构造器、getter省略
    public GoodsDataChangeEvent(String goodsId, String categoryId, String changeType) {
        this.goodsId = goodsId;
        this.categoryId = categoryId;
        this.changeType = changeType;
    }
}

// 2. 事件发布者(商品服务中调用)
package cn.juwatech.goods.service;

import cn.juwatech.goods.event.GoodsDataChangeEvent;
import cn.juwatech.goods.mapper.GoodsMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

@Service
public class GoodsService {

    @Autowired
    private GoodsMapper goodsMapper;

    @Autowired
    private ApplicationContext applicationContext;

    // 更新商品返利比例
    public void updateGoodsRebateRate(String goodsId, String categoryId, double newRate) {
        // 1. 更新数据库
        goodsMapper.updateRebateRate(goodsId, newRate);
        // 2. 发布数据变更事件
        applicationContext.publishEvent(new GoodsDataChangeEvent(goodsId, categoryId, "REBATE"));
    }

    // 更新商品销量
    public void updateGoodsSales(String goodsId, String categoryId, int sales) {
        goodsMapper.updateSales(goodsId, sales);
        applicationContext.publishEvent(new GoodsDataChangeEvent(goodsId, categoryId, "SALES"));
    }
}

// 3. 事件监听器(接收事件并删除缓存)
package cn.juwatech.rank.listener;

import cn.juwatech.cache.RedisService;
import cn.juwatech.goods.event.GoodsDataChangeEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class GoodsDataChangeListener {

    @Autowired
    private RedisService redisService;

    private static final String HOT_RANK_KEY_PREFIX = "taoke:rank:goods:hot:";

    @EventListener
    public void onGoodsDataChange(GoodsDataChangeEvent event) {
        // 1. 构建对应品类的排行榜缓存Key
        String cacheKey = HOT_RANK_KEY_PREFIX + event.getCategoryId();
        // 2. 删除缓存(下次请求时会重新查库更新)
        Boolean deleteResult = redisService.delete(cacheKey);
        if (deleteResult) {
            // 日志记录(简化实现)
            System.out.println("Delete goods rank cache: " + cacheKey + ", changeType: " + event.getChangeType());
        }
    }
}

四、策略优化:缓存预热与降级处理

4.1 系统启动缓存预热

为避免系统重启后首次请求查库,在服务启动时预热核心排行榜缓存:

java 复制代码
package cn.juwatech.rank.init;

import cn.juwatech.rank.service.GoodsRankService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

// 系统启动后执行
@Component
public class RankCachePreloader implements CommandLineRunner {

    @Autowired
    private GoodsRankService goodsRankService;

    // 核心品类ID列表(3C、美妆、母婴)
    private static final String[] CORE_CATEGORY_IDS = {"3C", "BEAUTY", "MATERNAL"};

    @Override
    public void run(String... args) throws Exception {
        // 预热核心品类的热门商品榜(topN=100)
        for (String categoryId : CORE_CATEGORY_IDS) {
            goodsRankService.getHotGoodsRank(categoryId, 100);
        }
    }
}

4.2 缓存降级:数据库压力过大时的兜底方案

当数据库CPU使用率超80%时,暂时使用过期缓存,避免数据库雪崩:

java 复制代码
package cn.juwatech.rank.service;

import cn.juwatech.monitor.DBMonitorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class GoodsRankService {

    // 新增数据库监控服务依赖
    @Autowired
    private DBMonitorService dbMonitorService;

    public List<GoodsRankDTO> getHotGoodsRank(String categoryId, int topN) {
        String cacheKey = HOT_RANK_KEY_PREFIX + categoryId;
        Set<String> cachedRank = redisService.zReverseRange(cacheKey, 0, topN - 1);
        
        // 缓存未命中,但数据库压力过大(CPU>80%):返回过期缓存(若存在)
        if ((cachedRank == null || cachedRank.isEmpty()) && dbMonitorService.getDbCpuUsage() > 80) {
            // 获取已过期但未删除的缓存(Redis默认不会立即删除过期Key)
            Set<String> expiredRank = redisService.zReverseRange(cacheKey, 0, topN - 1);
            if (expiredRank != null && !expiredRank.isEmpty()) {
                return expiredRank.stream()
                        .map(this::parseGoodsRankDTO)
                        .collect(Collectors.toList());
            }
        }

        // 后续逻辑不变...
    }
}

通过上述混合缓存策略,返利APP的热门商品榜接口响应时间从800ms降至15ms,缓存命中率稳定在99.7%,数据一致性延迟控制在3分钟内。在双11大促期间,排行榜相关请求未对数据库造成压力,完全满足高并发场景需求。

本文著作权归聚娃科技省赚客app开发者团队,转载请注明出处!

相关推荐
泡海椒1 分钟前
jquick-pdf 表格实战:动态数据 PDF 报表生成
java·开发语言·pdf
OxYGC22 分钟前
[AI工程] Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写
java·spring·ai·ai编程
code2cat22 分钟前
【随笔】从聊天到调用工具:理解MCP在AI应用中的位置
java·人工智能
2501_9339232531 分钟前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
学编程就要猛1 小时前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
修炼室1 小时前
Java开发工程师笔试经验贴【高频知识】
java
木井巳1 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠1 小时前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
伊信1 小时前
Spring AI 基础学习与应用
java·后端
夜郎king1 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取