在高功用的服务架构设计中,缓存是一个不可或缺的环节。在实际的项目中,咱们通常会将一些热点数据存储到RedisMemCache这类缓存中间件中,只要当缓存的拜访没有命中时再查询数据库。在提高拜访速度的一起,也能下降接口英文数据库的压力。

随着不断的开展,这一架构也产生了改进,在一些场景下可能单纯运用Redis类的长途缓存现已不够了,还需求进一步合作本地缓存运用,例如Guava cacheCaffeine,然后再次提高程序的响应速度与服务功用。所以,就产生了运用本地缓存作为一级缓存,再加上长途缓存作为二级缓存的两级缓存架构。

接口测试用例设计先不考虑并发等复杂问题的状况下,两级缓存的拜访接口自动化流程能够用下面这张图来表示:

Redis+Caffeine两级缓存,让拜访速度纵享丝滑

优点与问题

那么,运用两级缓存比较单纯运用长途缓存,具有什么优势呢?

  • 本地缓存根据本地环境的内存,拜访速度非常快,对于一些改变频率低、实时性要求低的数据,能够放在本地缓存中,提接口测试用例设计高拜访速度
  • 运用本接口地缓存能够削减和Redis类的长途缓存间的数据交互,削减网络I/O开支,下降这一进程中在网络通讯事务性工作是什么意思的耗时

可是在设计中数据库软件,仍是要考虑一些问题的,例如数据一致性问题。首要,两级缓存与数据库的数据要保持一致,一旦数据发生了修正,在事务性工作修正数据库的一起,本地缓存、长途缓存应该同步更新。

另外,假如是分布事务文书式环境下,一级缓存之间也会存在一致性问题,当一个节点下的本地缓存修正后,需求通知其他节点也改写本地缓存中的数据,否则会呈通信技术现读取到过期数据的状况,这一问题能够经过类似于Redgiti轮胎is中的发布/订阅功用解决。

此外,缓存的过期时刻、过期战略以及多线程拜访的问题也都需求考虑进去,不过咱们今天暂时先不考虑这些问题,先看一下怎么简略高效的在代码中完结两级缓存的办理。

准备作业

在简略梳理了一下要面临的问题后,下面开始两级缓存的代码实战,咱们整合号称最强本地缓存的Caffeine作为一级缓存、功用之王giteeRedis作为二级缓存。首要建一个springboot项目,引入缓存要用到的相关的依赖:

<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-data-redis</artifactId>
</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>

application.yml中装备Redis的衔接信息:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 10000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

鄙人面的比如中,咱们将运用Redigit命令sTemplate来对redis进行读写操作,RedisTemplate运用前需求装备一下C数据库系统概论第五版课后答案onnectionFactory和序列化办法,这一进Git程比较简略就不贴出代码了,有需求本文全部示例代码的能通信技术giticomfort是什么轮胎文末获取

下面咱们在单机环境下,将依照对事务侵入性的不同程度,分三个版原本git命令完结两级缓存的运用。

V1.0版别

咱们giticomfort是什么轮胎能够经过手动操作Caffeine中的Cache目标来缓存数据,它是一个类似Ma接口是什么p的数据结构,以key作为索引,value存储数据。在运用Cache前,需求先装备一下相关参数:

@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<String,Object> caffeineCache(){
        return Caffeine.newBuilder()
                .initialCapacity(128)//初始巨细
                .maximumSize(1024)//最大数量
                .expireAfterWrite(60, TimeUnit.SECONDS)//过期时刻
                .build();
    }
}

简略解释一下Cache相关的几个参数的意义:

  • initi通信工程专业alCapacity:初始缓存空巨细
  • maximumSize:缓存的最大数量,设置这个值能够防止呈现内存溢通信人家园
  • expireAfterWrite:指定缓存的过期时刻,是最终一次写操作后的一个时刻,这儿

此外,缓数据库系统的核心是存的过期战略也能够经过通信地址expireAfterAccessrefre接口和抽象类的区别shAgit教程fterWrite指定。

在创立完结Ca事务隔离级别che后,咱们就能够在事务代码中接口文档注入并运用它了。在没有运用任何缓存前事务所所长的委托任务,一个只要简略的Se通信地址是写什么地址rvice数据库原理代码是下面这样的,只要crud操作:

@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final OrderMapper orderMapper;
    @Override
    public Order getOrderById(Long id) {		
        Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
              .eq(Order::getId, id));				
        return order;
    }
    @Override
    public void updateOrder(Order order) {      
        orderMapper.updateById(order);
    }
    @Override
    public void deleteOrder(Long id) {
        orderMapper.deleteById(id);
    }
}

接下来,对上面的OrderService进行改造,在履git教程行正常事务外再加上操作两级缓存的代码,先看改造后的查询操作:

public Order getOrderById(Long id) {
    String key = CacheConstant.ORDER + id;
    Order order = (Order) cache.get(key,
            k -> {
                //先查询 Redis
                Object obj = redisTemplate.opsForValue().get(k);
                if (Objects.nonNull(obj)) {
                    log.info("get data from redis");
                    return obj;
                }
                // Redis没有则查询 DB
                log.info("get data from database");
                Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
                        .eq(Order::getId, id));
                redisTemplate.opsForValue().set(k, myOrder, 120, TimeUnit.SECONDS);
                return myOrder;
            });
    return order;
}

Cacheget办法中,会先从缓存中进行查找,假如找到缓存的值那么直接回来。假如没有找到则履行后面的办法,并把成果加入到缓存中。

因此上面的逻辑便是先查找Caffeine中的缓存,没有的话查找RedisRedis再不命中则查询数据库,写入Redis缓存的操作需求手动写入,而Caffeine的写事务的四个特性入由get办法自己完结。

在上面的比如中,设置Caffeine的过期时刻为60秒,而Redis的过期时刻事务隔离级别为120秒,下面进行测验,首要看第一次接口调用时,进行了数据库的查询:

Redis+Caffeine两级缓存,让拜访速度纵享丝滑

而在之后60秒内拜访接口时,都没有打印打任何sql或自界说的日志内容,阐明接口没有查询Redis或数据通信库,直接从Caffeine中读取了缓存。

等到距离第一事务性工作是什么意思次调用接口进行缓存的60秒后,再次调用接口:

Redis+Caffeine两级缓存,让拜访速度纵享丝滑

能够看到这时从Redis中读取了数据,由于这时Caffeine中的缓存现已过期了,可是Redis中的缓存没有过期依然可用。

下面再来看一giti下修正操作,代码在原先的基础上添加了手动修正Re数据库系统概论disCaffeine缓存的逻辑:

public void updateOrder(Order order) {
    log.info("update order data");
    String key=CacheConstant.ORDER + order.getId();
    orderMapper.updateById(order);
    //修正 Redis
    redisTemplate.opsForValue().set(key,order,120, TimeUnit.SECONDS);
    // 修正本地缓存
    cache.put(key,order);
}

看一下下面图中接口的调用、以及缓存的改写进程。能够看到在更新数据后,同步改写了缓存中的内容,再之后的拜访接口时不查询数据库,也能够拿到正确的成接口英文果:

Redis+Caffeine两级缓存,让拜访速度纵享丝滑

最终再来看一下删去操作,在删去数据的一起,手动移除ReidsCaf通信工程专业feine中的缓存:

public void deleteOrder(Long id) {
    log.info("delete order");
    orderMapper.deleteById(id);
    String key= CacheConstant.ORDER + id;
    redisTemplate.delete(key);
    cache.invalidate(key);
}

咱们在删去某个缓存后,再次调用之前的查询接口时,又会呈现从头查询数据库的状况:

Redis+Caffeine两级缓存,让拜访速度纵享丝滑

简略的演示到此为止,能够看到上面这种运通信工程用缓存的办法,尽管看起来没什么大问题,可是对代码的侵略性比较强。在事务处理的进程中要由咱们频繁的操作两级缓存,会给开发人员带来很大担负。那么,有什么办法能够简化这一进程呢?咱们看看下一个版别。

V2.0版别

spring项目中,提供了CacheMa接口类型nager接口和一些注解,允许让咱们经过注解的办法来操作缓存。先来看一下常用几个注解阐明:

  • @Cacheable:依据键从缓存中取值,假如缓存存在,那么获取缓存成功之后,直接数据库设计回来这个缓存的成果。假如缓存不存在,那么履行办法,并将成果放入缓数据库存中。
  • @CachePut:不论之前的键对应的缓存是否存数据库原理在,都履行办法,并将成果强制放入缓接口英文
  • @CacheEvict:履行完办法后,会移除去缓存中的数据。

假如要运用上面这几个注解办理缓存的话,咱们就不需求装备V1版别中的那个类型为CacheBean了,而是需求装备spring中的Cache事务的四个特性Managiteeger的相关参数,详细参数的装备和之前一样:

@Configuration
public class CacheManagerConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager=new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(128)
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.SECONDS));
        return cacheManager;
    }
}

然后在发事务性工作是什么意思动类上再添加上@EnableCaching注解,就接口是什么能够在项目中根据数据库有哪几种注解来运事务局是什么单位Caffeine的缓数据库原理存支撑了。下面,再次对Service层代码进行改造。

首要,仍是改造查询办法github永久回家地址,在办法上添加@Cacheable注解:

@Cacheable(value = "order",key = "#id")
//@Cacheable(cacheNames = "order",key = "#p0")
public Order getOrderById(Long id) {
    String key= CacheConstant.ORDER + id;
    //先查询 Redis
    Object obj = redisTemplate.opsForValue().get(key);
    if (Objects.nonNull(obj)){
        log.info("get data from redis");
        return (Order) obj;
    }
    // Redis没有则查询 DB
    log.info("get data from database");
    Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
            .eq(Order::getId, id));
    redisTemplate.opsForValue().set(key,myOrder,120, TimeUnit.SECONDS);
    return myOrder;
}

@Cacheable注解的特点多达9个,好事务所在咱们日接口卡常运用时只需求装备两个常用的就能够了。其中valuecacheNames互为别名联系,表示当时办法的成果会被缓存在哪个Cache上,运用中经过cachegiticomfort是什么轮胎Name来对Cache进行阻隔,每个cacheName对应一个Cache完结。valuecacheNames能够是一个数组,绑定多个Cache

而另一个重要特点key,用来指定缓存办法的回来成果时对应的key,这个特点支撑运用SpringEL表达式。通常状况下,咱们能够运用下面几种办法作为key

#参数名
#参数目标.特点名
#p参数对应下标

在上面的代码中,咱们看到添加了@Cacheable注解后,在代码中只需求保存接口是什么原有的事务处理逻辑和操作Redis部分的代码即可,Caffe事务所ine部分的缓存就交给spring处理了。

下面,咱们再来改造一下更新办法,相同,运用@CachePut注解后移除去手动更新Cache的操作:数据库有哪几种

@CachePut(cacheNames = "order",key = "#order.id")
public Order updateOrder(Order order) {
    log.info("update order data");
    orderMapper.updateById(order);
    //修正 Redis
    redisTemplate.opsForValue().set(CacheConstant.ORDER + order.getId(),
            order, 120, TimeUnit.SECONDS);
    return order;
}

注意,这儿和V1版别的代码有一点区别,在之前的更新操作办法中,是没有回来值的void类型,可是这儿需求修正回来值的类型,否则会缓存git教程一个空目标到缓存中对应的key上。当下次履行查询操作时,会直接回来空目标给调用方,事务隔离级别而不会履行办法中查询数据库或Redis的操作。

最终,删去办法的改造就很简略了,运用@CacheEvict注解,办法中只需求删去Redis中的缓存即可:

@CacheEvict(cacheNames = "order",key = "#id")
public void deleteOrder(Long id) {
    log.info("delete order");
    orderMapper.deleteById(id);
    redisTemplate.delete(CacheConstant.ORDER + id);
}

能够看到,凭借s通信技术pring中的CacheManagerCache相关的注解,对V1版别通信人家园的代码经过改进后,能够把数据库查询语句数据库手动操作两级缓存的强侵略代码办法,改进为本地缓存交给spring办理,Redis缓存手动修正的半侵略办法。那么,还能进一步改造,使之成为对事务代码完全无侵略的办法吗?

V3.0版别

模仿springgithub中文官网网页经过注解办gitlab理缓存的办法,github咱们也能够选择自界说注解,然后在切面中处理缓存,然后将对事务代码的侵略降到最低。

首要界说一个注解,用于添加在需求操作缓存的办法上:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {
    String cacheName();
    String key();	//支撑springEl表达式
    long l2TimeOut() default 120;
    CacheType type() default CacheType.FULL;
}

咱们运用cacheName + key作为缓存的真实key(仅存在一接口测试用例设计Cache中,不做CacheName阻隔),l2Tim接口测试eOut为能够设置的二级缓存Redis的过期时刻,type是一个枚举类型的变量,表示操作缓存的类型,枚举类型界说如下:

public enum CacheType {
    FULL,   //存取
    PUT,    //只存
    DELETE  //删去
}

由于要使key支撑springEl表达式,所以需求写一个办法,运用表达式解析器解析参数接口测试用例设计

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;
}

参数中的elString对应的便是注解中key的值,map是将原办法的参数封装后的成果。简略进行一下测验:

public void test() {
    String elString="#order.money";
    String elString2="#user";
    String elString3="#p0";   
    TreeMap<String,Object> map=new TreeMap<>();
    Order order = new Order();
    order.setId(111L);
    order.setMoney(123D);
    map.put("order",order);
    map.put("user","Hydra");
    String val = parse(elString, map);
    String val2 = parse(elString2, map);
    String val3 = parse(elString3, map);
    System.out.println(val);
    System.out.println(val2);
    System.out.println(val3);
}

履行成果如下,能够看到支撑依照参数称号事务性工作是什么意思、参数目标的特点称号读取,可是不支撑依照参数下标读取,暂时留个小坑今后再处理。

123.0
Hydra
null

至于Cache相关参数的装备,咱们沿接口和抽象类的区别袭V1版别中的装备即可。准备作业做完了,下面咱们界说切面,在切面中操作Cache来读写Ca事务所所长npcffeine的缓存,操作RedisTemplate读写Redis缓存。

@Slf4j @Component @Aspect
@AllArgsConstructor
public class CacheAspect {
    private final Cache cache;
    private final RedisTemplate redisTemplate;
    @Pointcut("@annotation(com.cn.dc.annotation.DoubleCache)")
    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]);
        }
        DoubleCache annotation = method.getAnnotation(DoubleCache.class);
        String elResult = ElParser.parse(annotation.key(), treeMap);
        String realKey = annotation.cacheName() + CacheConstant.COLON + 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();
        }
        //读写,查询Caffeine
        Object caffeineCache = cache.getIfPresent(realKey);
        if (Objects.nonNull(caffeineCache)) {
            log.info("get data from caffeine");
            return caffeineCache;
        }
        //查询Redis
        Object 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)){
            //写入Redis
            redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
            //写入Caffeine
            cache.put(realKey, object);        
        }
        return object;
    }
}

切面中首要做了下面几件作业:

  • 经过办法的参数,解析注解中keyspringEl表达式,拼装真实缓存的key
  • 依据操作缓存的类型,分别处理存取、只存、删去缓存操作
  • 删去和强制更新缓存的操作,都需求履行原办法,并进行相应的缓存删去或更新操作
  • 存取操作前,先检查缓存中是否有数据,假如有则直接回来,没有则履行原办法,并将成果存入缓存

修正Service层代码,代码中只保存原有事务代码,再添加上咱们自界说的注解就能够了:

@DoubleCache(cacheName = "order", key = "#id",
        type = CacheType.FULL)
public Order getOrderById(Long id) {
    Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
            .eq(Order::getId, id));
    return myOrder;
}
@DoubleCache(cacheName = "order",key = "#order.id",
        type = CacheType.PUT)
public Order updateOrder(Order order) {
    orderMapper.updateById(order);
    return order;
}
@DoubleCache(cacheName = "order",key = "#id",
        type = CacheType.DELETE)
public void deleteOrder(Long id) {
    orderMapper.deleteById(id);
}

到这儿,根据切面操作缓存的改造就完结了,Servi数据库系统的核心是ce的代码也瞬间清新了很多,事务所是干什么的让咱们能够持续专注于事务逻辑处理,而不必操心去操作两级缓存github了。

总结

本文依照对事务侵略的递减程度,接口文档依次介绍了三种办理两级缓存的办法。至于在项目中是否需求运用二级缓存,需求考虑本身事务状况,假如Redis这种长途缓存现已能够满意你的事务需github中文官网网页求,那么就没有必要再运用本地缓存了。究竟实际运用起来远没有那么简略,本文中只是介绍了最基础的运用,实际中的并发问git命令题、事务的回滚问题都需求考虑,还需求考虑什么数据适合放在一级缓存、什么数据适合放在二级缓存等等的其他问题。

那么,git命令这次的共享就到这儿,我是Hydra,下期见。

本文的全部代码示例已传到了Hydra的Github上,有需求的同学能够自取数据库系统概论第五版课后答案~

Git地址:

github.com/通信技术trunks2008/…

作者简介,码农参上,一个酷爱共享的大众号,风趣、深化、直接,与你聊聊技术。


我正在参加事务文书技术社通信工程区创作者签约方案招募活动,点击链接报名投稿。