返利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开发者团队,转载请注明出处!

相关推荐
Light602 小时前
领码SPARK融合平台 · TS × Java 双向契约 —— 性能与治理篇|缓存分段与版本秩序
低代码·缓存·spark
SimonKing2 小时前
告别繁琐配置!Retrofit-Spring-Boot-Starter让HTTP调用更优雅
java·后端·程序员
召摇2 小时前
Spring Boot 内置工具类深度指南
java·spring boot
JJJJ_iii3 小时前
【左程云算法09】栈的入门题目-最小栈
java·开发语言·数据结构·算法·时间复杂度
zzywxc7873 小时前
AI工具全景洞察:从智能编码到模型训练的全链路剖析
人工智能·spring·ios·prompt·ai编程
所愿ღ3 小时前
JavaWeb-Session和ServletContext
java·笔记·servlet
过尽漉雪千山3 小时前
Flink1.17.0集群的搭建
java·大数据·linux·flink·centos
爱读源码的大都督3 小时前
为什么Spring 6中要把synchronized替换为ReentrantLock?
java·后端·架构
Java烘焙师4 小时前
架构师必备:缓存更新模式总结
mysql·缓存