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价格
    }
}
相关推荐
Scarlett5 小时前
初识cocos,实现《FlappyBird》h5游戏
前端·cocos creator
古夕5 小时前
Vue 3 复杂表单父子组件双向绑定的最佳实践
前端·javascript·vue.js
烛阴5 小时前
TypeScript 进阶必修课:解锁强大的内置工具类型(一)
前端·javascript·typescript
anyup5 小时前
太全面啦!总结篇!99% 开发者可能都会遇到的 uView Pro 组件库问题
前端·vue.js·uni-app
૮・ﻌ・5 小时前
CSS基础学习第二天
前端·css·学习·emmet语法
Zayn5 小时前
前端路径别名跳转和提示失效?一文搞懂解决方案
前端·javascript·visual studio code
葡萄城技术团队6 小时前
【SpreadJS V18.2 新特性】Table 与 DataTable 双向转换功能详解
前端
Nicholas_ly6 小时前
copilot
前端