如何用Redis实现首页热点数据预加载?

来源:PostgreSQL教程作者:巫师头衔:草根站长
导读:本期聚焦于巫师创作的《如何用Redis实现首页热点数据预加载?》,敬请观看详情。首页接口响应时间从几十毫秒突然涨到几百毫秒,往往不是SQL突然变慢,而是缓存没有命中。热点数据如果等用户第一次访问时才写入Redis,就会出现首批请求同时穿透到数据库的情况,流量高峰时尤其明显。Redis预加载的思路是把首页需要频繁读取的商品、榜单、配置等内容提前加载到缓存,并保持数据在有效期内持续刷新。这样既能降低数据库连接压力,也能让用户始终拿到低延迟响应。本文会梳理预加载的触发时机、预热数据的筛选方式、并发控制与更新策略,并给出一个基于Java和Spring的落地示例。读者可以根据自己的缓存结构替换具体实现,但预热和并发控制逻辑基本通用,也能为首页接口的稳定性改造提供直接参考。

首页数据通常呈现明显的热点特征,访问量集中在少量商品、榜单或推荐位上。这些数据如果每次都由用户请求触发缓存写入,缓存过期后的第一批请求会集中打到数据库。预加载的作用就是把这些高频读取的数据提前放进Redis,让缓存命中率在业务高峰前就处于较高水平。

如何用Redis实现首页热点数据预加载?

一、为什么首页热点数据不能等请求来了再加载

首页接口的访问模型和普通详情页不同。详情页的key天然按ID分散,某个key过期影响范围有限;首页聚合数据则经常是几十个固定key被高频读取,比如首页推荐位、热销榜、活动配置。一旦这些key同时失效或Redis重启,恢复期间会有大量请求直接落到数据库。即使数据库单条索引查询不慢,并发量上来后连接池也会迅速被占满。

缓存击穿和缓存雪崩在首页场景会被放大。单个热点key过期后,如果没有保护,多个线程可能同时执行回源查询。假设回源接口耗时50毫秒,在1000 QPS下,这50毫秒内可能积压几十个相同查询。更严重的是,如果多个首页key的TTL设置成相同值,它们会在同一时间点过期,瞬时压力更大。

下面是一段没有预加载的常见写法。首次访问时发现缓存不存在,就查数据库再写入Redis,但在并发场景下无法阻止重复回源。

public HomeData getHomeData(String biz) {
    String key = "home:data:" + biz;
    String cache = redis.opsForValue().get(key);
    if (cache != null) {
        return JSON.parseObject(cache, HomeData.class);
    }
    HomeData data = homeMapper.selectByBiz(biz);
    redis.opsForValue().set(key, JSON.toJSONString(data), Duration.ofMinutes(10));
    return data;
}

这段代码在缓存未命中时没有互斥,高并发下会有多个请求同时执行selectByBiz。虽然最终缓存会写入,但数据库已经承受了不必要的压力。

二、预加载的触发时机与热点数据筛选

预加载首先需要明确什么时候触发。最简单的做法是应用启动完成后执行一次预热,把配置好的热点key全部加载到Redis。这种方式的优点是实现直接,缺点是Redis重启或手动清缓存后,需要等到下次应用发布或重启才能恢复。因此通常会在启动预热之外再增加定时刷新,让缓存即使被清理也能在较短时间内重新建立。

触发时机可以从三个维度设计:启动时预热、定时周期刷新、数据变更时更新。启动预热适合首页固定配置类数据,比如导航、类目、运营位;定时刷新适合榜单和推荐这种会周期性变化的数据;数据变更更新更适合后台修改配置后主动同步,能减少缓存脏数据窗口。

热点数据筛选同样不能忽视。首页模块很多,如果把所有key都塞进Redis,内存占用会明显上升。可以根据最近7天的访问日志统计key的请求次数,也可以直接从推荐系统、榜单服务或运营后台导出当天需要保证低延迟的热点集合。筛选时建议给每个热点设置一个优先级,优先预热访问量最大的前50到200个key,而不是全量预热。

下面是一个基于Spring Boot的启动预热和定时刷新示例。这里使用@PostConstruct执行启动加载,@Scheduled周期刷新,实际项目中可以配合配置中心读取热点列表。

@Service
public class HomePageCacheWarmer {

    private final StringRedisTemplate redis;
    private final HomePageService homePageService;

    public HomePageCacheWarmer(StringRedisTemplate redis, HomePageService homePageService) {
        this.redis = redis;
        this.homePageService = homePageService;
    }

    @PostConstruct
    public void warmUpOnStart() {
        List<String> hotBizList = homePageService.loadHotBizList();
        for (String biz : hotBizList) {
            refreshOne(biz);
        }
    }

    @Scheduled(fixedDelay = 120000, initialDelay = 60000)
    public void warmUpBySchedule() {
        List<String> hotBizList = homePageService.loadHotBizList();
        for (String biz : hotBizList) {
            refreshOne(biz);
        }
    }

    private void refreshOne(String biz) {
        String key = "home:hot:" + biz;
        HomeData data = homePageService.loadFromDb(biz);
        redis.opsForValue().set(key, JSON.toJSONString(data), Duration.ofMinutes(30));
    }
}

这段代码的问题在于多实例部署时每个实例都会执行同样的预热任务。如果实例数量不多,重复查询还能接受;如果实例数量较多,就等同于一批定时任务同时回源,需要引入分布式锁来限制。

三、并发控制与缓存更新策略

多实例预热最常见的问题是重复回源。比如十个服务实例在同一时间启动,每个实例都在@PostConstruct里拉取热点数据,数据库会收到十次相同的批量查询。对于大查询来说这是不必要的放大。解决办法是使用Redis本身提供的SET NX EX命令做分布式锁,只有获取锁成功的实例才执行预热,其他实例等待一段时间后直接读取缓存。

缓存更新策略比预热本身更影响一致性。对于首页热点数据,通常采用旁路缓存模式:读请求先查缓存,未命中则回源并写缓存;写请求先更新数据库,再删除或覆盖缓存。预加载可以看作一种主动写缓存操作,它的难点在于如果数据库内容已经变化,而预加载还使用旧数据覆盖,就会人为延长脏数据时间。因此预加载前最好有版本号或更新时间校验。

另一种思路是设置逻辑过期时间。物理TTL到了之后Redis就删除key,逻辑过期则把过期时间写在value里,key仍然保留。读取时发现逻辑过期,先返回旧值,同时异步去回源重建。这样可以避免缓存过期瞬间的集中回源,也能让首页接口保持稳定。

获取分布式锁进行预热的代码示例如下。这里使用Redis的setIfAbsent实现锁,并设置过期时间防止实例宕机后锁永不释放。

public boolean tryWarmUp(String lockKey) {
    String lockValue = UUID.randomUUID().toString();
    Boolean locked = redis.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30));
    if (Boolean.TRUE.equals(locked)) {
        try {
            List<String> hotBizList = homePageService.loadHotBizList();
            for (String biz : hotBizList) {
                refreshOne(biz);
            }
        } finally {
            String current = redis.opsForValue().get(lockKey);
            if (lockValue.equals(current)) {
                redis.delete(lockKey);
            }
        }
        return true;
    }
    return false;
}

四、一个可落地的首页热点预加载方案

把前面的内容串起来,完整的预加载方案可以分为三层。第一层是热点清单维护,可以由离线任务统计访问日志生成,也可以由运营平台手工维护,最终落到配置中心或Redis集合中。第二层是预热执行器,负责在启动、定时和收到变更消息时执行预热,并用分布式锁控制并发。第三层是读取兜底,首页接口读取Redis时如果发现key缺失,仍然走回源逻辑,但需要加互斥锁,保证极端情况下不会击穿数据库。

读取链路可以使用逻辑过期加异步重建。下面给出一段读取热点数据的封装方法。它在缓存存在但逻辑过期时,先返回旧数据,同时尝试获取重建锁。如果获取成功,就通过线程池异步加载新数据并覆盖缓存。

public HomeData getHotData(String biz) {
    String key = "home:hot:" + biz;
    String json = redis.opsForValue().get(key);
    if (json == null) {
        return loadAndCache(biz, key);
    }
    HotCacheWrapper wrapper = JSON.parseObject(json, HotCacheWrapper.class);
    if (wrapper.getExpireAt() > System.currentTimeMillis()) {
        return wrapper.getData();
    }
    // 逻辑过期,先返回旧值,异步重建
    String lockKey = "lock:hot:" + biz;
    boolean locked = redis.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(20));
    if (locked) {
        executor.execute(() -> {
            try {
                loadAndCache(biz, key);
            } finally {
                redis.delete(lockKey);
            }
        });
    }
    return wrapper.getData();
}

private HomeData loadAndCache(String biz, String key) {
    HomeData data = homePageService.loadFromDb(biz);
    HotCacheWrapper wrapper = new HotCacheWrapper();
    wrapper.setData(data);
    wrapper.setExpireAt(System.currentTimeMillis() + Duration.ofMinutes(30).toMillis());
    redis.opsForValue().set(key, JSON.toJSONString(wrapper));
    return data;
}

这段代码中的HotCacheWrapper用expireAt字段记录逻辑过期时间,物理key可以设置得长一些或不设置过期时间。这样即使后台任务没有及时刷新,首页请求也不会因为缓存过期而直接回源。

最后还需要考虑Redis的内存容量和监控。首页热点预加载通常只占Redis总数据的一小部分,但要避免把不必要的大对象塞进去。可以设置maxmemory和合适的淘汰策略,例如allkeys-lru或volatile-lru。监控上重点关注缓存命中率、预热耗时、回源次数和Redis内存使用。尤其是预热耗时,如果某天预热从几百毫秒变成几秒,通常说明数据库查询变慢或热点数据量膨胀,需要及时调整查询SQL或热点集合大小。

Redis预加载热点数据缓存预热修改时间:2026-10-03 18:30:12

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1003/65216.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。