Java开发 - 缓存

一、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语言核心类扩展功能的工具库,主要包括:

  1. 字符串处理:增强的字符串操作方法

  2. 数值处理:数字和数值类型的工具类

  3. 对象操作:对象比较、哈希码生成、toString方法等

  4. 异常处理:异常链和嵌套异常处理

  5. 系统属性:Java系统属性访问工具

  6. 随机数生成:更强大的随机数生成器

  7. 日期时间处理:日期和时间操作工具

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价格
    }
}
相关推荐
恋猫de小郭11 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅17 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606118 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了18 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅18 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅18 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅19 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment19 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅19 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊19 小时前
jwt介绍
前端