一、RedisUtil封装
java
package com.qj.redis.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Redis工具类
* 提供Redis常见的操作封装,简化Redis使用
* 使用Spring的RedisTemplate进行底层操作
*/
@Component
@Slf4j
public class RedisUtil {
// 缓存key的分隔符
private static final String CACHE_KEY_SEPARATOR = ".";
// Redis操作模板,由Spring注入
@Resource
private RedisTemplate redisTemplate;
/**
* 构建缓存key
* 将多个字符串参数用分隔符连接起来形成统一的key格式
*
* @param strObjs key的组成部分,可变参数
* @return 拼接后的完整key
*/
public String buildKey(String... strObjs) {
return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR));
}
/**
* 判断key是否存在
*
* @param key 要检查的key
* @return true-存在,false-不存在
*/
public boolean exist(String key) {
return redisTemplate.hasKey(key);
}
/**
* 删除指定的key
*
* @param key 要删除的key
* @return true-删除成功,false-删除失败
*/
public boolean del(String key) {
return redisTemplate.delete(key);
}
/**
* 设置键值对
*
* @param key 键
* @param value 值
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 设置键值对(仅当key不存在时)
*
* @param key 键
* @param value 值
* @param time 过期时间
* @param timeUnit 时间单位
* @return true-设置成功,false-设置失败(key已存在)
*/
public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {
return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
}
/**
* 获取指定key的值
*
* @param key 键
* @return key对应的值,如果key不存在则返回null
*/
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 向有序集合中添加元素
*
* @param key 集合key
* @param value 元素值
* @param score 分数(用于排序)
* @return true-添加成功,false-添加失败
*/
public Boolean zAdd(String key, String value, Long score) {
return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score)));
}
/**
* 获取有序集合的元素数量
*
* @param key 集合key
* @return 集合中的元素数量
*/
public Long countZset(String key) {
return redisTemplate.opsForZSet().size(key);
}
/**
* 获取有序集合中指定范围的元素
*
* @param key 集合key
* @param start 开始索引(从0开始)
* @param end 结束索引(-1表示到最后)
* @return 元素集合
*/
public Set<String> rangeZset(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* 从有序集合中移除指定元素
*
* @param key 集合key
* @param value 要移除的元素值
* @return 移除的元素数量
*/
public Long removeZset(String key, Object value) {
return redisTemplate.opsForZSet().remove(key, value);
}
/**
* 从有序集合中移除多个元素
*
* @param key 集合key
* @param value 要移除的元素集合
*/
public void removeZsetList(String key, Set<String> value) {
value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val));
}
/**
* 获取有序集合中指定元素的分数
*
* @param key 集合key
* @param value 元素值
* @return 元素的分数
*/
public Double score(String key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 获取有序集合中指定分数范围的元素
*
* @param key 集合key
* @param start 开始分数
* @param end 结束分数
* @return 元素集合
*/
public Set<String> rangeByScore(String key, long start, long end) {
return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end)));
}
/**
* 增加有序集合中指定元素的分数
*
* @param key 集合key
* @param obj 元素值
* @param score 要增加的分数
* @return 增加后的新分数
*/
public Object addScore(String key, Object obj, double score) {
return redisTemplate.opsForZSet().incrementScore(key, obj, score);
}
/**
* 获取有序集合中指定元素的排名(从小到大排序,排名从0开始)
*
* @param key 集合key
* @param obj 元素值
* @return 元素的排名,如果元素不存在返回null
*/
public Object rank(String key, Object obj) {
return redisTemplate.opsForZSet().rank(key, obj);
}
}
二、Redis实现自动预热缓存
**说明:**当项目启动的时候,预热一部分的缓存的场景,要创建在项目启动时就加载缓存的模块!
1. 定义缓存的抽象类AbstractCache
java
package com.qj.redis.init;
import org.springframework.stereotype.Component;
/**
* 缓存抽象基类
* 定义缓存操作的基本骨架,提供缓存初始化、获取、清理和重载的通用接口
* 使用Spring的@Component注解,便于被子类继承并纳入Spring容器管理
*/
@Component
public abstract class AbstractCache {
/**
* 初始化缓存
* 抽象方法,需要子类具体实现缓存初始化逻辑
* 例如:从数据库加载数据到缓存中
*/
public abstract void initCache();
/**
* 获取缓存数据
* 抽象方法,需要子类具体实现缓存获取逻辑
* @param <T> 缓存数据类型泛型
* @return 返回指定类型的缓存数据
*/
public abstract <T> T getCache();
/**
* 清理缓存
* 抽象方法,需要子类具体实现缓存清理逻辑
* 例如:删除Redis中的相关缓存键
*/
public abstract void clearCache();
/**
* 重新加载缓存
* 默认实现:先清理现有缓存,然后重新初始化缓存
* 提供了缓存刷新的标准流程,子类可按需重写
*/
public void reloadCache() {
clearCache();
initCache();
}
}
2. 实现需要进行缓存的类
(1)SysUserCache
java
package com.qj.sys.cache;
import com.qj.redis.init.AbstractCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* 系统用户缓存实现类
* 继承AbstractCache抽象类,提供系统用户相关的缓存操作具体实现
* 使用Redis作为缓存存储介质,通过RedisTemplate进行操作
*/
@Component // 声明为Spring组件,由Spring容器管理
public class SysUserCache extends AbstractCache {
// 定义系统用户缓存在Redis中的键名
private static final String SYS_USER_CACHE_KEY = "SYS_USER";
// 注入Redis操作模板
@Autowired
private RedisTemplate redisTemplate;
/**
* 初始化缓存
* 实现抽象方法,将系统用户数据加载到Redis缓存中
* 此处为示例,实际应用中可能需要从数据库查询数据后存入缓存
*/
@Override
public void initCache() {
// 实际项目中,这里应该与数据库或其他数据源联动
// 示例:将用户数据存入Redis,使用字符串类型存储
redisTemplate.opsForValue().set(SYS_USER_CACHE_KEY, "qj1");
}
/**
* 获取缓存数据
* 实现抽象方法,从Redis中获取系统用户缓存数据
* 如果缓存不存在,会自动重新加载缓存
* @param <T> 返回数据类型泛型
* @return 返回系统用户缓存数据
*/
@Override
public <T> T getCache() {
// 检查缓存键是否存在,如果不存在则重新加载缓存
if (!redisTemplate.hasKey(SYS_USER_CACHE_KEY).booleanValue()) {
reloadCache(); // 调用父类的重新加载缓存方法
}
// 从Redis中获取缓存数据并返回
return (T) redisTemplate.opsForValue().get(SYS_USER_CACHE_KEY);
}
/**
* 清理缓存
* 实现抽象方法,删除Redis中的系统用户缓存
*/
@Override
public void clearCache() {
// 从Redis中删除系统用户缓存键
redisTemplate.delete(SYS_USER_CACHE_KEY);
}
}
(2)UserCache
java
package com.qj.sys.cache;
import com.qj.redis.init.AbstractCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* 用户缓存实现类
* 继承AbstractCache抽象类,提供用户相关的缓存操作具体实现
* 使用Redis作为缓存存储介质,通过RedisTemplate进行操作
* 与SysUserCache类似但独立,可根据业务需求区分不同用户缓存
*/
@Component // 声明为Spring组件,由Spring容器管理
public class UserCache extends AbstractCache {
// 定义用户缓存在Redis中的键名
// 注意:此键名与SysUserCache中的键名不同,避免缓存键冲突
private static final String USER_CACHE_KEY = "USER";
// 注入Redis操作模板,用于执行Redis命令
@Autowired
private RedisTemplate redisTemplate;
/**
* 初始化缓存
* 实现抽象方法,将用户数据加载到Redis缓存中
* 此处为示例,实际应用中需要从数据库或其他数据源获取真实数据
*/
@Override
public void initCache() {
// 实际项目中,这里应该与数据库或其他数据源联动
// 示例:将用户数据存入Redis,使用字符串类型存储
// 注意:此处仅为演示,实际应存储真实用户数据
redisTemplate.opsForValue().set(USER_CACHE_KEY, "qj2");
}
/**
* 获取缓存数据
* 实现抽象方法,从Redis中获取用户缓存数据
* 采用懒加载模式:如果缓存不存在,会自动重新加载缓存
* @param <T> 返回数据类型泛型
* @return 返回用户缓存数据,实际类型根据调用上下文确定
*/
@Override
public <T> T getCache() {
// 检查缓存键是否存在,如果不存在则重新加载缓存
// 这种设计确保调用getCache时总能获取到数据(即使缓存意外失效)
if (!redisTemplate.hasKey(USER_CACHE_KEY).booleanValue()) {
reloadCache(); // 调用父类的重新加载缓存方法
}
// 从Redis中获取缓存数据并返回,进行类型转换
return (T) redisTemplate.opsForValue().get(USER_CACHE_KEY);
}
/**
* 清理缓存
* 实现抽象方法,删除Redis中的用户缓存
* 通常在数据更新后调用,确保缓存与数据源的一致性
*/
@Override
public void clearCache() {
// 从Redis中删除用户缓存键
redisTemplate.delete(USER_CACHE_KEY);
}
}
3. 定义类来获取ApplicationContext
**说明:**当一个类实现了ApplicationContextAware接口之后,这个类就可以方便的获得ApplicationContext对象(Spring上下文),Spring发现某个Bean实现了ApplicationContextAware接口,Spring容器会在创建该Bean之后,自动调用该Bean的setApplicationContext(参数)方法,调用该方法时,会将容器本身ApplicationContext对象作为参数传递给该方法。
java
package com.qj.redis.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Spring上下文工具类
* 实现ApplicationContextAware接口,用于获取Spring应用上下文
* 提供静态方法方便在非Spring管理的类中获取Spring容器中的Bean
*/
@Component // 声明为Spring组件,由Spring容器管理
public class SpringContextUtil implements ApplicationContextAware {
// 静态变量保存Spring应用上下文
private static ApplicationContext applicationCtxt;
/**
* 获取Spring应用上下文
* @return 返回当前Spring应用上下文实例
*/
public static ApplicationContext getApplicationContext() {
return applicationCtxt;
}
/**
* 设置Spring应用上下文
* 实现ApplicationContextAware接口的方法,Spring容器启动时自动调用
* @param applicationContext Spring应用上下文
* @throws BeansException 如果设置过程中出现异常
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 将传入的应用上下文赋值给静态变量,使其可在静态方法中使用
applicationCtxt = applicationContext;
}
/**
* 根据类型获取Spring容器中的Bean实例
* @param type Bean的Class类型
* @param <T> Bean类型泛型
* @return 返回指定类型的Bean实例
*/
public static <T> T getBean(Class<T> type) {
return applicationCtxt.getBean(type);
}
}
4. 启动并初始化缓存InitCache
**说明:**在使用SpringBoot构建项目时,我们通常有一些预先数据的加载。那么SpringBoot提供了CommandLineRunner方式来实现,CommandLineRunner是一个接口,我们需要时,只需实现该接口就行(如果存在多个加载的数据,我们也可以使用@Order注解来排序)
java
package com.qj.redis.init;
import com.qj.redis.util.SpringContextUtil;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.util.Map;
import java.util.Map.Entry;
/**
* 缓存初始化启动器
* 实现CommandLineRunner接口,在Spring Boot应用启动后自动执行缓存预热
* 通过@ConditionalOnProperty条件注解控制是否启用缓存预热功能
*/
@Component // 声明为Spring组件,由Spring容器管理
@ConditionalOnProperty(name = "init.cache.enable", havingValue = "true") // 条件注解:只有当配置文件中init.cache.enable=true时才启用该类
public class InitCache implements CommandLineRunner {
/**
* 项目启动时自动执行的方法
* 实现CommandLineRunner接口的run方法,Spring Boot启动完成后会自动调用
* @param args 命令行参数
* @throws Exception 可能抛出的异常
*/
@Override
public void run(String... args) throws Exception {
// 获取所有需要预热的缓存实例
ApplicationContext applicationContext = SpringContextUtil.getApplicationContext();
// 从Spring容器中获取所有AbstractCache类型的Bean
// 这些Bean都是具体的缓存实现类(如SysUserCache、UserCache等)
Map<String, AbstractCache> beanMap = applicationContext.getBeansOfType(AbstractCache.class);
// 如果存在缓存Bean,则遍历并调用它们的初始化方法
if (!beanMap.isEmpty()) {
for (Entry<String, AbstractCache> entry : beanMap.entrySet()) {
// 获取AbstractCache的具体实现类实例
// 这里通过SpringContextUtil再次获取Bean是为了确保获取的是Spring管理的代理对象
// 这样可以保证AOP等Spring特性正常工作
AbstractCache abstractCache = (AbstractCache) SpringContextUtil.getBean(entry.getValue().getClass());
// 调用缓存初始化方法,将数据加载到缓存中
abstractCache.initCache();
}
}
// 输出缓存预热完成日志
System.out.println("缓存预热成功...");
}
}
5. 配置文件开启
java
init:
cahce:
enable: true

6. 启动并测试
**说明:**可以看到在项目启动时,控制台顺利输出"缓存成功...",说明项目成功运行!

**注意:**查看Redis集群也正常看到已被缓存的两个Key的数据!

三、分布式锁的实现
**说明:**此处不使用Redission来直接实现,完全手动实现一个抢占式的分布式锁
使用场景:
(1)任务调度(集群环境下,一个服务的多个实例的任务不想同一时间都进行执行)
(2)并发修改相关(操作同一个数据)
1. 依赖文件pom.xml
**说明:**添加commons-lang包,用来做字符串的校验!
Apache Commons Lang 是一个提供了许多Java语言核心类扩展功能的工具库,主要包括:
字符串处理:增强的字符串操作方法
数值处理:数字和数值类型的工具类
对象操作:对象比较、哈希码生成、toString方法等
异常处理:异常链和嵌套异常处理
系统属性:Java系统属性访问工具
随机数生成:更强大的随机数生成器
日期时间处理:日期和时间操作工具
java
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
2. 运行时异常类ShareLockException
java
/**
* 共享锁异常类
* 自定义运行时异常,用于处理共享锁操作中的异常情况
* 继承自RuntimeException,属于非受检异常(unchecked exception)
*
* 使用场景:
* - 当共享锁获取失败时抛出
* - 当共享锁操作超时时抛出
* - 当共享锁状态异常时抛出
*/
public class ShareLockException extends RuntimeException {
/**
* 构造函数
* @param message 异常详细信息,用于说明异常原因和上下文
*/
public ShareLockException(String message) {
// 调用父类RuntimeException的构造函数,传入异常消息
super(message);
}
}
3. RedisUtil中添加相关方法
java
package com.qj.redis.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* Redis操作工具类
* 封装常用的Redis操作方法,提供简洁的API供业务代码调用
* 使用Spring的RedisTemplate进行底层操作
*/
@Component // 声明为Spring组件,由Spring容器管理
public class RedisUtil {
// 注入Redis操作模板
@Autowired
private RedisTemplate redisTemplate;
// 这里可能有其他方法,用...表示
/**
* 设置键值对(仅当键不存在时)
* 原子操作,常用于分布式锁的实现
*
* @param key 缓存的键
* @param value 缓存的值
* @param time 过期时间
* @param timeUnit 时间单位
* @return 设置成功返回true,键已存在返回false
*/
public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) {
return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
}
/**
* 根据键获取缓存值
*
* @param key 缓存的键
* @return 对应的值,如果键不存在返回null
*/
public String get(String key) {
String value = (String) redisTemplate.opsForValue().get(key);
return value;
}
/**
* 删除指定的缓存键
*
* @param key 要删除的缓存键
* @return 删除成功返回true,键不存在返回false
*/
public boolean del(String key) {
return redisTemplate.delete(key);
}
}
4. 实现: Redis分布式抢占锁RedisShareLockUtil
java
package com.qj.redis.util;
import com.qj.redis.exception.ShareLockException;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* Redis分布式锁工具类
* 基于Redis实现的分布式锁,支持加锁、解锁和尝试加锁操作
* 使用请求标识(requestId)确保只有加锁者才能解锁,防止误删其他客户端的锁
*/
@Component // 声明为Spring组件,由Spring容器管理
public class RedisShareLockUtil {
// 注入Redis工具类
@Resource
private RedisUtil redisUtil;
// 加锁超时时间(毫秒),防止无限等待
private Long TIME_OUT = 1000L;
/**
* 加锁方法(阻塞式)
* 在指定时间内尝试获取分布式锁,如果获取不到会进行重试
*
* @param lockKey 锁的键名
* @param requestId 请求标识(通常使用UUID),用于确保只有加锁者才能解锁
* @param time 锁的持有时间(毫秒)
* @return 加锁成功返回true,否则返回false
* @throws ShareLockException 当参数异常时抛出
*/
public boolean lock(String lockKey, String requestId, Long time) {
// 参数校验
if (StringUtils.isBlank(lockKey) || StringUtils.isBlank(requestId) || time <= 0) {
throw new ShareLockException("分布式锁-加锁参数异常");
}
long currentTime = System.currentTimeMillis();
long outTime = currentTime + TIME_OUT; // 计算超时时间点
Boolean result = false;
// 在超时时间内循环尝试获取锁
while (currentTime < outTime) {
// 尝试获取锁
result = redisUtil.setNx(lockKey, requestId, time, TimeUnit.MILLISECONDS);
if (result) {
// 获取锁成功,返回true
return result;
}
// 获取锁失败,休眠100毫秒后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新当前时间
currentTime = System.currentTimeMillis();
}
// 超时后仍未获取到锁,返回false
return result;
}
/**
* 解锁方法
* 根据锁键和请求标识释放分布式锁,确保只有加锁者才能解锁
*
* @param key 锁的键名
* @param requestId 请求标识,必须与加锁时使用的标识一致
* @return 解锁成功返回true,否则返回false
* @throws ShareLockException 当参数异常时抛出
*/
public boolean unLock(String key, String requestId) {
// 参数校验
if (StringUtils.isBlank(key) || StringUtils.isBlank(requestId)) {
throw new ShareLockException("分布式锁-解锁参数异常");
}
try {
// 获取锁当前的值
String value = redisUtil.get(key);
// 检查请求标识是否匹配,防止误删其他客户端的锁
if (requestId.equals(value)) {
// 只有加锁者才能解锁
redisUtil.del(key);
return true;
}
} catch (Exception e) {
// 记录日志,这里应该使用日志框架记录异常信息
// 补日志
}
// 解锁失败
return false;
}
/**
* 尝试加锁方法(非阻塞式)
* 尝试获取分布式锁一次,无论成功与否都立即返回
*
* @param lockKey 锁的键名
* @param requestId 请求标识
* @param time 锁的持有时间(毫秒)
* @return 加锁成功返回true,否则返回false
* @throws ShareLockException 当参数异常时抛出
*/
public boolean tryLock(String lockKey, String requestId, Long time) {
// 参数校验
if (StringUtils.isBlank(lockKey) || StringUtils.isBlank(requestId) || time <= 0) {
throw new ShareLockException("分布式锁-尝试加锁参数异常");
}
// 尝试获取锁一次
return redisUtil.setNx(lockKey, requestId, time, TimeUnit.MILLISECONDS);
}
}
5. 测试: 调用分布式锁
java
package com.qj.sys.controller;
import com.qj.redis.util.RedisShareLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequestMapping("test")
public class TestController {
@Autowired
private RedisShareLockUtil redisShareLockUtil;
@GetMapping("/testRedisShareLock")
public String testRedisShareLock() {
boolean result = redisShareLockUtil.lock("qj", "123456", 10000L);
log.info("分布式锁获取:{}", result);
return String.valueOf(result);
}
}
控制台返回: (独占锁)

四、Redis实现延迟队列
**说明:**我们有一个任务的情况下,我们会期望这个任务在某个时间点去执行,那么就要使用延迟队列。一般延迟队列可以使用RabbitMQ或者RocketMQ来进行实现,另外一种常用的方式就是使用Redis来进行实现了!
**实现方案:**基于redis来进行实现,我们主要使用的是zset这个数据类型天生的具有score的特性。zset可以根据score放入,而且可以通过range进行排序获取,以及删除指定的值。从业务上,我们可以再新增任务的时候放入,再通过定时任务进行拉取,要注意的一点就是拉取的时候要有分布式锁,不要进行重复拉取,或者交由分布式任务调度来处理拉取,都是可以的。
**使用场景:**我们更加偏向于 定时群发,定时取消 等。就举一个发博客的例子吧,博客我们可以选择定时发布,那么就可以应用redis的延迟队列来进行实现。要注意的一个点就是小心大key的产生,要做好延迟队列的key的隔离。
1. 延迟任务的实体类
java
package com.qj.sys.delayQueue;
import lombok.Data;
import java.util.Date;
@Data
public class MassMailTask {
// 相关任务ID
private Long taskId;
// 延迟任务的开始时间
private Date startTime;
}
2. 任务对延迟队列的推送方法和拉取的方法
实现思路:
**入队:**入队消息体一定要有时间的概念,把时间转换为毫秒,来作为我们zset的score;底层就是zset的add方法,由key,value以及score来组成。
**出队:**出队要基于rangeByScore来进行实现,指定我们的score的区间,也就是我们要拉取哪些的任务,拉取成功之后,我们先去执行业务逻辑,执行成功之后,我们再将其从消息队列进行删除。
java
package com.qj.sys.delayQueue;
import com.alibaba.fastjson.JSON;
import com.qj.redis.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 群发邮件任务延时队列服务
* 使用Redis的ZSet(有序集合)实现延时队列功能
* 用于处理定时群发邮件任务的调度
*/
@Service // 声明为Spring服务组件
@Slf4j // 使用Lombok的日志注解
public class MassMailTaskService {
// Redis中ZSet结构的key,用于存储群发邮件任务
private static final String MASS_MAIL_TASK_KEY = "massMailTask";
// 注入Redis工具类
@Resource
private RedisUtil redisUtil;
/**
* 将群发邮件任务推送到延时队列
* 使用Redis的ZSet结构,以任务开始时间作为分数(score)
*
* @param massMailTask 群发邮件任务对象
*/
public void pushMassMailTaskQueue(MassMailTask massMailTask) {
// 获取任务开始时间
Date startTime = massMailTask.getStartTime();
// 校验开始时间是否有效
if (startTime == null) {
return;
}
// 如果开始时间早于或等于当前时间,则不加入队列
if (startTime.compareTo(new Date()) <= 0) {
return;
}
// 记录日志
log.info("定时群发任务加入延时队列,massMailTask:{}", JSON.toJSON(massMailTask));
// 使用Redis的ZSet数据结构存储任务
// key: MASS_MAIL_TASK_KEY
// value: 任务ID的字符串形式
// score: 任务开始时间的时间戳
redisUtil.zAdd(MASS_MAIL_TASK_KEY, massMailTask.getTaskId().toString(), startTime.getTime());
}
/**
* 从延时队列中拉取到期的群发邮件任务
* 获取分数(时间戳)在0到当前时间之间的所有任务
* 获取后会从队列中移除这些任务
*
* @return 返回到期任务的ID集合
*/
public Set<Long> poolMassMailTaskQueue() {
// 获取当前时间之前的所有任务
// 参数说明:key, minScore(0), maxScore(当前时间戳)
Set<String> taskIdSet = redisUtil.rangeByScore(MASS_MAIL_TASK_KEY, 0, System.currentTimeMillis());
// 记录日志
log.info("获取延迟群发任务,taskIdSet:{}", JSON.toJSON(taskIdSet));
// 如果任务集合为空,返回空集合
if (CollectionUtils.isEmpty(taskIdSet)) {
return Collections.emptySet();
}
// 从Redis中移除已获取的任务
redisUtil.removeZsetList(MASS_MAIL_TASK_KEY, taskIdSet);
// 将任务ID字符串集合转换为Long类型集合
return taskIdSet.stream().map(n -> Long.parseLong(n)).collect(Collectors.toSet());
}
}
3. 编写测试类
**注意:**由于可能是分布式服务,所以可能是定时循环拉取,在拉取的时候不能所有服务的拉取都去拉,而是只允许一个任务去拉取,要么使用xxljob来实现,要么就选择分布式锁,只允许一个服务能够拉取并执行!
java
package com.qj.sys;
import com.alibaba.fastjson.JSON;
import com.qj.redis.util.RedisShareLockUtil;
import com.qj.sys.delayQueue.MassMailTask;
import com.qj.sys.delayQueue.MassMailTaskService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Set;
import java.util.UUID;
@SpringBootTest(classes = {SysApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Slf4j
public class MassMailTaskTest {
// 注入MassMailTaskService,用于操作延时任务队列
@Resource
private MassMailTaskService massMailTaskService;
// 注入RedisShareLockUtil,用于分布式锁的操作
@Resource
private RedisShareLockUtil redisShareLockUtil;
/**
* 测试方法:推送邮件任务到延时队列
*/
@Test
public void push() throws Exception {
// 创建一个SimpleDateFormat对象,用于将时间字符串转换为Date对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 创建一个MassMailTask对象,表示一个邮件任务
MassMailTask massMailTask = new MassMailTask();
massMailTask.setTaskId(1L); // 设置任务ID
massMailTask.setStartTime(simpleDateFormat.parse("2023-01-08 23:59:00")); // 设置任务的开始时间
// 将任务插入到延时队列中
massMailTaskService.pushMassMailTaskQueue(massMailTask);
// 输出日志,确认任务已插入
log.info("定时任务已插入!");
}
/**
* 测试方法:处理延时任务队列中的任务
*/
@Test
public void deal() {
// 定义锁的key值,用于Redis分布式锁
String lockKey = "test.delay.task";
// 创建一个唯一的请求ID,用于标识当前请求
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取分布式锁,锁定5秒钟
boolean locked = redisShareLockUtil.lock(lockKey, requestId, 5L);
// 如果获取锁失败,则直接返回
if (!locked) {
return;
}
// 从延时队列中获取一批任务ID
Set<Long> taskIdSet = massMailTaskService.poolMassMailTaskQueue();
// 打印获取到的任务ID集合
log.info("DelayTaskTest.deal.taskIdSet:{}", JSON.toJSON(taskIdSet));
// 如果任务集合为空,则返回
if (CollectionUtils.isEmpty(taskIdSet)) {
return;
}
// 处理获取到的任务,可以在此处添加具体的业务逻辑
} catch (Exception e) {
// 处理异常并输出日志
log.error("延时任务拉取执行失败:{}", e.getMessage(), e);
} finally {
// 无论如何,释放分布式锁
redisShareLockUtil.unLock(lockKey, requestId);
}
}
}
4. 运行测试
**(1)第一步:**任务推送延迟队列

**(2)第二步:**定时任务未到时间,直接进行拉取

**(3)第三步:**定时任务到时间了,然后进行拉取
**说明:**可以看到,到了时间之后,就可以正常拉取到定时任务!

**(4)第四步:**在延迟队列中插入两个延迟任务
**说明:**可以看到,到了时间之后,就可以正常拉取到所有定时任务,也是为什么需要使用ZSet来存储的原因!

五、使用Redis的Lua脚本实现CAS
**说明:**使用Lua脚本实现 CAS(比较并交换)的过程!
1. RedisLua工具类
java
package com.qj.sys.redislua;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
/**
* Redis Lua脚本执行组件
* 封装了比较并设置(CAS)操作的Lua脚本执行逻辑
* 提供原子性的比较并设置功能,确保在分布式环境下的数据一致性
*/
@Component // 声明为Spring组件,由Spring容器管理
@Slf4j // Lombok日志注解
public class CompareAndSetLua {
// 注入Redis操作模板
@Resource
private RedisTemplate redisTemplate;
// Redis脚本对象,用于执行Lua脚本
private DefaultRedisScript<Boolean> casScript;
/**
* 初始化方法
* 在Bean创建后自动执行,加载Lua脚本并配置脚本执行器
*/
@PostConstruct // 在依赖注入完成后自动执行
public void init() {
// 创建Redis脚本执行器
casScript = new DefaultRedisScript<>();
// 设置脚本返回类型为Boolean
casScript.setResultType(Boolean.class);
// 从类路径加载Lua脚本文件
// 假设脚本文件位于resources目录下的compareAndSet.lua
casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));
}
/**
* 执行比较并设置操作
* 调用Lua脚本实现原子性的比较并设置功能
*
* @param key Redis键名
* @param oldValue 期望的旧值
* @param newValue 要设置的新值
* @return 操作成功返回true,失败返回false
*/
public boolean compareAndSet(String key, Long oldValue, Long newValue) {
// 创建键列表,Lua脚本中通过KEYS[1]访问
ArrayList<String> keys = new ArrayList<>();
keys.add(key);
// 执行Lua脚本
// 参数说明:脚本对象、键列表、参数列表(旧值和新值)
Boolean result = (Boolean) redisTemplate.execute(casScript, keys, oldValue, newValue);
// 返回操作结果
return result;
}
}
2. 相关Lua脚本(compareAndSetLua.lua )
**注意:**在此key下,如果传入的oldValue和存在的值相同,则更新为newValue,否则不变!
java
-- Redis Lua脚本:原子性比较并设置(Compare and Set)操作
-- 实现类似Java中Atomic类的CAS(Compare and Swap)功能
-- 从键参数列表中获取第一个键名
local key = KEYS[1]
-- 从参数列表中获取期望的旧值和要设置的新值
local oldValue = ARGV[1]
local newValue = ARGV[2]
-- 从Redis中获取指定键的当前值
local redisValue = redis.call('get', key)
-- 判断条件:
-- 1. 如果键不存在(redisValue为false)
-- 2. 或者当前值等于期望的旧值(转换为数字比较)
if (redisValue == false or tonumber(redisValue) == tonumber(oldValue))
then
-- 条件满足,设置新值
redis.call('set', key, newValue)
-- 返回操作成功
return true
else
-- 条件不满足,当前值与期望的旧值不匹配
-- 返回操作失败
return false
end
3. 运行测试
java
package com.qj.sys;
import com.qj.sys.redislua.CompareAndSetLua;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
/**
* Redis Lua脚本测试类
* 用于测试基于Lua脚本实现的原子性比较并设置(CAS)操作
* 演示如何使用Lua脚本保证Redis操作的原子性
*/
@SpringBootTest(classes = {SysApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Spring Boot测试注解
@RunWith(SpringRunner.class) // 使用SpringRunner运行测试
@Slf4j // Lombok日志注解
public class RedisLuaTest {
// 注入Redis操作模板
@Resource
private RedisTemplate redisTemplate;
// 注入自定义的Lua脚本执行组件
@Resource
private CompareAndSetLua compareAndSetLua;
/**
* Redis Lua脚本测试方法
* 演示如何使用Lua脚本实现原子性的比较并设置操作
* 先设置一个初始值,然后使用Lua脚本进行原子性的比较和更新
*/
@Test
public void redisLuaTest() {
// 获取字符串值操作接口
ValueOperations<String, Long> opsForValue = redisTemplate.opsForValue();
// 在Redis中设置一个键值对,键为"qj",值为18
opsForValue.set("qj", 18L);
// 记录当前的值
log.info("qj的值为:{}", opsForValue.get("qj"));
// 使用Lua脚本执行比较并设置操作
// 参数说明:key, 期望的旧值, 要设置的新值
// 只有当当前值等于期望的旧值时,才会设置新值
boolean result = compareAndSetLua.compareAndSet("qj", 18L, 19L);
// 根据操作结果输出相应信息
if (result) {
log.info("修改成功!qj的值为:{}", opsForValue.get("qj"));
} else {
log.info("修改失败,当前值已不是期望的旧值");
}
}
}
运行效果:

六、Spring注解缓存实现
**说明1:**因为查询的时候,每次都走数据库会导致查询非常缓慢,所以Spring提供了一套缓存机制,在查询相同接口的时候会先查询缓存,再查询数据库,大大提高了接口响应速度!
说明2: Spring Boot会自动配置合适的CacheManager作为相关缓存的提供程序 (此处配置了Redis的CacheManager)**,**当你在配置类(@Configuration)上使用@EnableCaching注解时,会触发一个后处理器(post processor ),它检查每个Spring bean,查看是否已经存在注解对应的缓存;如果找到了,就会自动创建一个代理拦截方法调用,使用缓存的bean执行处理。
**注意:**在实际工作中基本不使用Spring注解缓存,因为无法为每个缓存单独设置过期时间(除非为每个缓存进行单独的配置),很可能导致整个业务产生缓存雪崩现象的出现!
1. 开启缓存
**说明:**需要在启动类上加上@EnableCaching注解!

2. 加上@Cacheable和@CacheEvict注解
**说明:**在业务接口上加上@Cacheable注解,并且为了保证数据一致性,需要配合@CacheEvict注解一起使用,用于在增删改的时候进行对缓存数据一致性的保障!
java
/**
* 通过主键查询单条数据
* 使用@Cacheable注解实现缓存功能,提高查询性能
* 当多次查询相同id的数据时,只有第一次会真正调用方法,后续直接从缓存中返回结果
*
* @param id 用户主键ID
* @return 包含用户数据的Result对象
*
* @Cacheable 注解说明:
* - cacheNames: 指定缓存名称,用于区分不同的缓存区域
* - key: 使用SpEL表达式生成缓存键,格式为'querySysUserById'+用户ID
* 例如:当id=123时,缓存键为"querySysUserById123"
*/
@GetMapping("/get/{id}")
@Cacheable(cacheNames = "SysUser", key = "'querySysUserById'+#id")
public Result<SysUserPo> queryById(@PathVariable("id") Long id) {
// 调用服务层方法查询用户数据
// 只有在缓存不存在时,才会执行此方法
return Result.ok(this.sysUserService.queryById(id));
}
/**
* 编辑用户数据
* 使用@CacheEvict注解清除缓存,确保数据更新后缓存的一致性
* 当用户数据更新后,清除对应的缓存,迫使下次查询时重新从数据库加载最新数据
*
* @param sysUserReq 用户请求数据
* @return 包含更新后用户数据的Result对象
*
* @CacheEvict 注解说明:
* - cacheNames: 指定要清除的缓存名称,与@Cacheable中的cacheNames对应
* - key: 使用SpEL表达式生成要清除的缓存键,格式为'querySysUserById'+用户ID
* 注意:这里假设sysUserReq中包含id字段,否则需要调整SpEL表达式
*/
@PutMapping("/edit")
@CacheEvict(cacheNames = "SysUser", key = "'querySysUserById'+#id")
public Result<SysUserPo> edit(@RequestBody SysUserReq sysUserReq) {
// 将请求对象转换为数据传输对象
SysUserDto sysUserDto = SysUserDtoConvert.INSTANCE.convertReqToDto(sysUserReq);
// 调用服务层方法更新用户数据
// 方法执行成功后,会自动清除指定的缓存
return Result.ok(this.sysUserService.update(sysUserDto));
}
3. 错误测试1: Redis乱码和不过期问题
**说明:**可以看到存入的缓存数据是乱码,并且TTL时间为-1永不过期!

4. 解决方案: 修改RedisCacheManager
**说明:**在Redis自动配置中,修改注入Bean容器的RedisCacheManager,修改其创建方式即可!
java
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Redis配置类
* 配置RedisTemplate和RedisCacheManager,自定义序列化方式和缓存策略
*/
@Configuration // 声明为配置类,Spring启动时会自动加载
public class RedisConfig {
/**
* 配置RedisTemplate
* 设置键和值的序列化方式,以及连接工厂
*
* @param redisConnectionFactory Redis连接工厂,由Spring自动注入
* @return 配置好的RedisTemplate实例
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建RedisTemplate实例
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 创建字符串序列化器,用于键的序列化
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置键的序列化方式为字符串序列化
redisTemplate.setKeySerializer(redisSerializer);
// 设置哈希键的序列化方式为字符串序列化
redisTemplate.setHashKeySerializer(redisSerializer);
// 设置值的序列化方式为Jackson JSON序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
// 设置哈希值的序列化方式为Jackson JSON序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
return redisTemplate;
}
/**
* 配置Redis缓存管理器
* 设置缓存值的序列化方式和默认过期时间
*
* @param redisConnectionFactory Redis连接工厂,由Spring自动注入
* @return 配置好的RedisCacheManager实例
*/
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
// 创建非阻塞的Redis缓存写入器
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 创建序列化对,使用Jackson JSON序列化器
SerializationPair<Object> pair = SerializationPair.fromSerializer(jackson2JsonRedisSerializer());
// 配置Redis缓存默认设置
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig() // 获取默认配置
.serializeValuesWith(pair) // 设置值的序列化方式
.entryTtl(Duration.ofSeconds(10)); // 设置缓存条目默认过期时间为10秒
// 创建并返回Redis缓存管理器
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
/**
* 创建Jackson JSON序列化器
* 用于Redis值的序列化和反序列化
*
* @return 配置好的Jackson2JsonRedisSerializer实例
*/
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
// 创建Jackson JSON序列化器,支持任何Object类型
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 创建ObjectMapper实例,配置JSON序列化和反序列化规则
ObjectMapper objectMapper = new ObjectMapper();
// 设置所有属性(包括私有属性)都可见
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 配置反序列化时忽略未知属性,避免因JSON中包含未知属性而报错
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 将配置好的ObjectMapper设置到序列化器中
jsonRedisSerializer.setObjectMapper(objectMapper);
return jsonRedisSerializer;
}
}
5. 错误测试2: 获取缓存失败
**说明:**可以看到数据乱码问题解决,并且也实现了10s过期!

**问题:**但是第一次请求成功的情况下,第二次请求就会发生如下错误,意味着从Redis中获取的数据无法正确的进行类型转换!

6. 解决方案: 修改Jackson序列化配置

**注意:**可以看到Redis缓存的数据中带上了类型!

7. 测试成功
**说明:**多次操作都能成功从缓存中获取数据,接口响应速度大幅度提高!

七、guava实现本地缓存工具
封装CacheUtil (Guava实现)
**说明:**利用guava本地缓存和函数式编程来实现一个本地缓存。
**注意:**因为之前缓存使用Redis来做,如果当所有的缓存都存储在Redis中的时候,一旦网络不稳定导致未及时相应,所有请求都可能被阻塞,导致内存和CPU被打满,从而引起重大问题!
1. 配置相关依赖
java
<guava.version>19.0</guava.version>
<fastjson.version>1.2.75</fastjson.version>
<!-- guava相关依赖 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- fastjson相关依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
2. CacheUtil
**说明:**此处只有查询时添加缓存的操作,没有实现编辑时删除缓存的操作!
java
package com.qj.redis.util;
import com.alibaba.fastjson.JSON;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Component
@Slf4j
public class CacheUtil<K, V> {
// 意图从nacos的配置文件中获取,流量大的时候开启本地缓存,流量小的时候关闭本地缓存
@Value("${guava.cache.switch}")
public Boolean switchCache;
// 初始化一个guava的Cache
private Cache<String, String> localCache = CacheBuilder.newBuilder()
.maximumSize(5000)
.expireAfterWrite(3, TimeUnit.SECONDS)
.build();
public Map<K, V> getResult(List<K> skuIdList, String cachePrefix,
Class<V> clazz, Function<List<K>, Map<K, V>> function) {
if (CollectionUtils.isEmpty(skuIdList)) {
return Collections.emptyMap();
}
Map<K, V> resultMap = new HashMap<>(16);
// 1)本地缓存未开
if (!switchCache) {
// 从rpc接口查所有数据,返回结果集
resultMap = function.apply(skuIdList);
return resultMap;
}
// 2)默认开启本地缓存
List<K> noCacheList = new ArrayList<>();
// (2.1)查guava缓存
for (K skuId : skuIdList) {
String cacheKey = cachePrefix + "_" + skuId;
String content = localCache.getIfPresent(cacheKey);
if (StringUtils.isNotBlank(content)) {
// 能查到的直接放进结果集中
V v = JSON.parseObject(content, clazz);
resultMap.put(skuId, v);
} else {
// 查不到的先放进noCacheList中,后面统一使用rpc查询
noCacheList.add(skuId);
}
}
// (2.2)如果没有查不到的,直接返回结果集
if (CollectionUtils.isEmpty(noCacheList)) {
return resultMap;
}
// (2.3)如果有查不到的,从rpc接口查guava中没有缓存的数据
Map<K, V> noCacheResultMap = function.apply(noCacheList);
// (2.4)如果rpc接口也没查到任何数据,直接返回结果集
if (CollectionUtils.isEmpty(noCacheResultMap)) {
return resultMap;
}
// (2.5)将从rpc中查出的结果,添加guava的本地缓存和结果集中
for (Entry<K, V> entry : noCacheResultMap.entrySet()) {
K skuId = entry.getKey();
V content = entry.getValue();
// 查询内容放进结果集
resultMap.put(skuId, content);
// 查询内容放进guava本地缓存
String cacheKey = cachePrefix + "_" + skuId;
localCache.put(cacheKey, JSON.toJSONString(content));
}
// (2.6)返回结果集
return resultMap;
}
}
3. 调用CacheUtil
**说明:**此处使用了伪代码来实现,此处重点是 ------ 将无法在缓存中查询的数据,转而需要通过使用RPC框架或者调用本地Service层查询数据库的代码,使用函数式编程方式,将其调用作为一个参数传入CacheUtil的方法中!
java
package com.qj.sys.controller;
import com.qj.redis.util.CacheUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 测试控制器
* 用于演示本地缓存的使用方式和效果
*/
@RestController // 声明为REST控制器,所有方法返回值直接作为HTTP响应体
@Slf4j // Lombok日志注解
@RequestMapping("test") // 设置基础请求路径为/test
public class TestController {
// 注入缓存工具类
@Autowired
private CacheUtil cacheUtil;
/**
* 测试本地缓存接口
* 演示如何使用CacheUtil获取多种类型的数据并利用缓存提高性能
*
* @param skuIdList SKU ID列表,通过请求参数传入
*/
@GetMapping("/testLocalCache")
public void testLocalCache(List<Long> skuIdList) {
// 获取SKU名称信息,使用缓存优化
// 参数说明:
// 1. skuIdList: 需要查询的SKU ID列表
// 2. "skuInfo.skuName": 缓存键前缀,用于区分不同类型的缓存
// 3. SkuInfo.class: 返回值类型
// 4. Lambda表达式: 缓存未命中时的数据加载逻辑
cacheUtil.getResult(skuIdList, "skuInfo.skuName", SkuInfo.class, (list) -> {
// 当缓存中不存在数据时,执行此方法获取数据
Map<Long, SkuInfo> skuInfo = getSkuInfo(skuIdList);
return skuInfo;
});
// 获取SKU价格信息,同样使用缓存优化
cacheUtil.getResult(skuIdList, "skuInfo.skuPrice", SkuPrice.class, (list) -> {
// 当缓存中不存在数据时,执行此方法获取数据
Map<Long, SkuPrice> skuPrice = getSkuPrice(skuIdList);
return skuPrice;
});
}
/**
* 模拟RPC接口 - 获取SKU信息
* 实际项目中可以使用OpenFeign等工具实现远程调用
*
* @param skuIdList SKU ID列表
* @return SKU信息映射表
*/
public Map<Long, SkuInfo> getSkuInfo(List<Long> skuIdList) {
// 模拟远程调用,返回空映射
// 实际项目中这里会调用商品服务的接口获取SKU信息
return Collections.emptyMap();
}
/**
* 模拟RPC接口 - 获取SKU价格
* 实际项目中可以使用OpenFeign等工具实现远程调用
*
* @param skuIdList SKU ID列表
* @return SKU价格映射表
*/
public Map<Long, SkuPrice> getSkuPrice(List<Long> skuIdList) {
// 模拟远程调用,返回空映射
// 实际项目中这里会调用价格服务的接口获取SKU价格
return Collections.emptyMap();
}
/**
* SKU信息内部类
* 用于表示商品基本信息
*/
class SkuInfo {
private Long id; // SKU ID
private String name; // SKU名称
}
/**
* SKU价格内部类
* 用于表示商品价格信息
*/
class SkuPrice {
private Long id; // SKU ID
private Double price; // SKU价格
}
}