以下是上述几种缓存预热方案的具体场景案例,帮助您更好地理解它们的适用场景和实际应用。
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. 代码说明
- 监听器触发预热: 使用 Spring 的
@EventListener注解,监听应用启动完成事件,确保预热逻辑在系统完全启动后执行。 - Redis 缓存: 使用
RedisTemplate实现缓存操作,设置了 1 小时的缓存过期时间(可以根据具体业务修改)。 - 模块化设计: 将数据库层、缓存层和控制器分离,方便扩展和维护。
5. 适用性和扩展
- 适用性: 该实现适用于需要在系统启动时加载固定热点数据的场景,例如电商首页、门户网站首页。
- 扩展:
- 可以引入动态配置(如配置文件定义哪些数据需要预热)。
- 数据量较大时,可以分批异步加载热点数据。
- 使用更高级的缓存框架(如 Caffeine)结合 Redis 提升性能。
2. 定时任务预热
具体场景:新闻门户网站的每日热点
- 背景: 新闻门户网站需要每天凌晨预加载当天的热点新闻,以便早晨用户访问时快速响应。
- 应用: 通过定时任务,每天凌晨 3 点从数据库中获取当天的热点新闻列表,并将这些新闻内容写入缓存。
- 案例:
- 新浪新闻、今日头条等平台,通过定时预热热点新闻,提升早高峰访问体验。
以下是一个基于 Java 和 Redis 的简单示例,展示如何为新闻门户网站的每日热点内容进行缓存预热。这种实现方案主要结合定时任务,将每日热点新闻提前加载到缓存中,确保用户访问时可以快速获取数据。
示例说明:
- 场景:新闻门户网站每天早晨生成热点新闻,提前将这些热点新闻数据加载到 Redis 缓存中。
- 技术栈:
- 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);
}
}
运行流程
- 每天凌晨 2 点,
CachePreheatTask定时任务会从NewsService中获取热点新闻,并将数据存入 Redis 缓存。 - 用户访问
/hot-news接口时,直接从 Redis 中读取热点新闻数据,提升访问速度。
注意事项
- Redis 缓存过期时间:可以根据业务需要为缓存设置过期时间,例如热点新闻数据每天更新,可以设置 24 小时过期。
redisTemplate.opsForValue().set(HOT_NEWS_CACHE_KEY, hotNews, 24, TimeUnit.HOURS); - 热点新闻的来源:实际生产中,新闻数据通常来自数据库或第三方 API,可以根据实际需求调整
fetchHotNews()的实现。 - 异常处理:需要处理 Redis 连接异常或数据加载失败的问题,确保任务的健壮性。
3. 用时加载(惰性加载)
具体场景:冷门商品详情页
- 背景: 电商平台中,某些商品访问量较少(冷门商品),不适合提前加载到缓存中,但在用户访问时需要快速响应。
- 应用: 用户请求冷门商品详情时,系统首先检查缓存是否存在数据。如果不存在,则从数据库加载商品详情,同时将数据写入缓存,供其他用户访问。
- 案例:
- 冷门商品、非主流视频、长尾关键词搜索结果等。
在电子商务平台上,冷门商品的访问量较少,但仍然可能被用户访问。为了提高这些冷门商品详情页的加载速度,可以通过缓存预热机制提前将这些商品的数据加载到缓存中。这样即使某个冷门商品被访问,也可以直接从缓存中读取数据,而无需频繁访问数据库,从而提高系统性能。
实现思路
- 定义冷门商品:统计一段时间内访问量较低的商品 ID。
- 冷门数据处理:将这些冷门商品的详情数据预先加载到缓存中。
- 缓存策略:将冷门商品放入缓存时设置一个合理的过期时间,以避免缓存占用过多资源。
- 技术栈:
- 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);
}
}
测试流程
-
冷门商品缓存预热:
- 每天凌晨 3 点,
CachePreheatTask定时任务会将冷门商品详情加载到 Redis 缓存中。 - 可以通过日志查看哪些商品已被预热到缓存。
- 每天凌晨 3 点,
-
用户访问商品详情:
- 当用户访问
/product?productId=101时,程序会优先从 Redis 缓存中读取数据。 - 如果缓存中不存在数据,则从数据库中查询,并将查询结果写入缓存。
- 当用户访问
注意事项
-
缓存过期时间:
- 设置冷门商品缓存的过期时间,例如 1 天,避免长期占用缓存空间。
redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.DAYS); -
冷门商品定义:
- 可以根据业务需求调整冷门商品的定义逻辑,例如统计过去 7 天的访问量较低的商品。
-
异常处理:
- 在任务执行和缓存操作中添加异常处理逻辑,确保任务的健壮性。
4. 缓存加载器
具体场景:用户信息服务
- 背景: 用户信息是核心业务数据(如用户详细资料、会员等级、偏好设置等),在用户登录或操作时需要频繁访问。
- 应用: 使用缓存框架(如 Guava Cache、Ehcache 等)提供的加载器机制,每当缓存中没有用户信息时,自动从数据库中加载。
- 案例:
- 微信、支付宝等系统中,用户登录时自动加载用户资料到缓存。
在大型系统中,用户信息(例如用户名、头像、联系方式等)是被频繁访问的基础数据。为了避免频繁访问数据库(如 MySQL),可以通过 Redis 实现用户信息的缓存。这样,在用户信息发生更新时,可以同步更新缓存,平衡数据一致性与系统性能。
以下是一个实现用户信息服务,它包括用户信息的查询、缓存、更新和缓存失效处理。
实现思路
-
用户信息数据结构:
- 用户信息包括用户 ID、用户名、邮箱、头像等。
- 数据通常存储在数据库中,但为了提高访问效率,可以利用 Redis 进行缓存。
-
缓存机制:
- 用户信息的查询:优先从 Redis 中读取,如果没有命中缓存,则从数据库读取并写入缓存。
- 用户信息的更新:更新数据库后,同时更新 Redis 缓存。
- 缓存过期策略:设置合理的缓存过期时间(如 1 小时),确保缓存数据不过时。
-
技术栈:
- 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);
}
}
测试流程
-
查询用户信息:
- 请求
GET /user/1,系统会优先从 Redis 缓存中获取用户信息。 - 如果缓存中不存在,则从数据库读取并写入缓存。
- 请求
-
更新用户信息:
- 请求
PUT /user/1,传入新的用户信息,系统会更新数据库并同步更新缓存。
- 请求
-
删除用户缓存:
- 请求
DELETE /user/cache/1,可以手动删除 Redis 中的用户缓存,用于强制失效。
- 请求
注意事项
-
缓存过期策略:
- 设定合理的缓存过期时间,例如 1 小时,避免缓存中的用户信息长期不更新。
-
一致性处理:
- 在高并发场景下,可能会出现数据库与缓存数据不一致的问题,可以通过延时双删策略或消息队列解决。
-
异常处理:
- 需要处理 Redis 连接异常的情况,确保系统可以回退到直接访问数据库。
5. 手动触发预热
具体场景:系统上线或版本升级
- 背景: 某电商平台上线新版本,涉及多个模块的数据变更(如新品推荐、活动页面),需要提前预热缓存以避免启动时的高数据库压力。
- 应用: 管理员通过后台管理系统或调用接口,手动触发缓存预热,将热点数据加载到缓存中,确保用户访问时快速响应。
- 案例:
- 活动上线(如双十一、618 等大促活动)前,手动预热活动页的商品列表、优惠信息等。
在实际开发中,系统上线或版本升级是一个非常重要的环节。为了保证系统平稳过渡,减少影响甚至避免服务中断,需要设计合理的上线和升级机制。以下是一个典型的场景:
-
场景需求:
- 系统需要进行版本升级,例如新增功能、修复 Bug。
- 升级过程中不能中断用户的正常访问。
- 需要支持多实例的分布式部署场景,并确保新版本上线后服务稳定运行。
-
技术方案:
- 蓝绿发布:通过部署两套环境(蓝+绿),实现版本的无缝切换。
- 灰度发布:逐步将流量从旧版本流向新版本,验证新版本稳定后逐步放量。
- 分布式锁:确保多实例场景下升级任务的唯一性和同步。
- 热加载或动态配置:在不需要重新启动服务的情况下实现配置的动态调整。
-
技术栈:
- Spring Boot:作为应用服务框架。
- Redis:用于实现分布式锁。
- Nginx:实现负载均衡和蓝绿发布。
- Spring Cloud Config:管理动态配置。
实现代码:系统升级与发布实现
以下是一个基于 Spring Boot 和 Redis 的系统升级和版本管理的 Java 示例代码,涵盖以下功能:
- 模拟蓝绿发布。
- 支持灰度发布。
- 提供版本切换的动态接口。
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);
}
}
测试说明
-
获取当前活跃版本:
- 请求
GET /version/active。 - 返回当前正在使用的版本信息。
- 请求
-
切换版本:
- 请求
POST /version/switch?versionId=1.1.0。 - 系统切换到指定版本,返回切换结果。
- 请求
-
获取所有版本信息:
- 请求
GET /version/all。 - 返回系统中的所有版本信息。
- 请求
注意事项
-
蓝绿发布实现:
- 配合 Nginx 的负载均衡策略,可以将流量切换到新版本服务,同时保留旧版本服务作为备份。
-
灰度发布实现:
- 可以结合用户 ID 或其他特性,逐步将部分用户的流量路由到新版本服务。
-
回滚机制:
- 如果新版本出现问题,可以通过接口快速切换回旧版本,确保系统稳定。
-
分布式锁:
- 确保版本切换操作的唯一性,避免多实例同时操作导致数据不一致。
6. 基于消息队列的异步预热
具体场景:秒杀活动库存更新
- 背景: 在秒杀活动中,商品库存信息需要实时更新到缓存中,而库存变更频率高,不能全部通过直接数据库查询完成。
- 应用: 当秒杀活动库存发生变化时,通过消息队列通知缓存服务更新缓存,确保用户查询时可以获取最新的库存数据。
- 案例:
- 秒杀系统:当用户下单或取消订单时,库存变化通过 Kafka 消息队列触发缓存更新。
秒杀活动是一种高并发场景,它的主要挑战在于如何合理地处理库存的扣减操作,同时确保系统的高性能和数据准确性。以下是秒杀场景中常见的功能需求和解决方案:
-
场景需求:
- 商品库存有限,例如某商品的库存为
100,用户需要在秒杀活动中抢购。 - 支持高并发访问,避免超卖(即库存为负)。
- 保证库存的一致性,避免多线程或多实例扣减库存时出现冲突。
- 商品库存有限,例如某商品的库存为
-
挑战点:
- 高并发:秒杀活动通常伴随着大量用户瞬时访问,需避免数据库的性能瓶颈。
- 超卖问题:需要严格控制库存,确保不会出现售出的商品数量超过实际库存的情况。
- 分布式场景:在多实例、多节点的分布式环境下,需要解决并发操作的问题。
-
优化方案:
- 缓存预扣减:通过 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);
}
}
测试流程
-
初始化库存:
- 请求
POST /seckill/init,传入商品 ID 和库存数量,例如:curl -X POST "http://localhost:8080/seckill/init?productId=1&stock=100" - 返回结果:
库存初始化成功。
- 请求
-
秒杀商品:
- 请求
POST /seckill/buy,传入商品 ID,例如:curl -X POST "http://localhost:8080/seckill/buy?productId=1" - 库存减少,返回结果:
恭喜您,秒杀成功!。
- 请求
-
库存不足:
- 当库存为 0 时,返回结果:
秒杀已结束。
- 当库存为 0 时,返回结果:
注意事项
-
超卖问题:
- 使用 Redis 的分布式锁和原子操作(如
decrement),可以有效避免超卖问题。
- 使用 Redis 的分布式锁和原子操作(如
-
高并发优化:
- 可将用户请求写入消息队列(如 RabbitMQ、Kafka),异步扣减库存,提高系统吞吐量。
-
库存回滚:
- 如果订单未支付或超时取消,需要实现库存的回滚逻辑。
-
限流保护:
- 为了防止恶意请求,可通过限流机制(如令牌桶算法)限制每秒请求数。
通过以上代码,结合 Redis 和分布式锁,可以实现一个高效、安全的秒杀活动库存更新逻辑,适用于多线程和分布式场景。
7. 冷热分离策略
具体场景:视频网站的热门视频
- 背景: 视频平台中的内容分为热门视频(高播放量)和冷门视频(低播放量),为了节省缓存空间,仅对热门视频进行缓存。
- 应用: 使用数据分析工具(如大数据日志分析)统计视频的播放量,根据阈值识别热门视频并加载到缓存中。
- 案例:
- 爱奇艺、腾讯视频等平台,将高播放量的热播剧、电影缓存到 CDN 节点或 Redis,低播放量视频不缓存。
视频网站的“热门视频”功能是一个典型的高并发、高实时性场景。其核心需求是在用户大量访问的情况下,实时统计视频的热度(如播放次数、点赞数等),并快速返回热门视频排行。
核心需求
-
视频热度的实时统计:
- 统计视频的播放次数、点赞次数等,作为热度的评估指标。
- 热度计算需要支持高并发读写操作。
-
热门视频排行榜:
- 根据视频热度动态生成排名。
- 支持按时间段(如日榜、周榜、月榜)统计。
-
高性能要求:
- 网站用户量大,需要保证高并发情况下的性能。
- 避免直接操作数据库,推荐使用缓存(如 Redis)来存储热度信息。
-
扩展性:
- 热度计算逻辑可以根据需求调整,例如权重(播放次数占比、点赞占比等)。
- 支持分布式部署。
技术选型
-
Redis:
- 使用 Redis 的数据结构(如
SortedSet)存储视频热度和排行榜,实现高效的排名功能。 - 使用 Redis 的原子操作(如
INCR)统计播放次数、点赞次数等。
- 使用 Redis 的数据结构(如
-
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);
}
}
测试流程
-
模拟播放增加:
- 请求
POST /hot-videos/play/{videoId}。 - 例如:
curl -X POST "http://localhost:8080/hot-videos/play/1" - 返回结果:
播放次数增加成功。
- 请求
-
模拟点赞增加:
- 请求
POST /hot-videos/like/{videoId}。 - 例如:
curl -X POST "http://localhost:8080/hot-videos/like/1" - 返回结果:
点赞次数增加成功。
- 请求
-
获取热门视频排行榜:
- 请求
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 + 点赞次数 * 2。 - 调整
incrementPlayCount和incrementLikeCount方法中的分值即可实现。
- 热度计算可以结合播放次数和点赞次数的权重,例如:
-
时间范围排行榜:
- 使用 Redis 的
ZSet有效时间设置,生成按日、周、月的排行榜。例如:- 使用不同的
key表示时间段:HOT_VIDEOS_DAY、HOT_VIDEOS_WEEK。 - 定时任务重置对应时间段的排行榜。
- 使用不同的
- 使用 Redis 的
-
限流保护:
- 对播放和点赞接口添加限流策略,防止恶意刷热度。
-
视频详情结合:
- 在获取排行榜时,结合数据库查询视频的详情信息(如标题、封面)返回。
通过 Redis 的 SortedSet 数据结构,可以高效地实现视频网站的热门视频功能。该方案性能优异,适合高并发场景,同时支持灵活扩展(如权重调整、时间段排名)。结合 Spring Boot 提供的接口服务,能够快速构建热门视频功能。
8. 与 CDN 结合的缓存预热
具体场景:大型静态资源(如图片、视频、CSS 文件)
- 背景: 网站上线新版本时,包含大量静态资源(如 JS、CSS 文件、图片等),这些资源需要分发到全球各地的 CDN 节点,确保用户快速加载。
- 应用: 在上线前调用 CDN 服务商的预热 API,将指定的静态资源推送到 CDN 节点缓存中,用户访问时直接从 CDN 获取数据。
- 案例:
- 阿里云、腾讯云 CDN 的缓存预热功能,适用于电商、门户网站等需要大规模分发静态资源的场景。
对于大型静态资源(如图片、视频、CSS 文件)的管理与分发,是现代互联网项目中非常重要的一环。主要需求包括:
-
静态资源的高效存储与分发:
- 图片、视频、CSS 等静态资源需要存储在高效的存储服务中。
- 提供快速分发的能力,尤其在高并发访问时,保证性能和稳定性。
-
内容优化:
- 大型文件需要通过压缩、切片等方式优化网络传输。
- 支持分块下载、断点续传等。
-
权限控制:
- 部分静态资源(如视频、图片)可能需要基于用户权限进行访问控制。
-
缓存机制:
- 静态资源通常通过 HTTP 缓存和 CDN(内容分发网络)加速访问,减少服务器压力。
实现方案与技术选型
-
存储服务:
- 使用云存储服务(如阿里云 OSS、腾讯云 COS、亚马逊 S3)存储静态资源。
- 如果是本地存储,可以选择 Nginx 作为静态资源服务器。
-
分发优化:
- 配置 CDN(如阿里云 CDN、Cloudflare)加速资源分发。
- 实现分块下载、断点续传等功能。
-
传输优化:
- 对图片进行压缩(如 WebP 格式)、对视频进行转码(如 H.265 编码)。
- 使用 GZIP 或 Brotli 压缩传输 CSS、JS 等文件。
-
权限控制:
- 通过生成签名 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
测试流程
-
上传文件到本地存储:
- 请求
POST /resources/local/upload。 - 使用
curl测试:curl -F "file=@/path/to/file.jpg" http://localhost:8080/resources/local/upload - 返回文件保存路径。
- 请求
-
下载文件(本地存储):
- 请求
GET /resources/local/download/{fileName}。 - 返回文件数据。
- 请求
-
上传文件到阿里云 OSS:
- 请求
POST /resources/aliyun/upload。 - 返回 OSS 文件访问路径。
- 请求
-
获取阿里云 OSS 签名 URL:
- 请求
GET /resources/aliyun/presigned/{fileName}?expirationInSeconds=3600。 - 返回带签名的 URL,可用于临时访问。
- 请求
扩展与优化
-
CDN 配置:
- 将阿里云 OSS 或本地存储文件配置到 CDN,提高静态资源分发效率。
-
断点续传与分块上传:
- 对于大文件(如视频),可以使用阿里云 OSS 的分块上传功能。
-
缓存优化:
- 配置 HTTP 缓存头(如
Cache-Control、ETag)支持客户端缓存。
- 配置 HTTP 缓存头(如
-
安全性:
- 使用签名 URL 或 JWT Token 验证用户权限,确保资源访问安全。
9. 基于 AI 的智能预热
具体场景:内容推荐系统
- 背景: 内容推荐平台(如新闻、视频、社交应用)需要提前加载用户可能感兴趣的内容到缓存中,提升推荐系统的响应速度。
- 应用: 通过机器学习算法预测用户的兴趣内容,根据预测结果将热点内容提前加载到缓存中。
- 案例:
- 今日头条、抖音等平台,根据用户历史行为和推荐算法,提前缓存可能点击的内容。
推荐系统是互联网应用中的核心功能之一,广泛用于电商、视频平台、新闻门户等领域。推荐系统的目标是根据用户的兴趣或行为,为其提供个性化的内容推荐。以下我们以视频网站为例,设计一个简单的内容推荐系统。
核心需求
-
用户行为收集:
- 收集用户的浏览行为(如观看、点赞、评论、收藏等),作为推荐的基础数据。
-
推荐算法:
- 实现协同过滤算法(基于用户行为或内容相似性)或热门推荐算法。
- 提供基于简单规则的个性化内容推荐。
-
实时性:
- 热门内容推荐需要实时更新,例如根据播放次数或点赞数排序。
- 个性化推荐需要根据用户最近的行为动态调整。
-
扩展性:
- 系统应支持算法扩展,如引入深度学习模型或增强推荐策略。
技术选型
-
存储:
- 使用 MySQL 或 MongoDB 存储用户行为数据。
- 使用 Redis 缓存实时热门内容,以提高推荐性能。
-
推荐算法:
- 基于协同过滤(Collaborative Filtering)的简单实现。
- 基于热门内容的推荐(如 Redis 的
SortedSet实现)。
-
技术框架:
- 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
测试流程
-
收集用户行为:
- 请求
POST /behavior,记录用户行为(如观看、点赞)。 - 示例请求:
curl -X POST -H "Content-Type: application/json" -d '{"userId":1,"contentId":100,"actionType":"watch"}' http://localhost:8080/behavior
- 请求
-
热门推荐测试:
- 请求
GET /recommend/hot,获取热门内容。 - 示例请求:
curl "http://localhost:8080/recommend/hot?topN=5"
- 请求
-
个性化推荐测试:
- 请求
GET /recommend/personalized?userId=1,获取用户个性化推荐。 - 示例请求:
curl "http://localhost:8080/recommend/personalized?userId=1"
- 请求
扩展与优化
-
引入更复杂的推荐算法:
- 使用基于矩阵分解(Matrix Factorization)或深度学习的推荐算法(如 TensorFlow 或 PyTorch)。
-
支持实时推荐:
- 实时更新用户行为,结合 Kafka 等消息队列实现流式推荐。
-
缓存优化:
- 使用 Redis 缓存推荐结果,减少重复计算。
-
可解释性:
- 提供推荐理由,例如 "因为你喜欢某视频,推荐了类似内容"。
总结
| 方案 | 场景案例 |
|---|---|
| 启动过程中预热 | 电商首页商品推荐、新闻门户网站首页热点数据 |
| 定时任务预热 | 每日热点新闻、定时更新电商商品价格或库存 |
| 用时加载(惰性加载) | 冷门商品详情页、后台管理系统中的非频繁操作 |
| 缓存加载器 | 用户信息加载(如登录时自动加载用户资料) |
| 手动触发预热 | 大促活动上线(如双十一、618)、版本升级后需要的缓存加载 |
| 基于消息队列的异步预热 | 秒杀系统库存更新、订单状态变化通知 |
| 冷热分离策略 | 视频平台的热播剧缓存、流量分层热点内容(如电商爆款商品) |
| 与 CDN 结合的缓存预热 | 网站的大量静态资源(如 JS、CSS 文件、图片) |
| 基于 AI 的智能预热 | 新闻推荐、视频推荐系统的情景预测 |
选择合适的缓存预热方案需要结合业务特点、数据访问规律以及技术实现难度,灵活调整策略以达到最佳性能。