Guava本地缓存的使用过程

 更新时间:2025年01月03日 09:25:54   作者:肥肥肥柯  
文章介绍了使用Guava和Redis实现二级缓存的原因,以及如何通过Guava作为一级缓存,Redis作为二级缓存来减少数据库压力,提高缓存的可靠性,同时,通过一个具体示例说明了如何在微服务场景中使用Guava和Redis进行二级缓存,最后,总结了Guava的参数机制

Guava和Redis实现二级缓存

1、目的

本地缓存为什么不使用hashMap或者concurrentHashMap

concurrentHahMap和hashMap一样,都是长期存在的缓存,除非调用remove方法,否则缓存中的数据无法主动释放

仅使用Guava本地缓存会有什么问题?

作为API或者某种功能系统来用的话,无论单机/集群(集群其实就形成了近乎Guava副本的情况),Guava中的数据增长到后期不可估量的时候,Guava是支撑不住的;而微服务情况下没法全局缓存,如果数据量无限增长、不可控的话还是不建议使用。

仅使用Redis缓存会有什么问题?

大数量的情况下(热搜)容易引发缓存雪崩进而导致服务器雪崩。

综上,结合Guava、Redis,Guava作为一级缓存,Redis作为二级缓存,可以在减少数据库压力的基础上,将“缓存”这道防线做的更加可靠。 

2、二级缓存场景示例

公司有一款摄像头,放在了我家经常无人居住的豪宅了,摄像头包括异常人像报警、断电报警、信号异常报警、捕获画面动态报警等等多种报警功能类型(跳过其他设定,规定同类型的报警间隔5秒内仍存在则继续报警)。

现在有需求:我可以在平台上配置我想要报警的报警类型(不然我哪天周末回豪宅了它还一直报警到平台打扰我休息),当有我报警信息过来并且是匹配我配置的报警信息时,这个这条报警将推送到我平台首页。

//这里忽略报警系统代码,报警系统推送报警消息是通过RocketMQ实现
topic: alarm-camera
@Configuration
public class RocketMqConsumer {
    private static Logger logger = LogManager.getLogger(RocketMqConsumer.class);


    public void init() {
        pullAlarm();
        logger.warn("rocketmq拉取告警数据成功!");
    }

    /**
     * pullAlarm:拉取告警源数据。
     * @author liaokh
     * @since JDK 1.8
     */
    public static void pullAlarm() {
        new Thread() {
            public void run() {
                logger.warn("---------开始消费报警broker---------");
                try {
                    // 声明并初始化一个consumer
                    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-consumer-dev-camera" + "-alarm");

                    // 同样也要设置NameServer地址
                    consumer.setNamesrvAddr("我的RocketMQ服务器地址");

                    // 广播模式 当 Consumer 使用广播模式时,每条消息都会被 Consumer 集群内所有的 Consumer 实例消费一次。
                    consumer.setMessageModel(MessageModel.BROADCASTING);

                    // 这里设置的是一个consumer的消费策略
                    // CONSUME_FROM_LAST_OFFSET 默认策略,从该队列最尾开始消费,即跳过历史消息
                    // CONSUME_FROM_FIRST_OFFSET 从队列最开始开始消费,即历史消息(还储存在broker的)全部消费一遍
                    // CONSUME_FROM_TIMESTAMP 从某个时间点开始消费,和setConsumeTimestamp()配合使用,默认是半个小时以前
                    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

                    // 设置consumer所订阅的Topic和Tag,*代表全部的Tag
                    consumer.subscribe("alarm-camera", "*");

                    // 设置一个Listener,主要进行消息的逻辑处理
                    consumer.registerMessageListener(new MessageListenerConcurrently() {

                        @Override
                        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                                        ConsumeConcurrentlyContext context) {

                            for (MessageExt msg : msgs) {
                                try {
                                    String tag = msg.getTags();
                                    String alarmJson = new String(msg.getBody());
                                    logger.warn("收到alarm-camera数据:tag:" + tag + " alarmJson:" + alarmJson);
                                    CameraAlarmResp resultAlarm = new CameraAlarmResp();
                                    AlarmMQResp alarm = JSON.parseObject(alarmJson, AlarmMQResp.class);
                                    //查看当前告警类型是否在该用户配置的列表中
                                    //根据摄像头设备号获取用户信息
                                    Camera cameraEntity = Utils.getCameraById(alarm.getCameraId());  //这种核心数据也可以加载到缓存中
                                    UserAlarm userAlarm = Utils.getUserAlarm(cameraEntity.getUserId());
                                    if (userAlarm == null || StringUtils.isBlank(userAlarm.getAlarmIds())){
                                        logger.error("设备号" + alarm.getId() + "的用户未配置需要推送的告警类型");
                                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                                    }
                                    boolean isReturn = true;
                                    //获取该用户告警列表过滤
                                    String[] userAlarmArr = userAlarm.getAlarmIds().split(",");
                                    for (String s : userAlarmArr) {
                                        if (alarm.getAlarmType().equals(s)){  //说明需要推送
                                            isReturn = false;
                                        }
                                    }
                                    if (isReturn){
                                        //匹配则该告警不需要推送,直接消费成功
                                        logger.warn("该设备号的用户未配置需要推送的告警类型");
                                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                                    }

                                    WebSocket webSocket = SpringUtil.getBean(WebSocket.class);
                                    //创建业务消息信息
                                    JSONObject obj = new JSONObject();
                                    obj.put("cmd", "alarm");//业务类型
                                    obj.put("msgId", msg.getMsgId());//消息id
                                    obj.put("msgTxt", JSON.toJSONString(alarm));//消息内容
                                    //单个用户发送
                                    webSocket.sendOneMessage(alarm.getUserId(), obj.toJSONString());
                                } catch (Exception e) {
                                    logger.error("请求异常", e);
                                }
                            }
                            // 返回消费状态,消费成功
                            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                        }
                    });

                    // 调用start()方法启动consumer
                    consumer.start();
                    logger.warn("rocketmq消费者创建成功");
                } catch (Exception e) {
                    logger.error("请求异常", e);
                }
            }
        }.start();
    }
}

该消费者将消费从报警系统推送过来的报警信息,如果符合用户配置的报警类型,就通过WebSocket(这里只需要知道websocket是用来和前端建立长连接的,如果需要详细了解其意义和使用请参考相关文章)推送到前端。

其实上面示例有提到,同种类型告警5s内如果仍然有报警将会5s后推送到平台,因此,为了避免每条MQ过来的时候,都去数据库查一次配置表,可能一个半个的用户报警消息算起来很少,但是如果这个摄像头大卖,涉及到大规模用户时,这种MQ将会变得特别多,每次MQ推送报警过来时候都要去判断是否推送,规模大了这个查库过程就显得特别low。

索性将这种配置数据存到缓存中,至于仅使用Guava或者仅使用Redis或者像本文一样结合使用,又或者根据项目发展递进使用,就取决于你自己了。 

@Component
public class Utils {
	private static final Logger logger = LoggerFactory.getLogger(GuavaCacheUtils.class);
	
	/**
	* 获取用户告警配置信息
	*/
    public static UserAlarm getUserAlarm(String userId){
		if(StringUtils.isBlank(userId)){
			return null;
		}
		UserAlarm userAlarm = null;
		try {
			userAlarm = GuavaCacheUtils.userAlarmCache.get(userId).orNull();
			if(null == userAlarm){
				GuavaCacheUtils.userAlarmCache.invalidate(userId);  //清除Guava的缓存
                //尝试从Redis中获取
                String userAlarmJson = RedisUtils.hget("alarm_camera", userId);
				userAlarm = JSON.parseObject(userAlarmJson, UserAlarm.class);
			}
		} catch (ExecutionException e) {
			logger.error("获取用户配置缓存异常",e);
		}
		return userAlarm;
	}

	/**
	* 获取摄像头信息
	*/
	public static Camera getCameraById(String cameraId){
		Camera camera= null;
		try {
			camera= GuavaCacheUtils.cameraCache.get(cameraId).orNull();
		} catch (ExecutionException e) {
			logger.error("获取设备数据异常异常",e);
		}
		return device;
	}
}
/**
 * ClassName:GuavaCacheUtils <br/>
 * @version
 * @since JDK 1.8
 * @see java(jvm)缓存存储
 */
@Component
public class GuavaCacheUtils {
   private static final Logger logger = LoggerFactory.getLogger(GuavaCacheUtils.class);
	 /**
	 * 用户告警推送列表缓存
	 *
	 * expireAfterWrite:10分钟内没有更新将被回收重新获取
	 *
	 * load:获取缓存为空时执行(去数据库查询并将结果放入缓存)
	 */
	public static LoadingCache<String, Optional<UserAlarm>> userAlarmCache = CacheBuilder.newBuilder()
			.expireAfterAccess(10, TimeUnit.MINUTES).build(new CacheLoader<String, Optional<UserAlarm>>() {
				@Override
				public Optional<UserAlarm> load(String userId) throws Exception {
                    UserAlarm userAlarm = SpringUtil.getBean(UserAlarmService.class)
                        .getOne(new LambdaQueryWrapper<UserAlarm>()
									.eq(UserAlarm::getUserId,userId));
					return Optional.fromNullable(userAlarm);
				}
			});

	/**
	 * 摄像头设备信息缓存
	 */
	public static LoadingCache<String, Optional<Camera>> cameraCache = CacheBuilder.newBuilder()
			.expireAfterAccess(10, TimeUnit.MINUTES)
			.build(new CacheLoader<String, Optional<Camera>>() {
				@Override
				public Optional<Camera> load(String cameraId) throws Exception {
					String cameraJson = RedisUtils.hget("camera", cameraId);
					Cameracamera= JSON.parseObject(cameraJson, Camera.class);
					return Optional.fromNullable(camera);
				}
			});
}
@Service
public class UserAlarmServiceImpl extends ServiceImpl<UserAlarmMapper, UserAlarm> implements UserAlarmService{
    //新增用户告警配置
    @Override
    public String insert(UserAlarm userAlarm){
        try{
            this.save(userAlarm);
            //随即存入Redis
            RedisUtil.hset("alarm_camera",userAlarm.getUserId,userAlarm);
        } catch (Exception e) {
            return "失败啦";
        }
        return "成功咯";
    }
    
    //修改用户告警配置
    @Override
    public String update(UserAlarm userAlarm){
        try{
            UpdateWrapper<UserAlarm> wrapper = new UpdateWrapper();
            wrapper.set("alarmType",userAlarm.getAlarmType());
                .eq("user_id",userAlarm.getUserId);
            this.save(userAlarm);
            //随即更新Redis
            RedisUtil.hset("alarm_camera",userAlarm.getUserId,userAlarm);
        } catch (Exception e) {
            return "失败啦";
        }
        return "成功咯";
    }
}

3、Guava参数机制

#回收机制

  • expireAfterAccess: 当缓存项在指定的时间段内没有被读或写就会被回收。
  • expireAfterWrite:当缓存项在指定的时间段内没有更新就会被回收。
  • refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。

#刷新机制

  • expireAfterAccess: 设定时间内没有读缓存才会reload。
  • expireAfterWrite/refreshAfterWrite:设定时间内有读缓存将不影响reload,不论此时数据库里的指是否修改了(同时还读缓存),时间到了直接reload。
/**
 * ClassName:GuavaCacheUtils <br/>
 * @version
 * @since JDK 1.8
 * @see java(jvm)缓存存储
 */
@Component
public class GuavaCacheUtils {
   private static final Logger logger = LoggerFactory.getLogger(GuavaCacheUtils.class);

   /**
    * LoadingCache登录缓存
    * 链式调用
    * removalListener:设置缓存被移除后的监听任务
    * build:构建对象
    */
   public static LoadingCache<String, Optional<User>> loginCache = CacheBuilder.newBuilder()
         .expireAfterAccess(720, TimeUnit.MINUTES).removalListener(new MyRemovalListener())
         .build(new CacheLoader<String, Optional<User>>() {
            @Override
            public Optional<User> load(String token) throws Exception {
               User user = null;
               try {
                   //到redis中匹配
                  String loginJson = RedisUtils.get(token);
                  user = JSON.parseObject(loginJson, User.class);
               } catch (Exception e) {
                  logger.error("登录缓存查询异常", e);
               }
               return Optional.fromNullable(user);
            }
         });

    /**
    * MyRemovalListener自定义缓存移除监听器,需要实现RemovalListener接口并实现RemovalListener<K,V>接口,K,V为key和value的泛型
    * Optional:主要用于解决空指针异常,简洁判空
    * notification.getCause():监听到的缓存失效原因
    */
   private static class MyRemovalListener implements RemovalListener<String, Optional<User>> {
      @Override
      public void onRemoval(RemovalNotification<String, Optional<User>> notification) {
         if (notification.getCause().toString().equals("EXPIRED")) {
            String token = notification.getKey();
            RedisUtils.del(0,token);
         }
      }
   }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java构造函数通透理解篇

    Java构造函数通透理解篇

    这篇文章主要介绍了Java构造函数,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • 使用IDEA直接连接MySQL数据库的方法

    使用IDEA直接连接MySQL数据库的方法

    这篇文章主要介绍了如何使用IDEA直接连接MySQL数据库,首先需要新建一个空项目,第一次连接 需要先下载驱动,文中给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2024-04-04
  • 浅析Java中的动态代理

    浅析Java中的动态代理

    动态代理指代理类和目标类的关系在程序运行的时候确定的,客户通过代理类来调用目标对象的方法。本文将通过案例详细讲解一下Java动态代理的原理及实现,需要的可以参考一下
    2022-09-09
  • Java新手学习之IO流的简单使用

    Java新手学习之IO流的简单使用

    IO主要用于设备之间的数据传输,Java将操作数据流的功能封装到了IO包中,这篇文章主要给大家介绍了关于Java新手学习之IO流简单使用的相关资料,需要的朋友可以参考下
    2021-10-10
  • java网络编程之群聊功能

    java网络编程之群聊功能

    这篇文章主要为大家详细介绍了java网络编程之群聊功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-05-05
  • Mybatis处理List参数过程

    Mybatis处理List参数过程

    本文详细介绍了Mybatis处理List参数的方法,通过实体类StudentInfo示例,展示了两种不同类型的List传参查询,涵盖从前期准备SQL到Mapper接口及XML配置的全过程
    2026-05-05
  • PowerJob的ProcessorLoader工作流程源码解读

    PowerJob的ProcessorLoader工作流程源码解读

    这篇文章主要为大家介绍了PowerJob的ProcessorLoader工作流程源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • Java数据结构之简单的连接点(link)实现方法示例

    Java数据结构之简单的连接点(link)实现方法示例

    这篇文章主要介绍了Java数据结构之简单的连接点(link)实现方法,涉及java指针指向节点的相关使用技巧,需要的朋友可以参考下
    2017-10-10
  • 使用Java8实现观察者模式的方法(上)

    使用Java8实现观察者模式的方法(上)

    本文给大家介绍使用java8实现观察者模式的方法,涉及到java8观察者模式相关知识,对此感兴趣的朋友一起学习吧
    2016-02-02
  • 详解如何在springcloud分布式系统中实现分布式锁

    详解如何在springcloud分布式系统中实现分布式锁

    最近在看分布式锁的资料,本文就介绍一下利用springcloud结合redis实现分布式锁,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11

最新评论