两级缓存相比单纯使用远程缓存,具有什么优势呢?
本地缓存基于本地环境的内存,访问速度非常快,对于一些变更频率低、实时性要求低的数据,可以放在本地缓存中,提升访问速度
使用本地缓存能够减少和Redis类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时
但是在设计中,还是要考虑一些问题的,例如数据一致性问题。首先,两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。
如果是分布式环境下,一级缓存之间也会存在一致性问题,当一个节点下的本地缓存修改后,需要通知其他节点也刷新本地缓存中的数据,否则会出现读取到过期数据的情况,这一问题可以通过类似于Redis中的发布/订阅功能解决。
此外,缓存的过期时间、过期策略以及多线程访问的问题也都需要考虑进去,不过我们今天暂时先不考虑这些问题,简单的在代码中实现两级缓存的管理。
图片中一级缓存找的图是Ehcache,但实际项目中我使用的是caffeine做一级缓存,redis做二级缓存原理都是一样,先引入相关依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
</dependency>
在yml中配置redis的相关信息
java
redis:
host: localhost
port: 6379
password:
timeout: 5000
# lettuce:
# pool:
# max-active: 8
# max-wait: -1ms
# max-idle: 8
# min-idle: 0
注释的lettuce都是默认值,实际要调整放开注释自行调整即可
枚举类型枚举
java
public enum CacheType {
/**
* 存取
*/
FULL,
/**
* 只存
*/
PUT,
/**
* 删除
*/
DEL
}
定义一个注解,用于添加在需要操作缓存的方法上,使用cacheName + key作为缓存的真正key,timeOut为可以设置的二级缓存Redis的过期时间,type是一个枚举类型的变量,表示操作缓存的类型
java
import com.yx.light.element.jpa.enums.CacheType;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface L2Cache {
/**
* 缓存名字
* @return
*/
String cacheName();
/**
* 缓存key
* @return
*/
String key() default ""; //支持springEl表达式
/**
* redis缓存超时时间
* @return
*/
long timeOut() default 120;
/**
* 缓存类型
* @return
*/
CacheType type() default CacheType.FULL;
}
RedisTemplate配置类
java
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 配置自定义redisTemplate
*
* @return
*/
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//设置值(value)的序列化采用Jackson2JsonRedisSerializer。
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//设置键(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
Caffeine配置类
java
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CaffeineConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
.initialCapacity(128)//初始大小
.maximumSize(1024)//最大数量
.expireAfterWrite(60, TimeUnit.SECONDS)//过期时间
.build();
}
}
El转换辅助工具类
java
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.TreeMap;
public class ElParserUtil {
private ElParserUtil() {
}
public static String parse(String elString, TreeMap<String, Object> map) {
elString = String.format("#{%s}", elString);
//创建表达式解析器
ExpressionParser parser = new SpelExpressionParser();
//通过evaluationContext.setVariable可以在上下文中设定变量。
EvaluationContext context = new StandardEvaluationContext();
map.entrySet().forEach(entry ->
context.setVariable(entry.getKey(), entry.getValue())
);
//解析表达式
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
//使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
String value = expression.getValue(context, String.class);
return value;
}
}
两级缓存切面,在切面中操作Cache来读写Caffeine的缓存,操作RedisTemplate读写Redis缓存。
java
import com.github.benmanes.caffeine.cache.Cache;
import com.yx.light.element.jpa.annotations.L2Cache;
import com.yx.light.element.jpa.enums.CacheType;
import com.yx.light.element.jpa.utils.ElParserUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@Aspect
@AllArgsConstructor
public class L2CacheAspect {
private final Cache cache;
private final RedisTemplate redisTemplate;
@Pointcut("@annotation(com.yx.light.element.jpa.annotations.L2Cache)")
public void cacheAspect() {
}
@Around("cacheAspect()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//拼接解析springEl表达式的map
String[] paramNames = signature.getParameterNames();
Object[] args = point.getArgs();
TreeMap<String, Object> treeMap = new TreeMap<>();
for (int i = 0; i < paramNames.length; i++) {
treeMap.put(paramNames[i], args[i]);
}
L2Cache annotation = method.getAnnotation(L2Cache.class);
String elResult = null;
if (treeMap.size() == 0) {
elResult = StringUtils.isEmpty(annotation.key()) ? method.getName() : annotation.key();
} else {
elResult = ElParserUtil.parse(annotation.key(), treeMap);
}
String realKey = annotation.cacheName() + ":" + elResult;
//强制更新
if (annotation.type() == CacheType.PUT) {
Object object = point.proceed();
if (Objects.isNull(object)) {
log.info("方法执行完毕无返回值,无需更新缓存");
return object;
}
redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
cache.put(realKey, object);
return object;
} else if (annotation.type() == CacheType.DEL) {
redisTemplate.delete(realKey);
cache.invalidate(realKey);
return point.proceed();
}
//读写,查询Caffeine
Object caffeineCache = cache.getIfPresent(realKey);
if (Objects.nonNull(caffeineCache)) {
log.info("从caffeine中获取数据");
return caffeineCache;
}
//查询Redis
Object redisCache = redisTemplate.opsForValue().get(realKey);
if (Objects.nonNull(redisCache)) {
log.info("从redis中获取数据");
cache.put(realKey, redisCache);
return redisCache;
}
log.info("从数据库中获取数据");
Object object = point.proceed();
if (Objects.nonNull(object)) {
//写入Redis
redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
//写入Caffeine
cache.put(realKey, object);
}
return object;
}
}
改造service实现类的几个方法简单测试一下
java
@Override
@L2Cache(cacheName = "GroupHeader", type = CacheType.FULL)
public List<GroupHeader> findAllGroupHeader() {
return groupHeaderRepository.findAll();
}
@Override
@L2Cache(cacheName = "GroupHeader", key = "#groupHeader.groupCode", type = CacheType.PUT)
public void editGroupHeader(GroupHeader groupHeader) {
groupHeaderRepository.save(groupHeader);
}
@Override
@L2Cache(cacheName = "GroupHeader", key = "#ids", type = CacheType.DEL)
public void deleteGroupHeader(String ids) {
String[] split = ids.split(",");
for (String id : split) {
groupHeaderRepository.deleteById(Long.parseLong(id));
}
}
连续调用两次查询接口看日志打印效果和redis客户端的查询