刚果商城(congomall)
整体架构
公共规约组件
congomall-base-spring-boot-starter
META-INF/spring.factories 自动装配
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.opengoofy.congomall.springboot.starter.base.config.ApplicationBaseAutoConfiguration
/**
* 应用基础自动装配
*
* @author chen.ma
* @github <a href="https://github.com/opengoofy" />
* @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
*/
public class ApplicationBaseAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ApplicationContextHolder congoApplicationContextHolder() {
return new ApplicationContextHolder();
}
@Bean
@ConditionalOnMissingBean
public ApplicationContentPostProcessor congoApplicationContentPostProcessor() {
return new ApplicationContentPostProcessor();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "congomall.fastjson.safa-mode", havingValue = "true")
public FastJsonSafeMode congoFastJsonSafeMode() {
return new FastJsonSafeMode();
}
}
首先,这个类实现了 ApplicationListener<ApplicationReadyEvent>
接口,表示它是一个监听应用启动事件的监听器。
在类的开头,引入了一些必要的包,并对版权进行了声明。
接下来是类的定义和成员变量的声明,其中 @Resource
注解用于注入 ApplicationContext
对象。
代码中的 executeOnlyOnce
是一个标志位,用于确保事件只执行一次。默认情况下,它被设置为 true
。
在 onApplicationEvent
方法中,通过加锁的方式保证在多线程环境下只执行一次。在方法内部,首先判断 executeOnlyOnce
是否为 true
,如果是,则发布一个 ApplicationInitializingEvent
事件,表示应用正在初始化。然后将 executeOnlyOnce
设置为 false
,确保事件只被执行一次。
总的来说,这段代码的作用是在应用启动完成后,通过发布 ApplicationInitializingEvent
事件来触发一些初始化操作。通过 executeOnlyOnce
的设置,确保事件只执行一次,避免重复执行。
请注意,我的解读是基于代码的语法和结构,无法获取代码的具体上下文和业务逻辑。如果你需要更详细的解读或有其他问题,请提供更多的信息。
确保事件只执行一次是为了避免重复执行相同的操作,从而避免可能引发的重复资源消耗或潜在的错误。
在某些情况下,特定的初始化操作只需要在应用启动时执行一次。如果这个事件被多次触发,那么这些初始化操作就会被重复执行,可能导致不必要的资源浪费或产生意外的行为。
通过设置一个标志位 executeOnlyOnce
,可以在多线程环境下保证事件只执行一次。当事件第一次被触发时,将标志位设置为 false
,后续的事件触发时会检查这个标志位,并不再执行相应的操作。
这种设计模式通常被称为 “单次执行” 或 “只执行一次” 模式,它常用于需要确保某些代码只被执行一次的场景,如初始化操作、注册回调函数等。
总而言之,确保事件只执行一次可以避免重复执行操作,提高代码执行效率,并减少可能出现的问题和错误。
确保事件只执行一次的场景有很多,以下是其中几个常见的例子:
- 应用初始化:某些应用启动后需要进行初始化操作,如加载配置、创建数据库连接等。这些初始化操作只需在应用启动时执行一次。
- 资源加载:在应用启动时加载一些资源,如读取配置文件、加载字典数据等。这些操作可以通过确保只执行一次来避免资源的重复加载和浪费。
- 缓存预热:在应用启动时,将某些数据加载到缓存中,以提高后续请求的响应速度。这个操作通常只需要在应用启动时执行一次,避免重复加载和缓存数据不一致的问题。
- 注册回调函数:有时需要在特定事件发生时触发回调函数,但只需在第一次事件发生时注册回调函数即可。后续的事件发生时,可以通过确保只执行一次来避免重复触发回调。
- 数据库迁移或升级:在应用升级或迁移时,可能需要执行一些数据库变更操作。通过确保只执行一次,可以避免重复执行数据库变更脚本并保持数据的一致性。
这些场景都需要确保事件只执行一次,避免重复操作和可能出现的问题。通过设置标志位或其他方式,可以在多线程环境下实现事件只执行一次的效果。
FastJson安全模式,开启后关闭类型隐式传递
/**
* FastJson安全模式,开启后关闭类型隐式传递
*
* @author chen.ma
* @github <a href="https://github.com/opengoofy" />
* @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
*/
public class FastJsonSafeMode implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.setProperty("fastjson2.parser.safeMode", "true");
}
}
方便非spring类获取bean
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext CONTEXT;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder.CONTEXT = applicationContext;
}
/**
* Get ioc container bean by type.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return CONTEXT.getBean(clazz);
}
/**
* Get ioc container bean by name and type.
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return CONTEXT.getBean(name, clazz);
}
/**
* Get a set of ioc container beans by type.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> Map<String, T> getBeansOfType(Class<T> clazz) {
return CONTEXT.getBeansOfType(clazz);
}
/**
* Find whether the bean has annotations.
*
* @param beanName
* @param annotationType
* @param <A>
* @return
*/
public static <A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType) {
return CONTEXT.findAnnotationOnBean(beanName, annotationType);
}
/**
* Get ApplicationContext.
*
* @return
*/
public static ApplicationContext getInstance() {
return CONTEXT;
}
}
congomall-common-spring-boot-starter
标识枚举
public enum FlagEnum {
/**
* 正常状态
*/
FALSE(0),
/**
* 删除状态
*/
TRUE(1);
private final Integer flag;
FlagEnum(Integer flag) {
this.flag = flag;
}
public Integer code() {
return this.flag;
}
public String strCode() {
return String.valueOf(this.flag);
}
@Override
public String toString() {
return strCode();
}
}
使用:
productCommentDO.setCommentFlag(FlagEnum.FALSE.code());
封装spring环境工具类
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Map;
/**
* Assert.
*/
public class Assert {
public static void isTrue(boolean expression, String message) {
if (!expression) {
throw new IllegalArgumentException(message);
}
}
public static void isTrue(boolean expression) {
isTrue(expression, "[Assertion failed] - this expression must be true");
}
public static void isNull(Object object, String message) {
if (object != null) {
throw new IllegalArgumentException(message);
}
}
public static void isNull(Object object) {
isNull(object, "[Assertion failed] - the object argument must be null");
}
public static void notNull(Object object, String message) {
if (object == null) {
throw new IllegalArgumentException(message);
}
}
public static void notNull(Object object) {
notNull(object, "[Assertion failed] - this argument is required; it must not be null");
}
public static void notEmpty(Collection<?> collection, String message) {
if (CollectionUtils.isEmpty(collection)) {
throw new IllegalArgumentException(message);
}
}
public static void notEmpty(Collection<?> collection) {
notEmpty(collection,
"[Assertion failed] - this collection must not be empty: it must contain at least 1 element");
}
public static void notEmpty(Map<?, ?> map, String message) {
if (CollectionUtils.isEmpty(map)) {
throw new IllegalArgumentException(message);
}
}
public static void notEmpty(Map<?, ?> map) {
notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry");
}
public static void notEmpty(String str, String message) {
if (StringUtils.isEmpty(str)) {
throw new IllegalArgumentException(message);
}
}
public static void notEmpty(String str) {
if (StringUtils.isEmpty(str)) {
notEmpty(str, "[Assertion failed] - this string must not be empty");
}
}
public static void notBlank(String str, String message) {
if (org.apache.commons.lang3.StringUtils.isBlank(str)) {
throw new IllegalArgumentException(message);
}
}
public static void notBlank(String str) {
notBlank(str, "[Assertion failed] - this string must not be blank");
}
public static void hasText(String text, String message) {
if (!StringUtils.hasText(text)) {
throw new IllegalArgumentException(message);
}
}
public static void hasText(String text) {
hasText(text,
"[Assertion failed] - this String argument must have text; it must not be null, empty, or blank");
}
}
当前环境判断
/**
* 环境工具类
*
* @author chen.ma
* @github <a href="https://github.com/opengoofy" />
* @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
*/
public class EnvironmentUtil {
private static List<String> ENVIRONMENT_LIST = new ArrayList<>();
static {
ENVIRONMENT_LIST.add("dev");
ENVIRONMENT_LIST.add("test");
}
/**
* 判断当前是否为正式环境
*
* @return
*/
public static boolean isProdEnvironment() {
ConfigurableEnvironment configurableEnvironment = ApplicationContextHolder.getBean(ConfigurableEnvironment.class);
String propertyActive = configurableEnvironment.getProperty("spring.profiles.active", "dev");
return !ENVIRONMENT_LIST.stream().filter(each -> propertyActive.contains(each)).findFirst().isPresent();
}
}
应用:验证码开关
public void checkoutValidCode(String verifyCode) {
if (EnvironmentUtil.isProdEnvironment()) {
if (StrUtil.isBlank(verifyCode)) {
throw new ClientException("验证码已失效");
}
verifyCode = StrUtil.trim(verifyCode);
this.verifyCode = StrUtil.trim(this.verifyCode);
if (!StrUtil.equals(verifyCode, this.verifyCode)) {
throw new ClientException("验证码错误");
}
}
}
sleep方法异常统一捕获
/**
* 线程池工具类
*
* @author chen.ma
* @github <a href="https://github.com/opengoofy" />
* @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
*/
public final class ThreadUtil {
/**
* 睡眠当前线程指定时间 {@param millis}
*
* @param millis 睡眠时间,单位毫秒
*/
@SneakyThrows(value = InterruptedException.class)
public static void sleep(long millis) {
Thread.sleep(millis);
}
}
@SneakyThrows(value = InterruptedException.class)
是 Lombok 注解之一,它的作用是在方法中自动处理 InterruptedException 异常。
在 Java 中,Thread.sleep() 方法会抛出 InterruptedException 异常,当一个线程在睡眠期间被中断时,即另一个线程调用该线程的 interrupt() 方法时,就会抛出 InterruptedException。
通常情况下,我们需要显式地在方法中捕获并处理 InterruptedException 异常。但是使用 @SneakyThrows
注解可以简化这一过程,使我们不需要在方法中编写 try-catch 语句来处理异常。
具体来说,@SneakyThrows(value = InterruptedException.class)
的作用是在编译时自动生成捕获和重新抛出 InterruptedException 异常的代码块。这样,我们就不需要在方法体中显式捕获 InterruptedException,而是将其交给 @SneakyThrows
注解来自动处理。
这样做的好处是减少了代码的冗余,提高了代码的可读性和简洁性。但需要注意的是,在使用 @SneakyThrows
注解时,方法声明必须包含对应的异常类型,否则编译器会报错。
总结来说,@SneakyThrows(value = InterruptedException.class)
的作用是在方法中自动处理 InterruptedException 异常,简化了代码编写,提高了代码的可读性和简洁性。
构建者模式
/**
* 线程池 {@link ThreadPoolExecutor} 构建器, 构建者模式
*
* @author chen.ma
* @github <a href="https://github.com/opengoofy" />
* @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
*/
public final class ThreadPoolBuilder implements Builder<ThreadPoolExecutor> {
private int corePoolSize = calculateCoreNum();
// 等价1.5倍corepoolsize
private int maximumPoolSize = corePoolSize + (corePoolSize >> 1);
private long keepAliveTime = 30000L;
private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
private BlockingQueue workQueue = new LinkedBlockingQueue(4096);
private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
private boolean isDaemon = false;
private String threadNamePrefix;
private ThreadFactory threadFactory;
private Integer calculateCoreNum() {
int cpuCoreNum = Runtime.getRuntime().availableProcessors();
return new BigDecimal(cpuCoreNum).divide(new BigDecimal("0.2")).intValue();
}
public ThreadPoolBuilder threadFactory(ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
return this;
}
public ThreadPoolBuilder corePoolSize(int corePoolSize) {
this.corePoolSize = corePoolSize;
return this;
}
public ThreadPoolBuilder maximumPoolSize(int maximumPoolSize) {
this.maximumPoolSize = maximumPoolSize;
if (maximumPoolSize < this.corePoolSize) {
this.corePoolSize = maximumPoolSize;
}
return this;
}
public ThreadPoolBuilder threadFactory(String threadNamePrefix, Boolean isDaemon) {
this.threadNamePrefix = threadNamePrefix;
this.isDaemon = isDaemon;
return this;
}
public ThreadPoolBuilder keepAliveTime(long keepAliveTime) {
this.keepAliveTime = keepAliveTime;
return this;
}
public ThreadPoolBuilder keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
this.keepAliveTime = keepAliveTime;
this.timeUnit = timeUnit;
return this;
}
public ThreadPoolBuilder rejected(RejectedExecutionHandler rejectedExecutionHandler) {
this.rejectedExecutionHandler = rejectedExecutionHandler;
return this;
}
public ThreadPoolBuilder workQueue(BlockingQueue workQueue) {
this.workQueue = workQueue;
return this;
}
public static ThreadPoolBuilder builder() {
return new ThreadPoolBuilder();
}
@Override
public ThreadPoolExecutor build() {
if (threadFactory == null) {
Assert.notEmpty(threadNamePrefix, "The thread name prefix cannot be empty or an empty string.");
threadFactory = ThreadFactoryBuilder.builder().prefix(threadNamePrefix).daemon(isDaemon).build();
}
ThreadPoolExecutor executorService;
try {
executorService = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
workQueue,
threadFactory,
rejectedExecutionHandler);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Error creating thread pool parameter.", ex);
}
return executorService;
}
}
最好根据本机CPU设置核心线程池数
private Integer calculateCoreNum() {
int cpuCoreNum = Runtime.getRuntime().availableProcessors();
return new BigDecimal(cpuCoreNum).divide(new BigDecimal("0.2")).intValue();
}
拒绝策略
AbortPolicy:抛出异常,终止任务
**CallerRunsPolicy:**使用调用线程执行任务
**DiscardPolicy:**直接丢弃
**DiscardPolicy:**丢弃队列最老任务,添加新任务
动态代理模式:增强线程池拒绝策略
public static RejectedExecutionHandler createProxy(RejectedExecutionHandler rejectedExecutionHandler, AtomicLong rejectedNum) {
// 动态代理模式: 增强线程池拒绝策略,比如:拒绝任务报警或加入延迟队列重复放入等逻辑
return (RejectedExecutionHandler) Proxy
.newProxyInstance(
rejectedExecutionHandler.getClass().getClassLoader(),
new Class[]{RejectedExecutionHandler.class},
new RejectedProxyInvocationHandler(rejectedExecutionHandler, rejectedNum));
}
这段代码实现了一个动态代理模式,用于增强线程池的拒绝策略。具体来说,它创建了一个代理对象,将原始的拒绝执行处理器(RejectedExecutionHandler)替换为增强后的处理器。
在 createProxy
方法中,使用了 Java 的动态代理机制。通过调用 Proxy.newProxyInstance
方法,可以创建一个代理对象,该代理对象实现了 RejectedExecutionHandler
接口,并且通过一个自定义的代理处理器(RejectedProxyInvocationHandler)来处理方法调用。
方法的参数包括原始的拒绝执行处理器(rejectedExecutionHandler
)和一个原子长整型对象(rejectedNum
)。代理处理器会将所有方法调用委托给原始的拒绝执行处理器,并在适当的时候进行增强处理。
通过使用动态代理模式,可以在不修改原始拒绝执行处理器代码的情况下,对其功能进行扩展。比如可以在任务被拒绝时触发报警、将任务加入延迟队列以便稍后再次尝试执行等额外逻辑。这样可以灵活地定制拒绝策略,并增强线程池的容错能力和业务逻辑。
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(1, 3, 1024, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1));
ThreadPoolExecutor.AbortPolicy abortPolicy = new ThreadPoolExecutor.AbortPolicy();
AtomicLong rejectedNum = new AtomicLong();
RejectedExecutionHandler proxyRejectedExecutionHandler = RejectedProxyUtil.createProxy(abortPolicy, rejectedNum);
threadPoolExecutor.setRejectedExecutionHandler(proxyRejectedExecutionHandler);
for (int i = 0; i < 5; i++) {
try {
threadPoolExecutor.execute(() -> ThreadUtil.sleep(100000L));
} catch (Exception ignored) {
ignored.printStackTrace();
}
}
System.out.println("================ 线程池拒绝策略执行次数: " + rejectedNum.get());
}
这段代码实现了一个简单的线程池示例,使用了之前提到的动态代理增强了线程池的拒绝策略。下面是代码的执行流程:
- 创建一个带有边界和容量的
ThreadPoolExecutor
对象,其中核心线程数为 1,最大线程数为 3,队列容量为 1。 - 使用
ThreadPoolExecutor.AbortPolicy
创建一个拒绝策略对象abortPolicy
,它会在任务被拒绝时抛出异常。 - 创建一个原子长整型对象
rejectedNum
,用于记录被拒绝的任务数量。 - 使用之前提到的
RejectedProxyUtil.createProxy
方法创建代理的拒绝执行处理器proxyRejectedExecutionHandler
,将原始的abortPolicy
和rejectedNum
作为参数传入。 - 将代理的拒绝执行处理器设置给线程池执行器,以替换默认的拒绝策略。
- 进行一个循环,共执行 5 次。在每次循环中,通过调用
threadPoolExecutor.execute
方法提交一个新的任务,该任务会暂停执行 100000 毫秒(100 秒)。 - 如果任务被拒绝,即超出了线程池的容量和队列容量限制,会进入 catch 块,并打印异常信息。
- 最后输出被拒绝的任务数量
rejectedNum.get()
。
这段代码展示了如何创建线程池、设置拒绝策略,并使用动态代理对拒绝策略进行增强。通过输出被拒绝的任务数量,可以观察线程池拒绝策略执行的次数。
AtomicLong
AtomicLong
是一个原子长整型类,用于在并发场景下进行线程安全的长整型操作。在这段代码中,rejectedNum
创建了一个新的 AtomicLong
对象,用于记录被拒绝的任务数量。
由于线程池的拒绝策略会在任务被拒绝时执行,为了线程安全地更新计数器,使用了 AtomicLong
。通过调用 rejectedNum.get()
方法可以获取当前的计数值,而通过调用 rejectedNum.incrementAndGet()
方法可以对计数进行原子性的自增操作。
decrementAndGet
快速消费线程池
/**
* 快速消费线程池
*
* @author chen.ma
* @github <a href="https://github.com/opengoofy" />
* @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
*/
public class EagerThreadPoolExecutor extends ThreadPoolExecutor {
public EagerThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
TaskQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
private final AtomicInteger submittedTaskCount = new AtomicInteger(0);
public int getSubmittedTaskCount() {
return submittedTaskCount.get();
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
submittedTaskCount.decrementAndGet();
}
@Override
public void execute(Runnable command) {
submittedTaskCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException ex) {
TaskQueue taskQueue = (TaskQueue) super.getQueue();
try {
if (!taskQueue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) {
submittedTaskCount.decrementAndGet();
throw new RejectedExecutionException("Queue capacity is full.", ex);
}
} catch (InterruptedException iex) {
submittedTaskCount.decrementAndGet();
throw new RejectedExecutionException(iex);
}
} catch (Exception ex) {
submittedTaskCount.decrementAndGet();
throw ex;
}
}
}
/**
* 快速消费任务队列
*
* @author chen.ma
* @github <a href="https://github.com/opengoofy" />
* @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
*/
public class TaskQueue<R extends Runnable> extends LinkedBlockingQueue<Runnable> {
@Setter
private EagerThreadPoolExecutor executor;
public TaskQueue(int capacity) {
super(capacity);
}
@Override
public boolean offer(Runnable runnable) {
int currentPoolThreadSize = executor.getPoolSize();
// 如果有核心线程正在空闲,将任务加入阻塞队列,由核心线程进行处理任务
if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
return super.offer(runnable);
}
// 当前线程池线程数量小于最大线程数,返回 False,根据线程池源码,会创建非核心线程
if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
return false;
}
// 如果当前线程池数量大于最大线程数,任务加入阻塞队列
return super.offer(runnable);
}
public boolean retryOffer(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
if (executor.isShutdown()) {
throw new RejectedExecutionException("Executor is shutdown!");
}
return super.offer(o, timeout, unit);
}
}
这样写的目的是为了实现一种特定的任务处理策略。在这个策略中,首先会尽可能地将任务分配给空闲的核心线程来处理,以保证快速消费和处理任务。
为什么要优先将任务分配给空闲的核心线程呢?这是因为核心线程是线程池的基础,它们始终存在并且可以立即执行任务,避免了线程创建和销毁的开销。通过利用核心线程处理任务,可以最大限度地利用线程池的资源,提高任务处理的效率。
如果核心线程都已经被占用,那么会进一步判断当前线程池的线程数量是否已达到最大限制。如果还未达到最大限制,则返回 false
,让线程池创建一个非核心线程来处理任务。这样可以控制线程池的线程数量,避免无限制地创建线程,从而保护系统资源的稳定性。
如果线程池的线程数量已经达到或超过最大限制,那么新任务将被添加到阻塞队列中等待处理。这样,即使线程池已满,任务也不会被丢弃,而是会在阻塞队列中等待有可用线程来处理。
综上所述,这样的设计可以根据线程池的状态和任务数量,采取不同的策略来处理任务,以实现快速消费和高效利用线程池资源的目标。
congomall-cache-spring-boot-starter
缓存穿透布隆过滤器
/**
* 防止缓存穿透的布隆过滤器
*/
@Bean
@ConditionalOnProperty(prefix = BloomFilterPenetrateProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public RBloomFilter<String> cachePenetrationBloomFilter(RedissonClient redissonClient, BloomFilterPenetrateProperties bloomFilterPenetrateProperties) {
RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter(bloomFilterPenetrateProperties.getName());
cachePenetrationBloomFilter.tryInit(bloomFilterPenetrateProperties.getExpectedInsertions(), bloomFilterPenetrateProperties.getFalseProbability());
return cachePenetrationBloomFilter;
}
@Override
public void safePut(String key, Object value, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter) {
put(key, value, timeout, timeUnit);
bloomFilter.add(key);
}
get
@Override
public <T> T safeGet(String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit,
RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheGetFilter, CacheGetIfAbsent<String> cacheGetIfAbsent) {
T result = get(key, clazz);
// 缓存结果不等于空或空字符串直接返回;通过函数判断是否返回空,为了适配布隆过滤器无法删除的场景;两者都不成立,判断布隆过滤器是否存在,不存在返回空
if (!CacheUtil.isNullOrBlank(result)
|| Optional.ofNullable(cacheGetFilter).map(each -> each.filter(key)).orElse(false)
|| Optional.ofNullable(bloomFilter).map(each -> !each.contains(key)).orElse(false)) {
return result;
}
RLock lock = redissonClient.getLock(SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX + key);
lock.lock();
try {
// 双重判定锁,减轻获得分布式锁后线程访问数据库压力
if (CacheUtil.isNullOrBlank(result = get(key, clazz))) {
// 如果访问 cacheLoader 加载数据为空,执行后置函数操作
if (CacheUtil.isNullOrBlank(result = loadAndSet(key, cacheLoader, timeout, timeUnit, true, bloomFilter))) {
Optional.ofNullable(cacheGetIfAbsent).ifPresent(each -> each.execute(key));
}
}
} finally {
lock.unlock();
}
return result;
}
缓存穿透、缓存击穿和缓存雪崩是常见的与缓存相关的问题和现象,它们可以对系统性能和可用性造成影响。
-
缓存穿透(Cache Penetration): 缓存穿透指的是在缓存中无法命中所需数据,并且每次请求都会导致数据库查询。这通常发生在恶意攻击或者非法用户请求不存在的数据时。由于缓存中不存在该数据,每次请求都直接访问数据库,从而导致数据库负载过高,甚至可能引起系统崩溃。
解决方案:可以通过在缓存中预先设置一个“空值”来避免缓存穿透。当查询到结果为空时,将空值存入缓存,并设置较短的过期时间,以防止攻击者频繁请求。
-
缓存击穿(Cache Breakdown): 缓存击穿指的是某个热点数据过期或被移除,而此时正好有大量并发请求同时访问该数据,导致请求直接访问数据库,增加了数据库负载。相比于缓存穿透,缓存击穿通常发生在存在合法数据的情况下。
解决方案:一种解决方案是使用互斥锁(例如分布式锁)来保护缓存数据的访问,只允许一个线程去重新加载缓存,并在加载期间屏蔽其他请求对该缓存的访问。另一种方式是使用缓存预热,提前主动加载热点数据到缓存中,避免在关键时刻过期。
-
缓存雪崩(Cache Avalanche): 缓存雪崩指的是在某个时间段内,大量缓存数据同时失效或过期,导致大量请求直接访问数据库,造成数据库负载剧增。通常这是由于缓存服务器宕机、网络故障或者大规模缓存数据同时失效等情况导致的。
解决方案:为了避免缓存雪崩,可以采用以下几种策略:
- 设置不同的缓存过期时间,避免同时大量缓存失效。
- 使用分布式缓存,将缓存数据部署在多个节点上,提高缓存的可用性和容错能力。
- 实施限流和熔断机制,控制缓存失效时的请求量,避免对数据库造成过大压力。
以上是对缓存穿透、缓存击穿和缓存雪崩的简要介绍及解决方案。在实际开发中,可根据具体情况选择适合的缓存策略和技术来应对这些问题。
静态代理模式
@Bean
// 静态代理模式: Redis 客户端代理类增强
public StringRedisTemplateProxy stringRedisTemplateProxy(RedisKeySerializer redisKeySerializer,
StringRedisTemplate stringRedisTemplate,
RedissonClient redissonClient) {
stringRedisTemplate.setKeySerializer(redisKeySerializer);
return new StringRedisTemplateProxy(stringRedisTemplate, redisDistributedProperties, redissonClient);
}
静态代理模式是一种设计模式,它通过创建一个代理对象来间接访问原始对象,并在代理对象中提供额外的功能或控制访问。在静态代理模式中,代理类和原始类是在编译时就确定的,并且代理类和原始类实现相同的接口或继承相同的父类。
静态代理模式通常由以下几个角色组成:
- 抽象角色(接口或抽象类):定义了代理类和原始类之间的公共方法,是代理类和原始类的共同接口。
- 原始角色:实际执行业务逻辑的类,也称为被代理类或目标类。
- 代理角色:代理类,持有对原始类的引用,并在其方法中调用原始类的方法,在调用前后可以执行额外的操作。
- 客户端:使用代理对象来访问原始对象的类。
静态代理模式的主要优点是可以在不修改原始类的情况下,通过代理类来增强原始类的功能,可以实现对原始类的访问控制、增加日志记录、性能监控等功能。缺点是当原始类的接口发生变化时,代理类也需要相应修改。
单例模式
// 饿汉式
public class singleton{
private static finnal singleton instance = new singleton();
private singleton(){
}
public static singleton get(){
return instance;
}
}
// 懒汉式
public class singleton{
private static singleton instance;
private singleton(){
}
public static singleton get(){
if(instance == null){
instance = = new singleton();
}
return instance;
}
// 双重判定锁
public static singleton get(){
if(instance == null){
synchronized(singleton.class){
if(instance == null){
instance = = new singleton();
}
}
}
return instance;
}
}
// 静态内部类
public class singleton{
private singleton(){
}
private static class s{
private static finnal singleton instance = new singleton();
}
public static singleton get(){
return s.instance;
}
}
// 枚举类
public enum Singleton {
INSTANCE;
// 添加其他方法和属性
public void doSomething() {
// 单例对象的操作
}
}