wordpress建站说明,解读网站建设,新人如何自学做网站,四川省建设行业数据共享平台官网CaffeineRedis两级缓存架构
在高性能的服务项目中#xff0c;我们一般会将一些热点数据存储到 Redis这类缓存中间件中#xff0c;只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时#xff0c;也能降低数据库的压力。
但是在一些场景下单纯使用 Redis 的分布…CaffeineRedis两级缓存架构
在高性能的服务项目中我们一般会将一些热点数据存储到 Redis这类缓存中间件中只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时也能降低数据库的压力。
但是在一些场景下单纯使用 Redis 的分布式缓存不能满足高性能的要求所以还需要加入使用本地缓存Caffeine从而再次提升程序的响应速度与服务性能。于是就产生了使用本地缓存Caffeine作为一级缓存再加上分布式缓存Redis作为二级缓存的两级缓存架构。 两级缓存架构优缺点
优点
一级缓存基于应用的内存访问速度非常快对于一些变更频率低、实时性要求低的数据可以放在本地缓存中提升访问速度使用一级缓存能够减少和 Redis 的二级缓存的远程数据交互减少网络 I/O 开销降低这一过程中在网络通信上的耗时。
缺点
数据一致性问题两级缓存与数据库的数据要保持一致一旦数据发生了修改在修改数据库的同时一级缓存、二级缓存应该同步更新。分布式多应用情况下一级缓存之间也会存在一致性问题当一个节点下的本地缓存修改后需要通知其他节点也刷新本地一级缓存中的数据否则会出现读取到过期数据的情况。缓存的过期时间、过期策略以及多线程的问题
CaffeineRedis两级缓存架构实战
1、准备表结构和数据
准备如下的表结构和相关数据
DROP TABLE IF EXISTS user;CREATE TABLE user
(id BIGINT(20) NOT NULL COMMENT 主键ID,name VARCHAR(30) NULL DEFAULT NULL COMMENT 姓名,age INT(11) NULL DEFAULT NULL COMMENT 年龄,email VARCHAR(50) NULL DEFAULT NULL COMMENT 邮箱,PRIMARY KEY (id)
);
插入对应的相关数据
DELETE FROM user;INSERT INTO user (id, name, age, email) VALUES
(1, Jone, 18, test1baomidou.com),
(2, Jack, 20, test2baomidou.com),
(3, Tom, 28, test3baomidou.com),
(4, Sandy, 21, test4baomidou.com),
(5, Billie, 24, test5baomidou.com);
2、创建项目
创建一个SpringBoot项目然后引入相关的依赖首先是父依赖
parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.6/versionrelativePath/ !-- lookup parent from repository --/parent具体的其他的依赖
!-- spring-boot-starter-web 的依赖 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency!-- 引入MyBatisPlus的依赖 --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.5.1/version/dependency!-- 数据库使用MySQL数据库 --dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency!-- 数据库连接池 Druid --dependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.14/version/dependency!-- lombok依赖 --dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency3、配置信息
然后我们需要在application.properties中配置数据源的相关信息
spring.datasource.driverClassNamecom.mysql.cj.jdbc.Driver
spring.datasource.urljdbc:mysql://localhost:3306/mp?serverTimezoneUTCuseUnicodetruecharacterEncodingutf-8useSSLtrue
spring.datasource.usernameroot
spring.datasource.password123456spring.datasource.typecom.alibaba.druid.pool.DruidDataSource然后我们需要在SpringBoot项目的启动类上配置Mapper接口的扫描路径 4、添加User实体
添加user的实体类
ToString
Data
public class User {private Long id;private String name;private Integer age;private String email;
}5、创建Mapper接口
在MyBatisPlus中的Mapper接口需要继承BaseMapper.
/*** MyBatisPlus中的Mapper接口继承自BaseMapper*/
public interface UserMapper extends BaseMapperUser {
}6、测试操作
然后来完成对User表中数据的查询操作
SpringBootTest
class MpDemo01ApplicationTests {Autowiredprivate UserMapper userMapper;Testvoid queryUser() {ListUser users userMapper.selectList(null);for (User user : users) {System.out.println(user);}}} 7、日志输出
为了便于学习我们可以指定日志的实现StdOutImpl来处理
# 指定日志输出
mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl然后操作数据库的时候就可以看到对应的日志信息了
手动两级缓存架构实战
Configuration
public class CaffeineConfig {Beanpublic CacheString,Object caffeineCache(){return Caffeine.newBuilder().initialCapacity(128)//初始大小.maximumSize(1024)//最大数量.expireAfterWrite(15, TimeUnit.SECONDS)//过期时间 15S.build();}
}//CaffeineRedis两级缓存查询public User query1_2(long userId){String key user-userId;User user (User) cache.get(key,k - {//先查询 Redis 2级缓存Object obj redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)) {log.info(get data from redis:key);return obj;}// Redis没有则查询 DBMySQLUser user2 userMapper.selectById(userId);log.info(get data from database:userId);redisTemplate.opsForValue().set(key, user2, 30, TimeUnit.SECONDS);return user2;});return user;}在 Cache 的 get 方法中会先从Caffeine缓存中进行查找如果找到缓存的值那么直接返回。没有的话查找 RedisRedis 再不命中则查询数据库最后都同步到Caffeine的缓存中。
通过案例演示也可以达到对应的效果。
另外修改、删除的代码可以看代码案例
注解方式两级缓存架构实战
在 spring中提供了 CacheManager 接口和对应的注解
Cacheable根据键从缓存中取值如果缓存存在那么获取缓存成功之后直接返回这个缓存的结果。如果缓存不存在那么执行方法并将结果放入缓存中。CachePut不管之前的键对应的缓存是否存在都执行方法并将结果强制放入缓存。CacheEvict执行完方法后会移除掉缓存中的数据。
使用注解就需要配置 spring 中的 CacheManager 在这个CaffeineConfig类中 Beanpublic CacheManager cacheManager(){CaffeineCacheManager cacheManagernew CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(128).maximumSize(1024).expireAfterWrite(15, TimeUnit.SECONDS));return cacheManager;}EnableCaching
在启动类上再添加上 EnableCaching 注解 在UserService类对应的方法上添加 Cacheable 注解 //CaffeineRedis两级缓存查询-- 使用注解Cacheable(value user, key #userId)public User query2_2(long userId){String key user-userId;//先查询 Redis 2级缓存Object obj redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)) {log.info(get data from redis:key);return (User)obj;}// Redis没有则查询 DBMySQLUser user userMapper.selectById(userId);log.info(get data from database:userId);redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);return user;}然后就可以达到类似的效果。
Cacheable 注解的属性
参数解释col3key缓存的key可以为空如果指定要按照SpEL表达式编写如不指定则按照方法所有参数组合Cacheable(value”testcache”, key”#userName”)value缓存的名称在 spring 配置文件中定义必须指定至少一个例如:Cacheable(value”mycache”)condition缓存的条件可以为空使用 SpEL 编写返回 true 或者 false只有为 true 才进行缓存Cacheable(value”testcache”,condition”#userName.length()2”)methodName当前方法名#root.methodNamemethod当前方法#root.method.nametarget当前被调用的对象#root.targettargetClass当前被调用的对象的class#root.targetClassargs当前方法参数组成的数组#root.args[0]caches当前被调用的方法使用的Cache#root.caches[0].name
这里有一个condition属性指定发生的条件
示例表示只有当userId为偶数时才会进行缓存 //只有当userId为偶数时才会进行缓存Cacheable(value user, key #userId, condition#userId%20)public User query2_3(long userId){String key user-userId;//先查询 Redis 2级缓存Object obj redisTemplate.opsForValue().get(key);if (Objects.nonNull(obj)) {log.info(get data from redis:key);return (User)obj;}// Redis没有则查询 DBMySQLUser user userMapper.selectById(userId);log.info(get data from database:userId);redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);return user;}CacheEvict
CacheEvict是用来标注在需要清除缓存元素的方法或类上的。
当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的对应Cache的名称key表示需要清除的是哪个key如未指定则会使用默认策略生成的keycondition表示清除操作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。 //清除缓存(所有的元素)CacheEvict(valueuser, key #userId,allEntriestrue)public void deleteAll(long userId) {System.out.println(userId);}//beforeInvocationtrue在调用该方法之前清除缓存中的指定元素CacheEvict(valueuser, key #userId,beforeInvocationtrue)public void delete(long userId) {System.out.println(userId);}自定义注解实现两级缓存架构实战
首先定义一个注解用于添加在需要操作缓存的方法上
Target(ElementType.METHOD)
Retention(RetentionPolicy.RUNTIME)
Documented
public interface DoubleCache {String cacheName();String key(); //支持springEl表达式long l2TimeOut() default 120;CacheType type() default CacheType.FULL;
}l2TimeOut 为可以设置的二级缓存 Redis 的过期时间
CacheType 是一个枚举类型的变量表示操作缓存的类型
public enum CacheType {FULL, //存取PUT, //只存DELETE //删除
}
从前面我们知道key要支持 springEl 表达式写一个ElParser的方法使用表达式解析器解析参数
public class ElParser {public static String parse(String elString, TreeMapString,Object map){elStringString.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;}
}package com.msb.caffeine.cache;import com.github.benmanes.caffeine.cache.Cache;
import lombok.AllArgsConstructor;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;Slf4j
Component
Aspect
AllArgsConstructor
public class CacheAspect {private final Cache cache;private final RedisTemplate redisTemplate;Pointcut(annotation(com.msb.caffeine.cache.DoubleCache))public void cacheAspect() {}Around(cacheAspect())public Object doAround(ProceedingJoinPoint point) throws Throwable {MethodSignature signature (MethodSignature) point.getSignature();Method method signature.getMethod();//拼接解析springEl表达式的mapString[] paramNames signature.getParameterNames();Object[] args point.getArgs();TreeMapString, Object treeMap new TreeMap();for (int i 0; i paramNames.length; i) {treeMap.put(paramNames[i],args[i]);}DoubleCache annotation method.getAnnotation(DoubleCache.class);String elResult ElParser.parse(annotation.key(), treeMap);String realKey annotation.cacheName() : elResult;//强制更新if (annotation.type() CacheType.PUT){Object object point.proceed();redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);cache.put(realKey, object);return object;}//删除else if (annotation.type() CacheType.DELETE){redisTemplate.delete(realKey);cache.invalidate(realKey);return point.proceed();}//读写查询CaffeineObject caffeineCache cache.getIfPresent(realKey);if (Objects.nonNull(caffeineCache)) {log.info(get data from caffeine);return caffeineCache;}//查询RedisObject redisCache redisTemplate.opsForValue().get(realKey);if (Objects.nonNull(redisCache)) {log.info(get data from redis);cache.put(realKey, redisCache);return redisCache;}log.info(get data from database);Object object point.proceed();if (Objects.nonNull(object)){//写入RedisredisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);//写入Caffeinecache.put(realKey, object);}return object;}
}
切面中主要做了下面几件工作
通过方法的参数解析注解中 key 的 springEl 表达式组装真正缓存的 key。根据操作缓存的类型分别处理存取、只存、删除缓存操作。删除和强制更新缓存的操作都需要执行原方法并进行相应的缓存删除或更新操作。存取操作前先检查缓存中是否有数据如果有则直接返回没有则执行原方法并将结果存入缓存。
然后使用的话就非常方便了代码中只保留原有业务代码再添加上我们自定义的注解就可以了 DoubleCache(cacheName user, key #userId,type CacheType.FULL)public User query3(Long userId) {User user userMapper.selectById(userId);return user;}DoubleCache(cacheName user,key #user.userId,type CacheType.PUT)public int update3(User user) {return userMapper.updateById(user);}DoubleCache(cacheName user,key #user.userId,type CacheType.DELETE)public void deleteOrder(User user) {userMapper.deleteById(user);}两级缓存架构的缓存一致性问题
就是如果一个应用修改了缓存另外一个应用的caffeine缓存是没有办法感知的所以这里就会有缓存的一致性问题 解决方案也很简单就是在Redis中做一个发布和订阅。
遇到修改缓存的处理需要向对应的频道发布一条消息然后应用同步监听这条消息有消息则需要删除本地的Caffeine缓存。
核心代码如下