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