Caffeine+Redis两级缓存架构

打印 上一主题 下一主题

主题 1016|帖子 1016|积分 3048

Caffeine+Redis两级缓存架构

在高性能的服务项目中,我们一般会将一些热点数据存储到 Redis这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时,也能低落数据库的压力。
但是在一些场景下单纯使用 Redis 的分布式缓存不能满足高性能的要求,所以还需要参加使用当地缓存Caffeine,从而再次提升步伐的相应速度与服务性能。于是,就产生了使用当地缓存(Caffeine)作为一级缓存,再加上分布式缓存(Redis)作为二级缓存的两级缓存架构。

两级缓存架构优缺点

优点:


  • 一级缓存基于应用的内存,访问速度非常快,对于一些变动频率低、实时性要求低的数据,可以放在当地缓存中,提升访问速度;
  • 使用一级缓存可以或许镌汰和 Redis 的二级缓存的远程数据交互,镌汰网络 I/O 开销,低落这一过程中在网络通信上的耗时。
缺点:


  • 数据同等性标题:两级缓存与数据库的数据要保持同等,一旦数据发生了修改,在修改数据库的同时,一级缓存、二级缓存应该同步更新。
  • 分布式多应用情况下:一级缓存之间也会存在同等性标题,当一个节点下的当地缓存修改后,需要关照其他节点也刷新当地一级缓存中的数据,否则会出现读取到过期数据的情况。
  • 缓存的过期时间、过期策略以及多线程的标题
Caffeine+Redis两级缓存架构实战

1、准备表结构和数据

准备如下的表结构和相干数据
  1. DROP TABLE IF EXISTS user;
  2. CREATE TABLE user
  3. (
  4.    id BIGINT(20) NOT NULL COMMENT '主键ID',
  5.    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
  6.    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
  7.    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
  8.    PRIMARY KEY (id)
  9. );
复制代码
插入对应的相干数据
  1. DELETE FROM user;
  2. INSERT INTO user (id, name, age, email) VALUES
  3. (1, 'Jone', 18, 'test1@baomidou.com'),
  4. (2, 'Jack', 20, 'test2@baomidou.com'),
  5. (3, 'Tom', 28, 'test3@baomidou.com'),
  6. (4, 'Sandy', 21, 'test4@baomidou.com'),
  7. (5, 'Billie', 24, 'test5@baomidou.com');
复制代码
2、创建项目

创建一个SpringBoot项目,然后引入相干的依靠,首先是父依靠
  1. <parent>
  2.        <groupId>org.springframework.boot</groupId>
  3.        <artifactId>spring-boot-starter-parent</artifactId>
  4.        <version>2.6.6</version>
  5.        <relativePath/> <!-- lookup parent from repository -->
  6.    </parent>
复制代码
详细的其他的依靠
  1. <!-- spring-boot-starter-web 的依赖 -->
  2.        <dependency>
  3.            <groupId>org.springframework.boot</groupId>
  4.            <artifactId>spring-boot-starter-web</artifactId>
  5.        </dependency>
  6.        <dependency>
  7.            <groupId>org.springframework.boot</groupId>
  8.            <artifactId>spring-boot-starter-test</artifactId>
  9.            <scope>test</scope>
  10.        </dependency>
  11.        <!-- 引入MyBatisPlus的依赖 -->
  12.        <dependency>
  13.            <groupId>com.baomidou</groupId>
  14.            <artifactId>mybatis-plus-boot-starter</artifactId>
  15.            <version>3.5.1</version>
  16.        </dependency>
  17.        <!-- 数据库使用MySQL数据库 -->
  18.        <dependency>
  19.            <groupId>mysql</groupId>
  20.            <artifactId>mysql-connector-java</artifactId>
  21.        </dependency>
  22.        <!-- 数据库连接池 Druid -->
  23.        <dependency>
  24.            <groupId>com.alibaba</groupId>
  25.            <artifactId>druid</artifactId>
  26.            <version>1.1.14</version>
  27.        </dependency>
  28.        <!-- lombok依赖 -->
  29.        <dependency>
  30.            <groupId>org.projectlombok</groupId>
  31.            <artifactId>lombok</artifactId>
  32.        </dependency>
复制代码
3、配置信息

然后我们需要在application.properties中配置数据源的相干信息
  1. spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
  2. spring.datasource.url=jdbc:mysql://localhost:3306/mp?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
  3. spring.datasource.username=root
  4. spring.datasource.password=123456
  5. spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
复制代码
然后我们需要在SpringBoot项目标启动类上配置Mapper接口的扫描路径

4、添加User实体

添加user的实体类
  1. @ToString
  2. @Data
  3. public class User {
  4.    private Long id;
  5.    private String name;
  6.    private Integer age;
  7.    private String email;
  8. }
复制代码
5、创建Mapper接口

在MyBatisPlus中的Mapper接口需要继承BaseMapper.
  1. /**
  2. * MyBatisPlus中的Mapper接口继承自BaseMapper
  3. */
  4. public interface UserMapper extends BaseMapper<User> {
  5. }
复制代码
6、测试利用

然后来完成对User表中数据的查询利用
  1. @SpringBootTest
  2. class MpDemo01ApplicationTests {
  3.    @Autowired
  4.    private UserMapper userMapper;
  5.    @Test
  6.    void queryUser() {
  7.        List<User> users = userMapper.selectList(null);
  8.        for (User user : users) {
  9.            System.out.println(user);
  10.        }
  11.    }
  12. }
复制代码

7、日记输出

为了便于学习我们可以指定日记的实现StdOutImpl来处理
  1. # 指定日志输出
  2. mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
复制代码

然后利用数据库的时间就可以看到对应的日记信息了:
手动两级缓存架构实战

  1. @Configuration
  2. public class CaffeineConfig {
  3.     @Bean
  4.     public Cache<String,Object> caffeineCache(){
  5.         return Caffeine.newBuilder()
  6.                 .initialCapacity(128)//初始大小
  7.                 .maximumSize(1024)//最大数量
  8.                 .expireAfterWrite(15, TimeUnit.SECONDS)//过期时间 15S
  9.                 .build();
  10.     }
  11. }
复制代码
  1. //Caffeine+Redis两级缓存查询
  2.     public User query1_2(long userId){
  3.         String key = "user-"+userId;
  4.         User user = (User) cache.get(key,
  5.                 k -> {
  6.                     //先查询 Redis  (2级缓存)
  7.                     Object obj = redisTemplate.opsForValue().get(key);
  8.                     if (Objects.nonNull(obj)) {
  9.                         log.info("get data from redis:"+key);
  10.                         return obj;
  11.                     }
  12.                     // Redis没有则查询 DB(MySQL)
  13.                     User user2 = userMapper.selectById(userId);
  14.                     log.info("get data from database:"+userId);
  15.                     redisTemplate.opsForValue().set(key, user2, 30, TimeUnit.SECONDS);
  16.                     return user2;
  17.                 });
  18.         return user;
  19.     }
复制代码
在 Cache 的 get 方法中,会先从Caffeine缓存中进行查找,如果找到缓存的值那么直接返回。没有的话查找 Redis,Redis 再不命中则查询数据库,末了都同步到Caffeine的缓存中。
通过案例演示也可以达到对应的效果。
另外修改、删除的代码可以看代码案例!
注解方式两级缓存架构实战

在 spring中,提供了 CacheManager 接口和对应的注解


  • @Cacheable:根据键从缓存中取值,如果缓存存在,那么获取缓存成功之后,直接返回这个缓存的结果。如果缓存不存在,那么实行方法,并将结果放入缓存中。
  • @CachePut:不管之前的键对应的缓存是否存在,都实行方法,并将结果强制放入缓存。
  • @CacheEvict:实行完方法后,会移除掉缓存中的数据。
使用注解,就需要配置 spring 中的 CacheManager ,在这个CaffeineConfig类中
  1. @Bean
  2.     public CacheManager cacheManager(){
  3.         CaffeineCacheManager cacheManager=new CaffeineCacheManager();
  4.         cacheManager.setCaffeine(Caffeine.newBuilder()
  5.                 .initialCapacity(128)
  6.                 .maximumSize(1024)
  7.                 .expireAfterWrite(15, TimeUnit.SECONDS));
  8.         return cacheManager;
  9.     }
复制代码
EnableCaching

在启动类上再添加上 @EnableCaching 注解

在UserService类对应的方法上添加 @Cacheable 注解
  1. //Caffeine+Redis两级缓存查询-- 使用注解
  2.     @Cacheable(value = "user", key = "#userId")
  3.     public User query2_2(long userId){
  4.         String key = "user-"+userId;
  5.         //先查询 Redis  (2级缓存)
  6.         Object obj = redisTemplate.opsForValue().get(key);
  7.         if (Objects.nonNull(obj)) {
  8.             log.info("get data from redis:"+key);
  9.             return (User)obj;
  10.         }
  11.         // Redis没有则查询 DB(MySQL)
  12.         User user = userMapper.selectById(userId);
  13.         log.info("get data from database:"+userId);
  14.         redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);
  15.         return user;
  16.     }
复制代码
然后就可以达到类似的效果。
@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为偶数时才会进行缓存
  1. //只有当userId为偶数时才会进行缓存
  2.     @Cacheable(value = "user", key = "#userId", condition="#userId%2==0")
  3.     public User query2_3(long userId){
  4.         String key = "user-"+userId;
  5.         //先查询 Redis  (2级缓存)
  6.         Object obj = redisTemplate.opsForValue().get(key);
  7.         if (Objects.nonNull(obj)) {
  8.             log.info("get data from redis:"+key);
  9.             return (User)obj;
  10.         }
  11.         // Redis没有则查询 DB(MySQL)
  12.         User user = userMapper.selectById(userId);
  13.         log.info("get data from database:"+userId);
  14.         redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);
  15.         return user;
  16.     }
复制代码
CacheEvict

@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。
当标记在一个类上时表示此中所有的方法的实行都会触发缓存的清除利用。
@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。此中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除利用是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略天生的key;condition表示清除利用发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。
  1. //清除缓存(所有的元素)
  2.     @CacheEvict(value="user", key = "#userId",allEntries=true)
  3.     public void deleteAll(long userId) {
  4.         System.out.println(userId);
  5.     }
  6.     //beforeInvocation=true:在调用该方法之前清除缓存中的指定元素
  7.     @CacheEvict(value="user", key = "#userId",beforeInvocation=true)
  8.     public void delete(long userId) {
  9.         System.out.println(userId);
  10.     }
复制代码
自定义注解实现两级缓存架构实战

首先定义一个注解,用于添加在需要利用缓存的方法上:
  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface DoubleCache {
  5.     String cacheName();
  6.     String key(); //支持springEl表达式
  7.     long l2TimeOut() default 120;
  8.     CacheType type() default CacheType.FULL;
  9. }
复制代码
l2TimeOut 为可以设置的二级缓存 Redis 的过期时间
CacheType 是一个枚举类型的变量,表示利用缓存的类型
  1. public enum CacheType {
  2.     FULL,   //存取
  3.     PUT,    //只存
  4.     DELETE  //删除
  5. }
复制代码
从前面我们知道,key要支持 springEl 表达式,写一个ElParser的方法,使用表达式分析器分析参数:
  1. public class ElParser {
  2.     public static String parse(String elString, TreeMap<String,Object> map){
  3.         elString=String.format("#{%s}",elString);
  4.         //创建表达式解析器
  5.         ExpressionParser parser = new SpelExpressionParser();
  6.         //通过evaluationContext.setVariable可以在上下文中设定变量。
  7.         EvaluationContext context = new StandardEvaluationContext();
  8.         map.entrySet().forEach(entry->
  9.                 context.setVariable(entry.getKey(),entry.getValue())
  10.         );
  11.         //解析表达式
  12.         Expression expression = parser.parseExpression(elString, new TemplateParserContext());
  13.         //使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
  14.         String value = expression.getValue(context, String.class);
  15.         return value;
  16.     }
  17. }
复制代码
  1. package com.msb.caffeine.cache;
  2. import com.github.benmanes.caffeine.cache.Cache;
  3. import lombok.AllArgsConstructor;
  4. import org.aspectj.lang.ProceedingJoinPoint;
  5. import org.aspectj.lang.annotation.Around;
  6. import org.aspectj.lang.annotation.Aspect;
  7. import org.aspectj.lang.annotation.Pointcut;
  8. import org.aspectj.lang.reflect.MethodSignature;
  9. import org.springframework.data.redis.core.RedisTemplate;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.springframework.stereotype.Component;
  12. import java.lang.reflect.Method;
  13. import java.util.Objects;
  14. import java.util.TreeMap;
  15. import java.util.concurrent.TimeUnit;
  16. @Slf4j
  17. @Component
  18. @Aspect
  19. @AllArgsConstructor
  20. public class CacheAspect {
  21.     private final Cache cache;
  22.     private final RedisTemplate redisTemplate;
  23.     @Pointcut("@annotation(com.msb.caffeine.cache.DoubleCache)")
  24.     public void cacheAspect() {
  25.     }
  26.     @Around("cacheAspect()")
  27.     public Object doAround(ProceedingJoinPoint point) throws Throwable {
  28.         MethodSignature signature = (MethodSignature) point.getSignature();
  29.         Method method = signature.getMethod();
  30.         //拼接解析springEl表达式的map
  31.         String[] paramNames = signature.getParameterNames();
  32.         Object[] args = point.getArgs();
  33.         TreeMap<String, Object> treeMap = new TreeMap<>();
  34.         for (int i = 0; i < paramNames.length; i++) {
  35.             treeMap.put(paramNames[i],args[i]);
  36.         }
  37.         DoubleCache annotation = method.getAnnotation(DoubleCache.class);
  38.         String elResult = ElParser.parse(annotation.key(), treeMap);
  39.         String realKey = annotation.cacheName() + ":" + elResult;
  40.         //强制更新
  41.         if (annotation.type()== CacheType.PUT){
  42.             Object object = point.proceed();
  43.             redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
  44.             cache.put(realKey, object);
  45.             return object;
  46.         }
  47.         //删除
  48.         else if (annotation.type()== CacheType.DELETE){
  49.             redisTemplate.delete(realKey);
  50.             cache.invalidate(realKey);
  51.             return point.proceed();
  52.         }
  53.         //读写,查询Caffeine
  54.         Object caffeineCache = cache.getIfPresent(realKey);
  55.         if (Objects.nonNull(caffeineCache)) {
  56.             log.info("get data from caffeine");
  57.             return caffeineCache;
  58.         }
  59.         //查询Redis
  60.         Object redisCache = redisTemplate.opsForValue().get(realKey);
  61.         if (Objects.nonNull(redisCache)) {
  62.             log.info("get data from redis");
  63.             cache.put(realKey, redisCache);
  64.             return redisCache;
  65.         }
  66.         log.info("get data from database");
  67.         Object object = point.proceed();
  68.         if (Objects.nonNull(object)){
  69.             //写入Redis
  70.             redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
  71.             //写入Caffeine
  72.             cache.put(realKey, object);
  73.         }
  74.         return object;
  75.     }
  76. }
复制代码
切面中重要做了下面几件工作:


  • 通过方法的参数,分析注解中 key 的 springEl 表达式,组装真正缓存的 key。
  • 根据利用缓存的类型,分别处理存取、只存、删除缓存利用。
  • 删除和强制更新缓存的利用,都需要实行原方法,并进行相应的缓存删除或更新利用。
  • 存取利用前,先查抄缓存中是否有数据,如果有则直接返回,没有则实行原方法,并将结果存入缓存。
然后使用的话就非常方便了,代码中只保留原有业务代码,再添加上我们自定义的注解就可以了:
  1.     @DoubleCache(cacheName = "user", key = "#userId",
  2.             type = CacheType.FULL)
  3.     public User query3(Long userId) {
  4.         User user = userMapper.selectById(userId);
  5.         return user;
  6.     }
  7.     @DoubleCache(cacheName = "user",key = "#user.userId",
  8.             type = CacheType.PUT)
  9.     public int update3(User user) {
  10.         return userMapper.updateById(user);
  11.     }
  12.     @DoubleCache(cacheName = "user",key = "#user.userId",
  13.             type = CacheType.DELETE)
  14.     public void deleteOrder(User user) {
  15.         userMapper.deleteById(user);
  16.     }
复制代码
两级缓存架构的缓存同等性标题

就是如果一个应用修改了缓存,另外一个应用的caffeine缓存是没有办法感知的,所以这里就会有缓存的同等性标题

解决方案也很简朴,就是在Redis中做一个发布和订阅。
遇到修改缓存的处理,需要向对应的频道发布一条消息,然后应用同步监听这条消息,有消息则需要删除当地的Caffeine缓存。
焦点代码如下:

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

吴旭华

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表