缓存预热怎么选?九大场景对号入座!

454 阅读38分钟

以下是上述几种缓存预热方案的具体场景案例,帮助您更好地理解它们的适用场景和实际应用。


1. 启动过程中预热(Spring 监听器实现)

具体场景:电商网站首页模块

  • 背景: 电商网站的首页展示大量商品信息(例如热门商品、分类列表、推荐内容等),这些数据是热点数据,且在系统启动后需要快速展示。
  • 应用: 系统启动时,通过监听器从数据库加载这些热点数据并存入缓存。这样,当用户访问首页时,可以直接从缓存中获取数据,提高访问速度。
  • 案例:
    • 京东、淘宝等电商平台在系统重启后预加载首页商品分类、推荐商品、品牌信息等。

以下是实现电商网站首页模块缓存预热的一个代码案例。我们以Spring Boot + Redis为基础,通过启动过程中预热的方式,在系统启动时将首页热点数据加载到缓存中。


实现代码

1. 数据准备

假设首页需要展示的热点数据包括:

  • 热门商品列表
  • 推荐内容
  • 广告位信息

这些数据通常存储在数据库中,接下来模拟数据库层的查询。


2. 实现步骤

(1) 数据库层模拟

创建一个模拟数据的服务类,用于从数据库中获取热点数据:

import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
public class DatabaseService {

    // 模拟从数据库获取热门商品列表
    public List<String> getHotProducts() {
        return Arrays.asList("商品A", "商品B", "商品C");
    }

    // 模拟从数据库获取推荐内容
    public List<String> getRecommendations() {
        return Arrays.asList("推荐内容1", "推荐内容2", "推荐内容3");
    }

    // 模拟从数据库获取广告位信息
    public List<String> getAds() {
        return Arrays.asList("广告1", "广告2", "广告3");
    }
}

(2) 缓存服务实现

创建一个服务类,用于将数据写入 Redis 缓存:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class CacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 将数据写入 Redis 缓存
    public void cacheData(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    // 从缓存中读取数据(可选,用于测试)
    public Object getCacheData(String key) {
        return redisTemplate.opsForValue().get(key);
    }
}

(3) 缓存预热逻辑实现

在系统启动时,通过 Spring 的生命周期监听器加载热点数据到缓存。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class CachePreheatListener {

    @Autowired
    private DatabaseService databaseService;

    @Autowired
    private CacheService cacheService;

    @EventListener(ApplicationReadyEvent.class) // 在系统启动完成后触发
    public void preheatCache() {
        System.out.println("开始预热缓存...");

        // 预热热门商品
        cacheService.cacheData("home:hot_products", databaseService.getHotProducts(), 1, TimeUnit.HOURS);

        // 预热推荐内容
        cacheService.cacheData("home:recommendations", databaseService.getRecommendations(), 1, TimeUnit.HOURS);

        // 预热广告位信息
        cacheService.cacheData("home:ads", databaseService.getAds(), 1, TimeUnit.HOURS);

        System.out.println("缓存预热完成!");
    }
}

(4) 配置 Redis

application.yml 文件中添加 Redis 的基本配置:

spring:
  redis:
    host: localhost        # Redis 服务地址
    port: 6379             # Redis 服务端口
    lettuce:
      pool:
        max-active: 8      # 连接池最大连接数
        max-idle: 8        # 连接池最大空闲连接数
        min-idle: 0        # 连接池最小空闲连接数

(5) 测试首页接口

编写一个简单的控制器接口,用于展示缓存中的首页数据:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class HomeController {

    @Autowired
    private CacheService cacheService;

    @GetMapping("/home")
    public Map<String, Object> getHomeData() {
        Map<String, Object> homeData = new HashMap<>();
        homeData.put("hot_products", cacheService.getCacheData("home:hot_products"));
        homeData.put("recommendations", cacheService.getCacheData("home:recommendations"));
        homeData.put("ads", cacheService.getCacheData("home:ads"));
        return homeData;
    }
}

3. 启动和验证

(1) 启动项目

启动 Spring Boot 项目后,可以看到控制台日志中打印了“开始预热缓存…”和“缓存预热完成!”的消息,表示缓存预热逻辑执行成功。

(2) 测试接口

通过浏览器或 Postman 访问接口 /home,可以看到返回的首页数据来自 Redis 缓存:

示例返回结果:

{
    "hot_products": ["商品A", "商品B", "商品C"],
    "recommendations": ["推荐内容1", "推荐内容2", "推荐内容3"],
    "ads": ["广告1", "广告2", "广告3"]
}

(3) 验证 Redis 中的数据

使用 Redis 客户端(如 redis-cli)验证是否正确写入 Redis:

执行以下命令:

redis-cli
> keys *
1) "home:hot_products"
2) "home:recommendations"
3) "home:ads"

> get "home:hot_products"
# 返回的 JSON 数据即缓存的数据

4. 代码说明

  1. 监听器触发预热: 使用 Spring 的 @EventListener 注解,监听应用启动完成事件,确保预热逻辑在系统完全启动后执行。
  2. Redis 缓存: 使用 RedisTemplate 实现缓存操作,设置了 1 小时的缓存过期时间(可以根据具体业务修改)。
  3. 模块化设计: 将数据库层、缓存层和控制器分离,方便扩展和维护。

5. 适用性和扩展

  • 适用性: 该实现适用于需要在系统启动时加载固定热点数据的场景,例如电商首页、门户网站首页。
  • 扩展:
    • 可以引入动态配置(如配置文件定义哪些数据需要预热)。
    • 数据量较大时,可以分批异步加载热点数据。
    • 使用更高级的缓存框架(如 Caffeine)结合 Redis 提升性能。

2. 定时任务预热

具体场景:新闻门户网站的每日热点

  • 背景: 新闻门户网站需要每天凌晨预加载当天的热点新闻,以便早晨用户访问时快速响应。
  • 应用: 通过定时任务,每天凌晨 3 点从数据库中获取当天的热点新闻列表,并将这些新闻内容写入缓存。
  • 案例:
    • 新浪新闻、今日头条等平台,通过定时预热热点新闻,提升早高峰访问体验。

以下是一个基于 Java 和 Redis 的简单示例,展示如何为新闻门户网站的每日热点内容进行缓存预热。这种实现方案主要结合定时任务,将每日热点新闻提前加载到缓存中,确保用户访问时可以快速获取数据。


示例说明:

  1. 场景:新闻门户网站每天早晨生成热点新闻,提前将这些热点新闻数据加载到 Redis 缓存中。
  2. 技术栈
    • Spring Boot
    • Redis (作为缓存)
    • 定时任务 (@Scheduled)

实现代码

1. 数据模型

创建一个简单的 News 数据模型,表示新闻数据。

public class News {
    private Long id; // 新闻ID
    private String title; // 新闻标题
    private String content; // 新闻内容

    // 构造方法、Getters 和 Setters
    public News(Long id, String title, String content) {
        this.id = id;
        this.title = title;
        this.content = content;
    }

    // toString() 方法
    @Override
    public String toString() {
        return "News{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", content='" + content + '\'' +
                '}';
    }

    // Getters 和 Setters 省略
}

2. 模拟新闻服务

创建一个 NewsService 类,用于获取热点新闻数据(模拟从数据库获取热点新闻)。

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class NewsService {

    // 模拟从数据库中查询热点新闻
    public List<News> fetchHotNews() {
        List<News> hotNews = new ArrayList<>();
        hotNews.add(new News(1L, "热点新闻1", "这是热点新闻1的内容"));
        hotNews.add(new News(2L, "热点新闻2", "这是热点新闻2的内容"));
        hotNews.add(new News(3L, "热点新闻3", "这是热点新闻3的内容"));
        return hotNews;
    }
}

3. 缓存预热任务

创建一个定时任务类,定期将热点新闻加载到 Redis 缓存中。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class CachePreheatTask {

    private static final String HOT_NEWS_CACHE_KEY = "HOT_NEWS";

    @Autowired
    private NewsService newsService;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 定时任务:每天凌晨 2 点执行
    @Scheduled(cron = "0 0 2 * * ?")
    public void preheatHotNewsCache() {
        // 1. 从数据库中获取热点新闻
        List<News> hotNews = newsService.fetchHotNews();

        // 2. 将热点新闻存入 Redis 缓存
        redisTemplate.opsForValue().set(HOT_NEWS_CACHE_KEY, hotNews);

        System.out.println("热点新闻已成功预热到缓存:" + hotNews);
    }
}

4. 配置 Redis 和定时任务

application.properties 中添加 Redis 和定时任务的配置。

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

# 开启定时任务
spring.task.scheduling.enabled=true

5. 启动类配置

确保 Spring Boot 项目启用了定时任务功能。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 开启定时任务功能
public class CachePreheatApplication {
    public static void main(String[] args) {
        SpringApplication.run(CachePreheatApplication.class, args);
    }
}

6. 测试热点缓存

创建一个控制器,用于验证热点新闻是否已经缓存成功。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class NewsController {

    private static final String HOT_NEWS_CACHE_KEY = "HOT_NEWS";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/hot-news")
    public List<News> getHotNews() {
        // 从 Redis 缓存中读取热点新闻
        return (List<News>) redisTemplate.opsForValue().get(HOT_NEWS_CACHE_KEY);
    }
}

运行流程

  1. 每天凌晨 2 点,CachePreheatTask 定时任务会从 NewsService 中获取热点新闻,并将数据存入 Redis 缓存。
  2. 用户访问 /hot-news 接口时,直接从 Redis 中读取热点新闻数据,提升访问速度。

注意事项

  1. Redis 缓存过期时间:可以根据业务需要为缓存设置过期时间,例如热点新闻数据每天更新,可以设置 24 小时过期。
    redisTemplate.opsForValue().set(HOT_NEWS_CACHE_KEY, hotNews, 24, TimeUnit.HOURS);
    
  2. 热点新闻的来源:实际生产中,新闻数据通常来自数据库或第三方 API,可以根据实际需求调整 fetchHotNews() 的实现。
  3. 异常处理:需要处理 Redis 连接异常或数据加载失败的问题,确保任务的健壮性。

3. 用时加载(惰性加载)

具体场景:冷门商品详情页

  • 背景: 电商平台中,某些商品访问量较少(冷门商品),不适合提前加载到缓存中,但在用户访问时需要快速响应。
  • 应用: 用户请求冷门商品详情时,系统首先检查缓存是否存在数据。如果不存在,则从数据库加载商品详情,同时将数据写入缓存,供其他用户访问。
  • 案例:
    • 冷门商品、非主流视频、长尾关键词搜索结果等。

在电子商务平台上,冷门商品的访问量较少,但仍然可能被用户访问。为了提高这些冷门商品详情页的加载速度,可以通过缓存预热机制提前将这些商品的数据加载到缓存中。这样即使某个冷门商品被访问,也可以直接从缓存中读取数据,而无需频繁访问数据库,从而提高系统性能。


实现思路

  1. 定义冷门商品:统计一段时间内访问量较低的商品 ID。
  2. 冷门数据处理:将这些冷门商品的详情数据预先加载到缓存中。
  3. 缓存策略:将冷门商品放入缓存时设置一个合理的过期时间,以避免缓存占用过多资源。
  4. 技术栈
    • Spring Boot:开发框架。
    • Redis:缓存存储。
    • 定时任务:定期预热缓存。

代码实现

1. 冷门商品数据模型

创建 Product 类来表示商品详情。

public class Product {
    private Long id; // 商品ID
    private String name; // 商品名称
    private String description; // 商品描述
    private Double price; // 商品价格

    // 构造方法、Getters 和 Setters
    public Product(Long id, String name, String description, Double price) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                ", price=" + price +
                '}';
    }
}

2. 模拟数据库服务

创建 ProductService 类,用于模拟获取商品详情和冷门商品列表。

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ProductService {

    // 模拟从数据库中查询商品详情
    public Product getProductById(Long productId) {
        return new Product(productId, "商品" + productId, "这是商品" + productId + "的描述", 100.0 + productId);
    }

    // 模拟从数据库中查询冷门商品ID列表
    public List<Long> getLowDemandProductIds() {
        // 假设这些商品是冷门商品
        List<Long> lowDemandProductIds = new ArrayList<>();
        lowDemandProductIds.add(101L);
        lowDemandProductIds.add(102L);
        lowDemandProductIds.add(103L);
        return lowDemandProductIds;
    }
}

3. 缓存预热任务

创建 CachePreheatTask 类,定期将冷门商品详情加载到 Redis 缓存中。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class CachePreheatTask {

    private static final String PRODUCT_CACHE_KEY_PREFIX = "PRODUCT_";

    @Autowired
    private ProductService productService;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 定时任务:每天凌晨 3 点执行
    @Scheduled(cron = "0 0 3 * * ?")
    public void preheatLowDemandProductCache() {
        // 1. 获取冷门商品ID列表
        List<Long> lowDemandProductIds = productService.getLowDemandProductIds();

        // 2. 遍历商品ID,获取对应的商品详情并存入 Redis
        for (Long productId : lowDemandProductIds) {
            Product product = productService.getProductById(productId);
            String cacheKey = PRODUCT_CACHE_KEY_PREFIX + productId;
            redisTemplate.opsForValue().set(cacheKey, product);
            System.out.println("已预热冷门商品到缓存:" + product);
        }
    }
}

4. Redis 配置和定时任务启用

application.properties 中配置 Redis 和定时任务。

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

# 开启定时任务
spring.task.scheduling.enabled=true

5. 获取商品详情接口

创建 ProductController,提供接口供用户获取商品详情。如果缓存中有数据,则直接返回缓存数据;否则从数据库获取。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    private static final String PRODUCT_CACHE_KEY_PREFIX = "PRODUCT_";

    @Autowired
    private ProductService productService;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/product")
    public Product getProduct(@RequestParam Long productId) {
        String cacheKey = PRODUCT_CACHE_KEY_PREFIX + productId;

        // 1. 从缓存中获取商品详情
        Product cachedProduct = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (cachedProduct != null) {
            System.out.println("从缓存中获取商品:" + cachedProduct);
            return cachedProduct;
        }

        // 2. 如果缓存中没有,则从数据库中查询商品详情
        Product productFromDb = productService.getProductById(productId);

        // 3. 将查询到的数据写入缓存
        redisTemplate.opsForValue().set(cacheKey, productFromDb);

        System.out.println("从数据库中获取商品,并存入缓存:" + productFromDb);
        return productFromDb;
    }
}

6. 启动类配置

确保 Spring Boot 项目启用了定时任务功能。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 开启定时任务功能
public class CachePreheatApplication {
    public static void main(String[] args) {
        SpringApplication.run(CachePreheatApplication.class, args);
    }
}

测试流程

  1. 冷门商品缓存预热

    • 每天凌晨 3 点,CachePreheatTask 定时任务会将冷门商品详情加载到 Redis 缓存中。
    • 可以通过日志查看哪些商品已被预热到缓存。
  2. 用户访问商品详情

    • 当用户访问 /product?productId=101 时,程序会优先从 Redis 缓存中读取数据。
    • 如果缓存中不存在数据,则从数据库中查询,并将查询结果写入缓存。

注意事项

  1. 缓存过期时间

    • 设置冷门商品缓存的过期时间,例如 1 天,避免长期占用缓存空间。
    redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.DAYS);
    
  2. 冷门商品定义

    • 可以根据业务需求调整冷门商品的定义逻辑,例如统计过去 7 天的访问量较低的商品。
  3. 异常处理

    • 在任务执行和缓存操作中添加异常处理逻辑,确保任务的健壮性。

4. 缓存加载器

具体场景:用户信息服务

  • 背景: 用户信息是核心业务数据(如用户详细资料、会员等级、偏好设置等),在用户登录或操作时需要频繁访问。
  • 应用: 使用缓存框架(如 Guava Cache、Ehcache 等)提供的加载器机制,每当缓存中没有用户信息时,自动从数据库中加载。
  • 案例:
    • 微信、支付宝等系统中,用户登录时自动加载用户资料到缓存。

在大型系统中,用户信息(例如用户名、头像、联系方式等)是被频繁访问的基础数据。为了避免频繁访问数据库(如 MySQL),可以通过 Redis 实现用户信息的缓存。这样,在用户信息发生更新时,可以同步更新缓存,平衡数据一致性与系统性能。

以下是一个实现用户信息服务,它包括用户信息的查询、缓存、更新和缓存失效处理。


实现思路

  1. 用户信息数据结构

    • 用户信息包括用户 ID、用户名、邮箱、头像等。
    • 数据通常存储在数据库中,但为了提高访问效率,可以利用 Redis 进行缓存。
  2. 缓存机制

    • 用户信息的查询:优先从 Redis 中读取,如果没有命中缓存,则从数据库读取并写入缓存。
    • 用户信息的更新:更新数据库后,同时更新 Redis 缓存。
    • 缓存过期策略:设置合理的缓存过期时间(如 1 小时),确保缓存数据不过时。
  3. 技术栈

    • Spring Boot:开发框架。
    • Redis:缓存存储。
    • MySQL:模拟用户信息存储。
    • Spring Data Redis:操作 Redis。

代码实现

1. 数据模型

创建一个 User 类,表示用户信息。

public class User {
    private Long id;        // 用户ID
    private String username; // 用户名
    private String email;    // 邮箱
    private String avatar;   // 头像

    // 构造方法、Getters 和 Setters
    public User(Long id, String username, String email, String avatar) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.avatar = avatar;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", email='" + email + '\'' +
                ", avatar='" + avatar + '\'' +
                '}';
    }

    // Getters 和 Setters 省略
}

2. 模拟数据库服务

创建 UserRepository 类,用于模拟数据库操作。

import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;

@Repository
public class UserRepository {

    // 模拟数据库的用户数据
    private static final Map<Long, User> DATABASE = new HashMap<>();

    static {
        DATABASE.put(1L, new User(1L, "Alice", "alice@example.com", "avatar1.png"));
        DATABASE.put(2L, new User(2L, "Bob", "bob@example.com", "avatar2.png"));
        DATABASE.put(3L, new User(3L, "Charlie", "charlie@example.com", "avatar3.png"));
    }

    // 根据用户ID查询用户信息
    public User getUserById(Long userId) {
        return DATABASE.get(userId);
    }

    // 更新数据库中的用户信息
    public void updateUser(User user) {
        DATABASE.put(user.getId(), user);
    }
}

3. 用户服务类

创建 UserService 类,处理用户信息的查询和更新逻辑。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class UserService {

    private static final String USER_CACHE_KEY_PREFIX = "USER_";

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 查询用户信息,优先从缓存中获取
     */
    public User getUserById(Long userId) {
        String cacheKey = USER_CACHE_KEY_PREFIX + userId;

        // 1. 从 Redis 缓存中获取用户信息
        User cachedUser = (User) redisTemplate.opsForValue().get(cacheKey);
        if (cachedUser != null) {
            System.out.println("从缓存中获取用户信息:" + cachedUser);
            return cachedUser;
        }

        // 2. 如果缓存中没有,则从数据库中查询
        User userFromDb = userRepository.getUserById(userId);
        if (userFromDb != null) {
            // 3. 将用户信息写入缓存,设置过期时间为 1 小时
            redisTemplate.opsForValue().set(cacheKey, userFromDb, 1, TimeUnit.HOURS);
            System.out.println("从数据库中获取用户信息,并存入缓存:" + userFromDb);
        }
        return userFromDb;
    }

    /**
     * 更新用户信息,同时更新缓存
     */
    public void updateUser(User user) {
        // 1. 更新数据库中的用户信息
        userRepository.updateUser(user);

        // 2. 更新 Redis 缓存
        String cacheKey = USER_CACHE_KEY_PREFIX + user.getId();
        redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
        System.out.println("更新用户信息,并同步更新缓存:" + user);
    }

    /**
     * 删除用户缓存(可用于强制失效)
     */
    public void deleteUserCache(Long userId) {
        String cacheKey = USER_CACHE_KEY_PREFIX + userId;
        redisTemplate.delete(cacheKey);
        System.out.println("删除缓存中的用户信息,用户ID:" + userId);
    }
}

4. 用户控制器

创建 UserController,提供接口供外部访问。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 获取用户信息
     */
    @GetMapping("/{userId}")
    public User getUser(@PathVariable Long userId) {
        return userService.getUserById(userId);
    }

    /**
     * 更新用户信息
     */
    @PutMapping("/{userId}")
    public String updateUser(@PathVariable Long userId, @RequestBody User user) {
        user.setId(userId);
        userService.updateUser(user);
        return "用户信息已更新";
    }

    /**
     * 删除用户缓存
     */
    @DeleteMapping("/cache/{userId}")
    public String deleteUserCache(@PathVariable Long userId) {
        userService.deleteUserCache(userId);
        return "用户缓存已删除";
    }
}

5. Redis 配置

application.properties 中配置 Redis。

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

6. 启动类

确保 Spring Boot 项目正常运行。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

测试流程

  1. 查询用户信息

    • 请求 GET /user/1,系统会优先从 Redis 缓存中获取用户信息。
    • 如果缓存中不存在,则从数据库读取并写入缓存。
  2. 更新用户信息

    • 请求 PUT /user/1,传入新的用户信息,系统会更新数据库并同步更新缓存。
  3. 删除用户缓存

    • 请求 DELETE /user/cache/1,可以手动删除 Redis 中的用户缓存,用于强制失效。

注意事项

  1. 缓存过期策略

    • 设定合理的缓存过期时间,例如 1 小时,避免缓存中的用户信息长期不更新。
  2. 一致性处理

    • 在高并发场景下,可能会出现数据库与缓存数据不一致的问题,可以通过延时双删策略或消息队列解决。
  3. 异常处理

    • 需要处理 Redis 连接异常的情况,确保系统可以回退到直接访问数据库。

5. 手动触发预热

具体场景:系统上线或版本升级

  • 背景: 某电商平台上线新版本,涉及多个模块的数据变更(如新品推荐、活动页面),需要提前预热缓存以避免启动时的高数据库压力。
  • 应用: 管理员通过后台管理系统或调用接口,手动触发缓存预热,将热点数据加载到缓存中,确保用户访问时快速响应。
  • 案例:
    • 活动上线(如双十一、618 等大促活动)前,手动预热活动页的商品列表、优惠信息等。

在实际开发中,系统上线或版本升级是一个非常重要的环节。为了保证系统平稳过渡,减少影响甚至避免服务中断,需要设计合理的上线和升级机制。以下是一个典型的场景:

  1. 场景需求

    • 系统需要进行版本升级,例如新增功能、修复 Bug。
    • 升级过程中不能中断用户的正常访问。
    • 需要支持多实例的分布式部署场景,并确保新版本上线后服务稳定运行。
  2. 技术方案

    • 蓝绿发布:通过部署两套环境(蓝+绿),实现版本的无缝切换。
    • 灰度发布:逐步将流量从旧版本流向新版本,验证新版本稳定后逐步放量。
    • 分布式锁:确保多实例场景下升级任务的唯一性和同步。
    • 热加载或动态配置:在不需要重新启动服务的情况下实现配置的动态调整。
  3. 技术栈

    • Spring Boot:作为应用服务框架。
    • Redis:用于实现分布式锁。
    • Nginx:实现负载均衡和蓝绿发布。
    • Spring Cloud Config:管理动态配置。

实现代码:系统升级与发布实现

以下是一个基于 Spring Boot 和 Redis 的系统升级和版本管理的 Java 示例代码,涵盖以下功能:

  1. 模拟蓝绿发布。
  2. 支持灰度发布。
  3. 提供版本切换的动态接口。

1. 版本模型

定义 Version 类,用于表示系统的版本信息。

public class Version {
    private String versionId;    // 版本号
    private String description; // 版本描述
    private boolean isActive;   // 是否为当前活跃版本

    public Version(String versionId, String description, boolean isActive) {
        this.versionId = versionId;
        this.description = description;
        this.isActive = isActive;
    }

    public String getVersionId() {
        return versionId;
    }

    public void setVersionId(String versionId) {
        this.versionId = versionId;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isActive() {
        return isActive;
    }

    public void setActive(boolean active) {
        isActive = active;
    }

    @Override
    public String toString() {
        return "Version{" +
                "versionId='" + versionId + '\'' +
                ", description='" + description + '\'' +
                ", isActive=" + isActive +
                '}';
    }
}

2. Redis 分布式锁工具类

在分布式场景下,避免同时对版本进行多次切换操作,需要使用 Redis 实现分布式锁。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 尝试获取分布式锁
     */
    public boolean tryLock(String key, String value, long timeout) {
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
        return success != null && success;
    }

    /**
     * 释放分布式锁
     */
    public void unlock(String key, String value) {
        String currentValue = redisTemplate.opsForValue().get(key);
        if (value.equals(currentValue)) {
            redisTemplate.delete(key);
        }
    }
}

3. 版本切换服务

创建 VersionService 类,负责版本管理和切换逻辑。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class VersionService {

    private static final String ACTIVE_VERSION_KEY = "ACTIVE_VERSION";
    private static final String REDIS_LOCK_KEY = "VERSION_LOCK";

    @Autowired
    private RedisLock redisLock;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 模拟的版本列表
    private List<Version> versionList = new ArrayList<>();

    public VersionService() {
        versionList.add(new Version("1.0.0", "初始版本", true));
        versionList.add(new Version("1.1.0", "优化功能", false));
        versionList.add(new Version("2.0.0", "全新功能", false));
    }

    /**
     * 获取当前活跃版本
     */
    public Version getActiveVersion() {
        String activeVersionId = (String) redisTemplate.opsForValue().get(ACTIVE_VERSION_KEY);
        return versionList.stream()
                .filter(version -> version.getVersionId().equals(activeVersionId))
                .findFirst()
                .orElse(null);
    }

    /**
     * 切换到指定版本
     */
    public String switchToVersion(String versionId) {
        String lockValue = String.valueOf(System.currentTimeMillis());

        // 加分布式锁,防止并发切换版本
        if (!redisLock.tryLock(REDIS_LOCK_KEY, lockValue, 10)) {
            return "系统升级正在进行中,请稍后再试!";
        }

        try {
            for (Version version : versionList) {
                version.setActive(version.getVersionId().equals(versionId));
            }

            // 更新 Redis 中的活跃版本
            redisTemplate.opsForValue().set(ACTIVE_VERSION_KEY, versionId);

            return "系统已成功切换到版本:" + versionId;
        } finally {
            redisLock.unlock(REDIS_LOCK_KEY, lockValue);
        }
    }

    /**
     * 获取所有版本信息
     */
    public List<Version> getAllVersions() {
        return versionList;
    }
}

4. 版本管理控制器

创建 VersionController,提供外部接口。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/version")
public class VersionController {

    @Autowired
    private VersionService versionService;

    /**
     * 获取当前活跃版本
     */
    @GetMapping("/active")
    public Version getActiveVersion() {
        return versionService.getActiveVersion();
    }

    /**
     * 获取所有版本信息
     */
    @GetMapping("/all")
    public List<Version> getAllVersions() {
        return versionService.getAllVersions();
    }

    /**
     * 切换版本
     */
    @PostMapping("/switch")
    public String switchToVersion(@RequestParam String versionId) {
        return versionService.switchToVersion(versionId);
    }
}

5. Redis 配置

application.properties 中添加 Redis 配置。

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

6. 启动类

确保 Spring Boot 项目正常启动。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class VersionUpgradeApplication {
    public static void main(String[] args) {
        SpringApplication.run(VersionUpgradeApplication.class, args);
    }
}

测试说明

  1. 获取当前活跃版本

    • 请求 GET /version/active
    • 返回当前正在使用的版本信息。
  2. 切换版本

    • 请求 POST /version/switch?versionId=1.1.0
    • 系统切换到指定版本,返回切换结果。
  3. 获取所有版本信息

    • 请求 GET /version/all
    • 返回系统中的所有版本信息。

注意事项

  1. 蓝绿发布实现

    • 配合 Nginx 的负载均衡策略,可以将流量切换到新版本服务,同时保留旧版本服务作为备份。
  2. 灰度发布实现

    • 可以结合用户 ID 或其他特性,逐步将部分用户的流量路由到新版本服务。
  3. 回滚机制

    • 如果新版本出现问题,可以通过接口快速切换回旧版本,确保系统稳定。
  4. 分布式锁

    • 确保版本切换操作的唯一性,避免多实例同时操作导致数据不一致。

6. 基于消息队列的异步预热

具体场景:秒杀活动库存更新

  • 背景: 在秒杀活动中,商品库存信息需要实时更新到缓存中,而库存变更频率高,不能全部通过直接数据库查询完成。
  • 应用: 当秒杀活动库存发生变化时,通过消息队列通知缓存服务更新缓存,确保用户查询时可以获取最新的库存数据。
  • 案例:
    • 秒杀系统:当用户下单或取消订单时,库存变化通过 Kafka 消息队列触发缓存更新。

秒杀活动是一种高并发场景,它的主要挑战在于如何合理地处理库存的扣减操作,同时确保系统的高性能和数据准确性。以下是秒杀场景中常见的功能需求和解决方案:

  1. 场景需求

    • 商品库存有限,例如某商品的库存为 100,用户需要在秒杀活动中抢购。
    • 支持高并发访问,避免超卖(即库存为负)。
    • 保证库存的一致性,避免多线程或多实例扣减库存时出现冲突。
  2. 挑战点

    • 高并发:秒杀活动通常伴随着大量用户瞬时访问,需避免数据库的性能瓶颈。
    • 超卖问题:需要严格控制库存,确保不会出现售出的商品数量超过实际库存的情况。
    • 分布式场景:在多实例、多节点的分布式环境下,需要解决并发操作的问题。
  3. 优化方案

    • 缓存预扣减:通过 Redis 实现库存的分布式锁和扣减操作。
    • 队列异步处理:将用户请求放入消息队列中,异步操作数据库,避免高并发直接操作数据库。
    • 库存校验与回滚:多重校验库存,避免超卖,同时处理异常时的回滚逻辑。

实现方案与代码

以下示例代码通过 Redis 实现秒杀活动的库存管理,同时结合 Spring Boot 和分布式锁,确保库存扣减的一致性和高效性。


1. 商品库存模型

创建 Product 类,表示商品的基本信息和库存。

public class Product {
    private Long productId;    // 商品ID
    private String name;       // 商品名称
    private Integer stock;     // 商品库存

    public Product(Long productId, String name, Integer stock) {
        this.productId = productId;
        this.name = name;
        this.stock = stock;
    }

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    @Override
    public String toString() {
        return "Product{" +
                "productId=" + productId +
                ", name='" + name + '\'' +
                ", stock=" + stock +
                '}';
    }
}

2. Redis 分布式锁工具类

通过 Redis 实现分布式锁,避免多线程或多实例同时扣减库存。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 尝试加锁
     */
    public boolean tryLock(String key, String value, long timeout) {
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
        return success != null && success;
    }

    /**
     * 释放锁
     */
    public void unlock(String key, String value) {
        String currentValue = redisTemplate.opsForValue().get(key);
        if (value.equals(currentValue)) {
            redisTemplate.delete(key);
        }
    }
}

3. 秒杀服务

创建 SeckillService 类,负责秒杀活动的主要逻辑。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class SeckillService {

    private static final String STOCK_KEY_PREFIX = "STOCK_";
    private static final String LOCK_KEY_PREFIX = "LOCK_";

    @Autowired
    private RedisLock redisLock;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 初始化商品库存
     */
    public void initializeStock(Long productId, Integer stock) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        redisTemplate.opsForValue().set(stockKey, stock);
        System.out.println("商品库存初始化成功,商品ID:" + productId + ",库存:" + stock);
    }

    /**
     * 秒杀逻辑
     */
    public String seckill(Long productId) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        String lockKey = LOCK_KEY_PREFIX + productId;
        String lockValue = String.valueOf(System.currentTimeMillis());

        // 获取分布式锁
        if (!redisLock.tryLock(lockKey, lockValue, 5)) {
            return "系统繁忙,请稍后再试";
        }

        try {
            // 检查库存
            Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
            if (stock == null || stock <= 0) {
                return "秒杀已结束";
            }

            // 扣减库存
            redisTemplate.opsForValue().decrement(stockKey);
            System.out.println("秒杀成功,商品ID:" + productId + ",剩余库存:" + (stock - 1));
            return "恭喜您,秒杀成功!";
        } finally {
            // 释放锁
            redisLock.unlock(lockKey, lockValue);
        }
    }
}

4. 秒杀接口控制器

创建 SeckillController 类,提供秒杀活动的接口。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/seckill")
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    /**
     * 初始化库存
     */
    @PostMapping("/init")
    public String initializeStock(@RequestParam Long productId, @RequestParam Integer stock) {
        seckillService.initializeStock(productId, stock);
        return "库存初始化成功";
    }

    /**
     * 秒杀商品
     */
    @PostMapping("/buy")
    public String seckill(@RequestParam Long productId) {
        return seckillService.seckill(productId);
    }
}

5. Redis 配置

application.properties 中配置 Redis。

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

6. 启动类

确保 Spring Boot 项目正常启动。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SeckillApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeckillApplication.class, args);
    }
}

测试流程

  1. 初始化库存

    • 请求 POST /seckill/init,传入商品 ID 和库存数量,例如:
      curl -X POST "http://localhost:8080/seckill/init?productId=1&stock=100"
      
    • 返回结果:库存初始化成功
  2. 秒杀商品

    • 请求 POST /seckill/buy,传入商品 ID,例如:
      curl -X POST "http://localhost:8080/seckill/buy?productId=1"
      
    • 库存减少,返回结果:恭喜您,秒杀成功!
  3. 库存不足

    • 当库存为 0 时,返回结果:秒杀已结束

注意事项

  1. 超卖问题

    • 使用 Redis 的分布式锁和原子操作(如 decrement),可以有效避免超卖问题。
  2. 高并发优化

    • 可将用户请求写入消息队列(如 RabbitMQ、Kafka),异步扣减库存,提高系统吞吐量。
  3. 库存回滚

    • 如果订单未支付或超时取消,需要实现库存的回滚逻辑。
  4. 限流保护

    • 为了防止恶意请求,可通过限流机制(如令牌桶算法)限制每秒请求数。

通过以上代码,结合 Redis 和分布式锁,可以实现一个高效、安全的秒杀活动库存更新逻辑,适用于多线程和分布式场景。


7. 冷热分离策略

具体场景:视频网站的热门视频

  • 背景: 视频平台中的内容分为热门视频(高播放量)和冷门视频(低播放量),为了节省缓存空间,仅对热门视频进行缓存。
  • 应用: 使用数据分析工具(如大数据日志分析)统计视频的播放量,根据阈值识别热门视频并加载到缓存中。
  • 案例:
    • 爱奇艺、腾讯视频等平台,将高播放量的热播剧、电影缓存到 CDN 节点或 Redis,低播放量视频不缓存。

视频网站的“热门视频”功能是一个典型的高并发、高实时性场景。其核心需求是在用户大量访问的情况下,实时统计视频的热度(如播放次数、点赞数等),并快速返回热门视频排行。


核心需求

  1. 视频热度的实时统计

    • 统计视频的播放次数、点赞次数等,作为热度的评估指标。
    • 热度计算需要支持高并发读写操作。
  2. 热门视频排行榜

    • 根据视频热度动态生成排名。
    • 支持按时间段(如日榜、周榜、月榜)统计。
  3. 高性能要求

    • 网站用户量大,需要保证高并发情况下的性能。
    • 避免直接操作数据库,推荐使用缓存(如 Redis)来存储热度信息。
  4. 扩展性

    • 热度计算逻辑可以根据需求调整,例如权重(播放次数占比、点赞占比等)。
    • 支持分布式部署。

技术选型

  1. Redis

    • 使用 Redis 的数据结构(如 SortedSet)存储视频热度和排行榜,实现高效的排名功能。
    • 使用 Redis 的原子操作(如 INCR)统计播放次数、点赞次数等。
  2. Spring Boot

    • 提供接口服务,供前端或用户调用。
    • 处理热度数据的读写逻辑。

实现方案

以下是基于 Redis 和 Spring Boot 实现的视频网站热门视频功能代码。


1. 视频模型

创建 Video 类,表示视频的基本信息。

public class Video {
    private Long videoId;      // 视频ID
    private String title;      // 视频标题
    private String description; // 视频描述

    public Video(Long videoId, String title, String description) {
        this.videoId = videoId;
        this.title = title;
        this.description = description;
    }

    public Long getVideoId() {
        return videoId;
    }

    public void setVideoId(Long videoId) {
        this.videoId = videoId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Video{" +
                "videoId=" + videoId +
                ", title='" + title + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}

2. Redis 操作服务

通过 Redis 的 SortedSet 数据结构实现热度统计和排行榜。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class HotVideoService {

    private static final String VIDEO_SCORE_KEY = "HOT_VIDEOS";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 增加视频播放次数
     */
    public void incrementPlayCount(Long videoId) {
        redisTemplate.opsForZSet().incrementScore(VIDEO_SCORE_KEY, videoId, 1);
    }

    /**
     * 增加视频点赞次数
     */
    public void incrementLikeCount(Long videoId) {
        redisTemplate.opsForZSet().incrementScore(VIDEO_SCORE_KEY, videoId, 2);
    }

    /**
     * 获取热门视频排行榜
     */
    public Set<ZSetOperations.TypedTuple<Object>> getHotVideos(int topN) {
        return redisTemplate.opsForZSet().reverseRangeWithScores(VIDEO_SCORE_KEY, 0, topN - 1);
    }
}

3. 热门视频控制器

创建 HotVideoController,提供播放、点赞和获取排行榜的接口服务。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@RestController
@RequestMapping("/hot-videos")
public class HotVideoController {

    @Autowired
    private HotVideoService hotVideoService;

    /**
     * 视频播放次数增加
     */
    @PostMapping("/play/{videoId}")
    public String playVideo(@PathVariable Long videoId) {
        hotVideoService.incrementPlayCount(videoId);
        return "播放次数增加成功";
    }

    /**
     * 视频点赞次数增加
     */
    @PostMapping("/like/{videoId}")
    public String likeVideo(@PathVariable Long videoId) {
        hotVideoService.incrementLikeCount(videoId);
        return "点赞次数增加成功";
    }

    /**
     * 获取热门视频排行榜
     */
    @GetMapping("/rank")
    public Map<String, Object> getHotVideoRank(@RequestParam(defaultValue = "10") int topN) {
        Set<ZSetOperations.TypedTuple<Object>> hotVideos = hotVideoService.getHotVideos(topN);

        Map<String, Object> response = new HashMap<>();
        response.put("topN", topN);
        response.put("data", hotVideos);
        return response;
    }
}

4. Redis 配置

application.properties 中配置 Redis。

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

5. 启动类

确保 Spring Boot 项目正常启动。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HotVideoApplication {
    public static void main(String[] args) {
        SpringApplication.run(HotVideoApplication.class, args);
    }
}

测试流程

  1. 模拟播放增加

    • 请求 POST /hot-videos/play/{videoId}
    • 例如:
      curl -X POST "http://localhost:8080/hot-videos/play/1"
      
    • 返回结果:播放次数增加成功
  2. 模拟点赞增加

    • 请求 POST /hot-videos/like/{videoId}
    • 例如:
      curl -X POST "http://localhost:8080/hot-videos/like/1"
      
    • 返回结果:点赞次数增加成功
  3. 获取热门视频排行榜

    • 请求 GET /hot-videos/rank?topN=5
    • 例如:
      curl "http://localhost:8080/hot-videos/rank?topN=5"
      
    • 返回结果为排行榜数据,例如:
      {
          "topN": 5,
          "data": [
              {"value": "1", "score": 25.0},
              {"value": "2", "score": 20.0}
          ]
      }
      

方案扩展

  1. 权重调整

    • 热度计算可以结合播放次数和点赞次数的权重,例如:热度 = 播放次数 * 1 + 点赞次数 * 2
    • 调整 incrementPlayCountincrementLikeCount 方法中的分值即可实现。
  2. 时间范围排行榜

    • 使用 Redis 的 ZSet 有效时间设置,生成按日、周、月的排行榜。例如:
      • 使用不同的 key 表示时间段:HOT_VIDEOS_DAYHOT_VIDEOS_WEEK
      • 定时任务重置对应时间段的排行榜。
  3. 限流保护

    • 对播放和点赞接口添加限流策略,防止恶意刷热度。
  4. 视频详情结合

    • 在获取排行榜时,结合数据库查询视频的详情信息(如标题、封面)返回。

通过 Redis 的 SortedSet 数据结构,可以高效地实现视频网站的热门视频功能。该方案性能优异,适合高并发场景,同时支持灵活扩展(如权重调整、时间段排名)。结合 Spring Boot 提供的接口服务,能够快速构建热门视频功能。


8. 与 CDN 结合的缓存预热

具体场景:大型静态资源(如图片、视频、CSS 文件)

  • 背景: 网站上线新版本时,包含大量静态资源(如 JS、CSS 文件、图片等),这些资源需要分发到全球各地的 CDN 节点,确保用户快速加载。
  • 应用: 在上线前调用 CDN 服务商的预热 API,将指定的静态资源推送到 CDN 节点缓存中,用户访问时直接从 CDN 获取数据。
  • 案例:
    • 阿里云、腾讯云 CDN 的缓存预热功能,适用于电商、门户网站等需要大规模分发静态资源的场景。

对于大型静态资源(如图片、视频、CSS 文件)的管理与分发,是现代互联网项目中非常重要的一环。主要需求包括:

  1. 静态资源的高效存储与分发

    • 图片、视频、CSS 等静态资源需要存储在高效的存储服务中。
    • 提供快速分发的能力,尤其在高并发访问时,保证性能和稳定性。
  2. 内容优化

    • 大型文件需要通过压缩、切片等方式优化网络传输。
    • 支持分块下载、断点续传等。
  3. 权限控制

    • 部分静态资源(如视频、图片)可能需要基于用户权限进行访问控制。
  4. 缓存机制

    • 静态资源通常通过 HTTP 缓存和 CDN(内容分发网络)加速访问,减少服务器压力。

实现方案与技术选型

  1. 存储服务

    • 使用云存储服务(如阿里云 OSS、腾讯云 COS、亚马逊 S3)存储静态资源。
    • 如果是本地存储,可以选择 Nginx 作为静态资源服务器。
  2. 分发优化

    • 配置 CDN(如阿里云 CDN、Cloudflare)加速资源分发。
    • 实现分块下载、断点续传等功能。
  3. 传输优化

    • 对图片进行压缩(如 WebP 格式)、对视频进行转码(如 H.265 编码)。
    • 使用 GZIP 或 Brotli 压缩传输 CSS、JS 等文件。
  4. 权限控制

    • 通过生成签名 URL 实现资源的时间有效性控制。
    • 对用户权限进行校验,例如 VIP 用户才能访问特定视频。

Java 实现案例

以下是一个基于 Spring Boot 的静态资源管理实现代码,结合本地存储和阿里云 OSS 示例。


1. 静态资源上传与下载服务

本地存储实现

本地存储的场景适合开发测试环境或中小型项目部署场景。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

@Service
public class LocalStorageService {

    @Value("${static.resource.path:/tmp/static-resources}")
    private String storagePath;

    /**
     * 上传静态资源
     */
    public String uploadResource(MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }

        // 创建存储目录
        File dir = new File(storagePath);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // 保存文件
        String filePath = storagePath + File.separator + file.getOriginalFilename();
        file.transferTo(new File(filePath));
        return filePath;
    }

    /**
     * 下载静态资源
     */
    public byte[] downloadResource(String fileName) throws IOException {
        String filePath = storagePath + File.separator + fileName;
        File file = new File(filePath);
        if (!file.exists()) {
            throw new IllegalArgumentException("文件不存在");
        }
        return Files.readAllBytes(Paths.get(filePath));
    }
}

阿里云 OSS 实现

阿里云 OSS 适用于生产环境,提供高效的静态资源存储与分发。

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;

@Service
public class AliyunOssService {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;

    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;

    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;

    @Value("${aliyun.oss.bucketName}")
    private String bucketName;

    /**
     * 上传文件到阿里云 OSS
     */
    public String uploadResource(MultipartFile file) throws Exception {
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try (InputStream inputStream = file.getInputStream()) {
            String fileName = file.getOriginalFilename();
            ossClient.putObject(bucketName, fileName, inputStream);
            return "https://" + bucketName + "." + endpoint + "/" + fileName;
        } finally {
            ossClient.shutdown();
        }
    }

    /**
     * 生成签名 URL(用于权限控制)
     */
    public String generatePresignedUrl(String fileName, int expirationInSeconds) {
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            java.util.Date expiration = new java.util.Date(System.currentTimeMillis() + expirationInSeconds * 1000);
            return ossClient.generatePresignedUrl(bucketName, fileName, expiration).toString();
        } finally {
            ossClient.shutdown();
        }
    }
}

2. 静态资源控制器

创建 StaticResourceController,提供接口用于文件上传和下载。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@RestController
@RequestMapping("/resources")
public class StaticResourceController {

    @Autowired
    private LocalStorageService localStorageService;

    @Autowired
    private AliyunOssService aliyunOssService;

    /**
     * 上传文件(本地存储)
     */
    @PostMapping("/local/upload")
    public String uploadLocal(@RequestParam("file") MultipartFile file) throws IOException {
        return localStorageService.uploadResource(file);
    }

    /**
     * 上传文件(阿里云 OSS 存储)
     */
    @PostMapping("/aliyun/upload")
    public String uploadAliyun(@RequestParam("file") MultipartFile file) throws Exception {
        return aliyunOssService.uploadResource(file);
    }

    /**
     * 下载文件(本地存储)
     */
    @GetMapping("/local/download/{fileName}")
    public void downloadLocal(@PathVariable String fileName, HttpServletResponse response) throws IOException {
        byte[] data = localStorageService.downloadResource(fileName);
        response.setContentType("application/octet-stream");
        response.getOutputStream().write(data);
    }

    /**
     * 获取阿里云 OSS 签名 URL
     */
    @GetMapping("/aliyun/presigned/{fileName}")
    public String getAliyunPresignedUrl(@PathVariable String fileName, @RequestParam(defaultValue = "3600") int expirationInSeconds) {
        return aliyunOssService.generatePresignedUrl(fileName, expirationInSeconds);
    }
}

3. 配置文件

配置 application.properties 文件。

# 本地存储路径
static.resource.path=/tmp/static-resources

# 阿里云 OSS 配置
aliyun.oss.endpoint=oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=yourAccessKeyId
aliyun.oss.accessKeySecret=yourAccessKeySecret
aliyun.oss.bucketName=yourBucketName

测试流程

  1. 上传文件到本地存储

    • 请求 POST /resources/local/upload
    • 使用 curl 测试:
      curl -F "file=@/path/to/file.jpg" http://localhost:8080/resources/local/upload
      
    • 返回文件保存路径。
  2. 下载文件(本地存储)

    • 请求 GET /resources/local/download/{fileName}
    • 返回文件数据。
  3. 上传文件到阿里云 OSS

    • 请求 POST /resources/aliyun/upload
    • 返回 OSS 文件访问路径。
  4. 获取阿里云 OSS 签名 URL

    • 请求 GET /resources/aliyun/presigned/{fileName}?expirationInSeconds=3600
    • 返回带签名的 URL,可用于临时访问。

扩展与优化

  1. CDN 配置

    • 将阿里云 OSS 或本地存储文件配置到 CDN,提高静态资源分发效率。
  2. 断点续传与分块上传

    • 对于大文件(如视频),可以使用阿里云 OSS 的分块上传功能。
  3. 缓存优化

    • 配置 HTTP 缓存头(如 Cache-ControlETag)支持客户端缓存。
  4. 安全性

    • 使用签名 URL 或 JWT Token 验证用户权限,确保资源访问安全。

9. 基于 AI 的智能预热

具体场景:内容推荐系统

  • 背景: 内容推荐平台(如新闻、视频、社交应用)需要提前加载用户可能感兴趣的内容到缓存中,提升推荐系统的响应速度。
  • 应用: 通过机器学习算法预测用户的兴趣内容,根据预测结果将热点内容提前加载到缓存中。
  • 案例:
    • 今日头条、抖音等平台,根据用户历史行为和推荐算法,提前缓存可能点击的内容。

推荐系统是互联网应用中的核心功能之一,广泛用于电商、视频平台、新闻门户等领域。推荐系统的目标是根据用户的兴趣或行为,为其提供个性化的内容推荐。以下我们以视频网站为例,设计一个简单的内容推荐系统。


核心需求

  1. 用户行为收集

    • 收集用户的浏览行为(如观看、点赞、评论、收藏等),作为推荐的基础数据。
  2. 推荐算法

    • 实现协同过滤算法(基于用户行为或内容相似性)或热门推荐算法。
    • 提供基于简单规则的个性化内容推荐。
  3. 实时性

    • 热门内容推荐需要实时更新,例如根据播放次数或点赞数排序。
    • 个性化推荐需要根据用户最近的行为动态调整。
  4. 扩展性

    • 系统应支持算法扩展,如引入深度学习模型或增强推荐策略。

技术选型

  1. 存储

    • 使用 MySQL 或 MongoDB 存储用户行为数据。
    • 使用 Redis 缓存实时热门内容,以提高推荐性能。
  2. 推荐算法

    • 基于协同过滤(Collaborative Filtering)的简单实现。
    • 基于热门内容的推荐(如 Redis 的 SortedSet 实现)。
  3. 技术框架

    • Spring Boot:实现推荐系统的 RESTful API。
    • Redis:支持实时数据存储和推荐计算。

实现方案

以下是一个基于 Spring Boot 的内容推荐系统实现案例。


1. 数据模型设计

用户行为数据模型

用于存储用户的行为记录,例如观看记录、点赞记录。

import javax.persistence.*;
import java.util.Date;

@Entity
@Table(name = "user_behavior")
public class UserBehavior {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long userId;        // 用户ID
    private Long contentId;     // 内容ID
    private String actionType;  // 行为类型:watch、like、comment、favorite
    private Date actionTime;    // 行为发生时间

    // Getters and Setters
}
内容数据模型

用于存储视频或文章的基本信息。

import javax.persistence.*;

@Entity
@Table(name = "content")
public class Content {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;       // 内容标题
    private String description; // 内容简介
    private String tags;        // 标签(逗号分隔)
    private Integer popularity; // 热度(播放次数、点赞数等)

    // Getters and Setters
}

2. 用户行为数据收集服务

创建服务接口,用于记录用户行为。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;

@Service
public class UserBehaviorService {

    @Autowired
    private UserBehaviorRepository userBehaviorRepository;

    /**
     * 记录用户行为
     */
    public void logUserBehavior(Long userId, Long contentId, String actionType) {
        UserBehavior behavior = new UserBehavior();
        behavior.setUserId(userId);
        behavior.setContentId(contentId);
        behavior.setActionType(actionType);
        behavior.setActionTime(new Date());
        userBehaviorRepository.save(behavior);
    }
}

3. 推荐逻辑与算法服务

热门推荐服务

基于 Redis 的 SortedSet 实现热门内容推荐。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class HotContentService {

    private static final String HOT_CONTENT_KEY = "hot_content";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 更新内容热度
     */
    public void updateContentPopularity(Long contentId, int score) {
        redisTemplate.opsForZSet().incrementScore(HOT_CONTENT_KEY, contentId.toString(), score);
    }

    /**
     * 获取热门内容
     */
    public Set<ZSetOperations.TypedTuple<Object>> getHotContent(int topN) {
        return redisTemplate.opsForZSet().reverseRangeWithScores(HOT_CONTENT_KEY, 0, topN - 1);
    }
}
个性化推荐服务

基于简单的协同过滤实现个性化推荐。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class RecommendationService {

    @Autowired
    private UserBehaviorRepository userBehaviorRepository;

    @Autowired
    private ContentRepository contentRepository;

    /**
     * 基于简单协同过滤的推荐
     */
    public List<Content> recommendForUser(Long userId) {
        // 获取当前用户的行为内容ID
        List<Long> userContentIds = userBehaviorRepository.findContentIdsByUserId(userId);

        // 获取与当前用户行为相似的用户
        List<Long> similarUserIds = userBehaviorRepository.findSimilarUsers(userContentIds);

        // 获取相似用户的推荐内容(去重当前用户已浏览内容)
        Set<Long> recommendedContentIds = new HashSet<>();
        for (Long similarUserId : similarUserIds) {
            List<Long> similarUserContentIds = userBehaviorRepository.findContentIdsByUserId(similarUserId);
            recommendedContentIds.addAll(similarUserContentIds);
        }
        recommendedContentIds.removeAll(userContentIds);

        // 查询内容详情并返回
        return contentRepository.findAllById(recommendedContentIds);
    }
}

4. 推荐系统控制器

为用户提供推荐内容的 API 接口。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Set;

@RestController
@RequestMapping("/recommend")
public class RecommendController {

    @Autowired
    private HotContentService hotContentService;

    @Autowired
    private RecommendationService recommendationService;

    /**
     * 获取热门内容推荐
     */
    @GetMapping("/hot")
    public Set<Object> getHotContent(@RequestParam(defaultValue = "10") int topN) {
        return hotContentService.getHotContent(topN);
    }

    /**
     * 获取个性化推荐内容
     */
    @GetMapping("/personalized")
    public List<Content> getPersonalizedRecommendation(@RequestParam Long userId) {
        return recommendationService.recommendForUser(userId);
    }
}

5. 配置文件

配置 Redis 和数据库连接。

application.properties

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/recommendation
spring.datasource.username=root
spring.datasource.password=123456

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

测试流程

  1. 收集用户行为

    • 请求 POST /behavior,记录用户行为(如观看、点赞)。
    • 示例请求:
      curl -X POST -H "Content-Type: application/json" -d '{"userId":1,"contentId":100,"actionType":"watch"}' http://localhost:8080/behavior
      
  2. 热门推荐测试

    • 请求 GET /recommend/hot,获取热门内容。
    • 示例请求:
      curl "http://localhost:8080/recommend/hot?topN=5"
      
  3. 个性化推荐测试

    • 请求 GET /recommend/personalized?userId=1,获取用户个性化推荐。
    • 示例请求:
      curl "http://localhost:8080/recommend/personalized?userId=1"
      

扩展与优化

  1. 引入更复杂的推荐算法

    • 使用基于矩阵分解(Matrix Factorization)或深度学习的推荐算法(如 TensorFlow 或 PyTorch)。
  2. 支持实时推荐

    • 实时更新用户行为,结合 Kafka 等消息队列实现流式推荐。
  3. 缓存优化

    • 使用 Redis 缓存推荐结果,减少重复计算。
  4. 可解释性

    • 提供推荐理由,例如 "因为你喜欢某视频,推荐了类似内容"。

总结

方案场景案例
启动过程中预热电商首页商品推荐、新闻门户网站首页热点数据
定时任务预热每日热点新闻、定时更新电商商品价格或库存
用时加载(惰性加载)冷门商品详情页、后台管理系统中的非频繁操作
缓存加载器用户信息加载(如登录时自动加载用户资料)
手动触发预热大促活动上线(如双十一、618)、版本升级后需要的缓存加载
基于消息队列的异步预热秒杀系统库存更新、订单状态变化通知
冷热分离策略视频平台的热播剧缓存、流量分层热点内容(如电商爆款商品)
与 CDN 结合的缓存预热网站的大量静态资源(如 JS、CSS 文件、图片)
基于 AI 的智能预热新闻推荐、视频推荐系统的情景预测

选择合适的缓存预热方案需要结合业务特点数据访问规律以及技术实现难度,灵活调整策略以达到最佳性能。