前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Redis+Caffeine 太强了!二级缓存可以这样实现!

Redis+Caffeine 太强了!二级缓存可以这样实现!

原创
作者头像
程序员蜗牛
发布于 2024-02-26 14:15:16
发布于 2024-02-26 14:15:16
1.4K00
代码可运行
举报
运行总次数:0
代码可运行

在实际的项目中,我们通常会将一些热点数据存储RedisMemCache这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库

在一些场景下可能还需要进一步配合本地缓存使用,例如Guava cacheCaffeine,从而再次提升程序的响应速度与服务性能。

于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构。

二级缓存的访问流程可以用下面这张图来表示:

优点与问题

准备工作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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的连接信息:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

我们使用RedisTemplate来对redis进行读写操作。

下面在单机环境下,将按照对业务侵入性的不同程度,分三个版本来实现两级缓存的使用。

V1.0版本

在使用Cache前,需要先配置一下相关参数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<String,Object> caffeineCache(){
        return Caffeine.newBuilder()
                .initialCapacity(128)//初始大小
                .maximumSize(1024)//最大数量
                .expireAfterWrite(60, TimeUnit.SECONDS)//过期时间
                .build();
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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进行改造,在执行正常业务外再加上操作两级缓存的代码,先看改造后的查询操作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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;
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);
}

看一下下面图中接口的调用、以及缓存的刷新过程。可以看到在更新数据后,同步刷新了缓存中的内容,再之后的访问接口时不查询数据库,也可以拿到正确的结果:

最后再来看一下删除操作,在删除数据的同时,手动移除ReidsCaffeine中的缓存:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void deleteOrder(Long id) {
    log.info("delete order");
    orderMapper.deleteById(id);
    String key= CacheConstant.ORDER + id;
    redisTemplate.delete(key);
    cache.invalidate(key);
}

我们在删除某个缓存后,再次调用之前的查询接口时,又会出现重新查询数据库的情况:

V2.0版本

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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;
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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;
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#参数名
#参数对象.属性名
#p参数对应下标
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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;
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@CacheEvict(cacheNames = "order",key = "#id")
public void deleteOrder(Long id) {
    log.info("delete order");
    orderMapper.deleteById(id);
    redisTemplate.delete(CacheConstant.ORDER + id);
}

V3.0版本

模仿spring通过注解管理缓存的方式,我们也可以选择自定义注解,然后在切面中处理缓存,从而将对业务代码的入侵降到最低。

首先定义一个注解,用于添加在需要操作缓存的方法上:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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隔离),l2TimeOut为可以设置的二级缓存Redis的过期时间,type是一个枚举类型的变量,表示操作缓存的类型,枚举类型定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum CacheType {
    FULL,   //存取
    PUT,    //只存
    DELETE  //删除
}

因为要使key支持springEl表达式,所以需要写一个方法,使用表达式解析器解析参数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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是将原方法的参数封装后的结果。简单进行一下测试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);
}

执行结果如下,可以看到支持按照参数名称、参数对象的属性名称读取,但是不支持按照参数下标读取,暂时留个小坑以后再处理。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
123.0
Hydra
null

至于Cache相关参数的配置,我们沿用V1版本中的配置即可。准备工作做完了,下面我们定义切面,在切面中操作Cache来读写Caffeine的缓存,操作RedisTemplate读写Redis缓存。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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;
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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);
}

到这里,基于切面操作缓存的改造就完成了,Service的代码也瞬间清爽了很多,让我们可以继续专注于业务逻辑处理,而不用费心去操作两级缓存了。

总结

本文按照对业务入侵的递减程度,依次介绍了三种管理两级缓存的方法。

本文中只是介绍了最基础的使用,实际中的并发问题、事务的回滚问题都需要考虑,还需要思考什么数据适合放在一级缓存、什么数据适合放在二级缓存等等的其他问题。

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
Zabbix与乐维监控对比分析(七)——网络功能篇
前面我们详细介绍了Zabbix与乐维监控在架构与性能、Agent管理、自动发现、权限管理、对象管理、告警管理、可视化及图形图表方面的对比分析,接下来我们将对二者网络功能方面进行对比分析。
乐维_lwops
2023/01/06
3560
Zabbix与乐维监控对比分析(七)——网络功能篇
Zabbix与乐维监控对比分析(八)——其他功能篇
前面我们详细介绍了Zabbix与乐维监控的架构与性能、Agent管理、自动发现、权限管理、对象管理、告警管理、可视化、图形图表及网络功能方面的对比分析,接下来我们将对二者其他功能进行对比分析。
乐维_lwops
2023/01/13
3440
Zabbix与乐维监控对比分析(八)——其他功能篇
Zabbix与乐维监控对比分析(五)——可视化篇
大家好,我是乐乐。前面我们详细介绍了Zabbix与乐维监控的架构与性能、Agent管理、自动发现、权限管理、对象管理、告警管理方面的对比分析,相信大家对二者的对比分析有了相对深入的了解,接下来我们将对二者的可视化功能进行对比分析。可视化是当代IT监控的一个创举,让IT监控很大程度摆脱枯燥烦杂的数据,使得监控过程变得更直观。
乐维_lwops
2022/12/23
8260
Zabbix与乐维监控对比分析(五)——可视化篇
Zabbix与乐维监控对比分析(四)——告警管理篇
在前面发布的Zabbix与乐维监控对比分析文章中,我们评析了二者在架构与性能、Agent管理、自动发现、权限管理、对象管理等方面的差异。接下来让我们一起看看二者在告警管理方面的差异。
乐维_lwops
2022/12/16
3690
Zabbix与乐维监控对比分析(四)——告警管理篇
乐维监控与Zabbix对比分析(一)——架构、性能
近年来,Zabbix凭借其近乎无所不能的监控及优越的性能一路高歌猛进,在开源监控领域独占鳌头;以下将对乐维与Zabbix的各项优劣势进行一一对比,本篇为二者架构、性能的对比,后续还将发布更多zabbix技术分享,大家可以持续关注。
乐维_lwops
2022/11/23
5570
乐维监控与Zabbix对比分析(一)——架构、性能
【Z投稿】Zabbix 5.0.0beta1体验- MySQL监控
Zabbix社区专家,他从事IT运维工作7年,不仅是cactifans作者,还是go语言爱好者,Devops实践者,使用zabbix6年,具有丰富的使用经验和二次开发经验。
Zabbix
2021/01/29
4970
【Z投稿】Zabbix 5.0.0beta1体验- MySQL监控
Zabbix与乐维监控对比分析(二)——Agent管理、自动发现、权限管理
上期我们详细介绍了Zabbix与乐维监控的架构与性能对比分析,透过架构与性能对比分析。本篇是Zabbix对比乐维监控专题系列文章之二——Agent管理、自动发现、权限管理篇。
乐维_lwops
2022/12/02
3640
Zabbix与乐维监控对比分析(二)——Agent管理、自动发现、权限管理
这 5 种常用运维监控工具都不会?你算啥运维人
运维监控工具千千万,仅开源的解决方案就有流量监控(MRTG、Cacti、SmokePing、Graphite 等)和性能告警(Nagios、Zabbix、Zenoss Core、Ganglia、OpenTSDB等)可供选择。
互联网老辛
2021/04/22
2.9K0
这 5 种常用运维监控工具都不会?你算啥运维人
专家专栏|使用Zabbix监控Ceph集群的三种方式
Zabbix运维工程师,熟悉Zabbix开源监控系统的架构。乐于分享Zabbix运维经验,个人公众号“运维开发故事”。
Zabbix
2021/03/04
9200
专家专栏|使用Zabbix监控Ceph集群的三种方式
zabbix监控面试题[通俗易懂]
agentd需要安装到被监控的主机上,它负责定期收集各项数据,并发送到zabbix server端,zabbix server将数据存储到数据库中,zabbix web根据数据在前端进行展现和绘图。这里agentd收集数据分为主动和被动两种模式:
全栈程序员站长
2022/07/01
1.6K0
zabbix监控-基本原理介绍
一、Linux下开源监控系统简单介绍 1)cacti:存储数据能力强,报警性能差 2)nagios:报警性能差,存储数据仅有简单的一段可以判断是否在合理范围内的数据长度,储存在内存中。比如,连续采样数据存储,有连续三次不在合理范围内的数据就报警 3)zabbix:结合上面两种工具的优点,又可以存储数据,又可以报警。 二、什么是Zabbix及其优缺点(对比Cacti和Nagios) Zabbix是一个基于Web界面提供分布式系统监视及网络监视功能的企业级开源解决方案。它能监视各种网络参数,保证服务器系统的安全
洗尽了浮华
2018/01/22
6K0
zabbix监控-基本原理介绍
专家专栏|Zabbix Agent2监控Docker
Zabbix运维工程师,熟悉Zabbix开源监控系统的架构。乐于分享Zabbix运维经验,个人公众号“运维开发故事”。
Zabbix
2021/01/29
7720
专家专栏|Zabbix Agent2监控Docker
长文|基于Zabbix的可观测性监控
本文整理自王小东在2022Zabbix峰会演讲分享。ppt可在公众号后台回复“ppt"。
Zabbix
2023/07/03
6320
案例|银行 Zabbix 监控架构分享
Zabbix 是一个基于 Web 界面提供分布式系统监视及网络监视功能的企业级开源解决方案。它能监视各种网络参数,保证服务器系统的安全运营,并提供灵活的通知机制以让系统管理员快速定位、解决存在的各种问题,借助Zabbix 可很轻松地减轻运维人员繁重的服务器管理任务,保证业务系统持续运行。其后端使用数据库存储监控配置和历史数据,可以非常方便地对接数据分析、报表定制等渠道,在前端开放了丰富的 RESTful API 供第三方平台调用,整体架构在当下的 DevOps 的趋势下显得非常亮眼。
Zabbix
2021/01/29
2K0
案例|银行 Zabbix 监控架构分享
Zabbix(1)-监控服务与zabbix介绍
对于传统意义的监控来说,监控系统属于安防系统中应用最多的系统之一,主要是用来监控异常和不好的事情发生,或者提供事件发生过程的记录和事后分析等功能。如视频监控系统就是典型的监控系统,视频监控系统就从早期的 CCTV 发展到 DVR到目前已经发展为基于 IP 网络的视频监控 IPVS。
mikelLam
2022/10/31
5830
Zabbix(1)-监控服务与zabbix介绍
02 . Zabbix配置监控项及聚合图形
接下来我们用浏览器或者elinks访问一下nginx,产生一些数据,然后去zabbix上查看变化
iginkgo18
2020/09/27
1.1K0
02 . Zabbix配置监控项及聚合图形
Zabbix Meetup上海站回顾
首场线下Zabbix Meetup 上海站于4月23日圆满举办,现场有 130 +位嘉宾参加,场面热烈。感谢本次活动的赞助商杭州网银互联科技股份有限公司大力支持。
Zabbix
2021/05/24
8790
Zabbix Meetup上海站回顾
zabbix 监控系统_供天
前言:作为一个运维,需要会使用监控系统查看服务器状态以及网站流量指标,利用监控系统的数据去了解上线发布的结果,和网站的健康状态。
全栈程序员站长
2022/11/19
1.7K0
zabbix 监控系统_供天
徒手教你制作运维监控大屏
  公司业务的不断发展,紧接而来的是业务种类的增加、服务器数量的增长、网络环境的越发复杂以及发布更加频繁,从而不可避免地带来了线上事故的增多,因此需要对服务器到应用的全方位监控,提前预警。
欢醉
2020/06/19
3.5K0
01 . Zabbix简介原理及部署
1> 数据采集: 可用性和性能检测,自动发现,支持agent,snmp,JMX,telnet等多种采集方式,支持主动和被动数据传输、支持用户自定义插件,自定义间隔收集数据.
iginkgo18
2020/09/27
7340
01 . Zabbix简介原理及部署
推荐阅读
相关推荐
Zabbix与乐维监控对比分析(七)——网络功能篇
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档