Guava本地緩存的使用過程
Guava和Redis實(shí)現(xiàn)二級緩存
1、目的
本地緩存為什么不使用hashMap或者concurrentHashMap?
concurrentHahMap和hashMap一樣,都是長期存在的緩存,除非調(diào)用remove方法,否則緩存中的數(shù)據(jù)無法主動(dòng)釋放。
僅使用Guava本地緩存會有什么問題?
作為API或者某種功能系統(tǒng)來用的話,無論單機(jī)/集群(集群其實(shí)就形成了近乎Guava副本的情況),Guava中的數(shù)據(jù)增長到后期不可估量的時(shí)候,Guava是支撐不住的;而微服務(wù)情況下沒法全局緩存,如果數(shù)據(jù)量無限增長、不可控的話還是不建議使用。
僅使用Redis緩存會有什么問題?
大數(shù)量的情況下(熱搜)容易引發(fā)緩存雪崩進(jìn)而導(dǎo)致服務(wù)器雪崩。
綜上,結(jié)合Guava、Redis,Guava作為一級緩存,Redis作為二級緩存,可以在減少數(shù)據(jù)庫壓力的基礎(chǔ)上,將“緩存”這道防線做的更加可靠。
2、二級緩存場景示例
公司有一款攝像頭,放在了我家經(jīng)常無人居住的豪宅了,攝像頭包括異常人像報(bào)警、斷電報(bào)警、信號異常報(bào)警、捕獲畫面動(dòng)態(tài)報(bào)警等等多種報(bào)警功能類型(跳過其他設(shè)定,規(guī)定同類型的報(bào)警間隔5秒內(nèi)仍存在則繼續(xù)報(bào)警)。
現(xiàn)在有需求:我可以在平臺上配置我想要報(bào)警的報(bào)警類型(不然我哪天周末回豪宅了它還一直報(bào)警到平臺打擾我休息),當(dāng)有我報(bào)警信息過來并且是匹配我配置的報(bào)警信息時(shí),這個(gè)這條報(bào)警將推送到我平臺首頁。
//這里忽略報(bào)警系統(tǒng)代碼,報(bào)警系統(tǒng)推送報(bào)警消息是通過RocketMQ實(shí)現(xiàn) topic: alarm-camera
@Configuration
public class RocketMqConsumer {
private static Logger logger = LogManager.getLogger(RocketMqConsumer.class);
public void init() {
pullAlarm();
logger.warn("rocketmq拉取告警數(shù)據(jù)成功!");
}
/**
* pullAlarm:拉取告警源數(shù)據(jù)。
* @author liaokh
* @since JDK 1.8
*/
public static void pullAlarm() {
new Thread() {
public void run() {
logger.warn("---------開始消費(fèi)報(bào)警broker---------");
try {
// 聲明并初始化一個(gè)consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-consumer-dev-camera" + "-alarm");
// 同樣也要設(shè)置NameServer地址
consumer.setNamesrvAddr("我的RocketMQ服務(wù)器地址");
// 廣播模式 當(dāng) Consumer 使用廣播模式時(shí),每條消息都會被 Consumer 集群內(nèi)所有的 Consumer 實(shí)例消費(fèi)一次。
consumer.setMessageModel(MessageModel.BROADCASTING);
// 這里設(shè)置的是一個(gè)consumer的消費(fèi)策略
// CONSUME_FROM_LAST_OFFSET 默認(rèn)策略,從該隊(duì)列最尾開始消費(fèi),即跳過歷史消息
// CONSUME_FROM_FIRST_OFFSET 從隊(duì)列最開始開始消費(fèi),即歷史消息(還儲存在broker的)全部消費(fèi)一遍
// CONSUME_FROM_TIMESTAMP 從某個(gè)時(shí)間點(diǎn)開始消費(fèi),和setConsumeTimestamp()配合使用,默認(rèn)是半個(gè)小時(shí)以前
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// 設(shè)置consumer所訂閱的Topic和Tag,*代表全部的Tag
consumer.subscribe("alarm-camera", "*");
// 設(shè)置一個(gè)Listener,主要進(jìn)行消息的邏輯處理
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數(shù)據(jù):tag:" + tag + " alarmJson:" + alarmJson);
CameraAlarmResp resultAlarm = new CameraAlarmResp();
AlarmMQResp alarm = JSON.parseObject(alarmJson, AlarmMQResp.class);
//查看當(dāng)前告警類型是否在該用戶配置的列表中
//根據(jù)攝像頭設(shè)備號獲取用戶信息
Camera cameraEntity = Utils.getCameraById(alarm.getCameraId()); //這種核心數(shù)據(jù)也可以加載到緩存中
UserAlarm userAlarm = Utils.getUserAlarm(cameraEntity.getUserId());
if (userAlarm == null || StringUtils.isBlank(userAlarm.getAlarmIds())){
logger.error("設(shè)備號" + 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){
//匹配則該告警不需要推送,直接消費(fèi)成功
logger.warn("該設(shè)備號的用戶未配置需要推送的告警類型");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
WebSocket webSocket = SpringUtil.getBean(WebSocket.class);
//創(chuàng)建業(yè)務(wù)消息信息
JSONObject obj = new JSONObject();
obj.put("cmd", "alarm");//業(yè)務(wù)類型
obj.put("msgId", msg.getMsgId());//消息id
obj.put("msgTxt", JSON.toJSONString(alarm));//消息內(nèi)容
//單個(gè)用戶發(fā)送
webSocket.sendOneMessage(alarm.getUserId(), obj.toJSONString());
} catch (Exception e) {
logger.error("請求異常", e);
}
}
// 返回消費(fèi)狀態(tài),消費(fèi)成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 調(diào)用start()方法啟動(dòng)consumer
consumer.start();
logger.warn("rocketmq消費(fèi)者創(chuàng)建成功");
} catch (Exception e) {
logger.error("請求異常", e);
}
}
}.start();
}
}該消費(fèi)者將消費(fèi)從報(bào)警系統(tǒng)推送過來的報(bào)警信息,如果符合用戶配置的報(bào)警類型,就通過WebSocket(這里只需要知道websocket是用來和前端建立長連接的,如果需要詳細(xì)了解其意義和使用請參考相關(guān)文章)推送到前端。
其實(shí)上面示例有提到,同種類型告警5s內(nèi)如果仍然有報(bào)警將會5s后推送到平臺,因此,為了避免每條MQ過來的時(shí)候,都去數(shù)據(jù)庫查一次配置表,可能一個(gè)半個(gè)的用戶報(bào)警消息算起來很少,但是如果這個(gè)攝像頭大賣,涉及到大規(guī)模用戶時(shí),這種MQ將會變得特別多,每次MQ推送報(bào)警過來時(shí)候都要去判斷是否推送,規(guī)模大了這個(gè)查庫過程就顯得特別low。
索性將這種配置數(shù)據(jù)存到緩存中,至于僅使用Guava或者僅使用Redis或者像本文一樣結(jié)合使用,又或者根據(jù)項(xiàng)目發(fā)展遞進(jìn)使用,就取決于你自己了。
@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("獲取設(shè)備數(shù)據(jù)異常異常",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分鐘內(nèi)沒有更新將被回收重新獲取
*
* load:獲取緩存為空時(shí)執(zhí)行(去數(shù)據(jù)庫查詢并將結(jié)果放入緩存)
*/
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);
}
});
/**
* 攝像頭設(shè)備信息緩存
*/
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參數(shù)機(jī)制
#回收機(jī)制
expireAfterAccess: 當(dāng)緩存項(xiàng)在指定的時(shí)間段內(nèi)沒有被讀或?qū)懢蜁换厥铡?/li>expireAfterWrite:當(dāng)緩存項(xiàng)在指定的時(shí)間段內(nèi)沒有更新就會被回收。refreshAfterWrite:當(dāng)緩存項(xiàng)上一次更新操作之后的多久會被刷新。
#刷新機(jī)制
expireAfterAccess: 設(shè)定時(shí)間內(nèi)沒有讀緩存才會reload。expireAfterWrite/refreshAfterWrite:設(shè)定時(shí)間內(nèi)有讀緩存將不影響reload,不論此時(shí)數(shù)據(jù)庫里的指是否修改了(同時(shí)還讀緩存),時(shí)間到了直接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登錄緩存
* 鏈?zhǔn)秸{(diào)用
* removalListener:設(shè)置緩存被移除后的監(jiān)聽任務(wù)
* build:構(gòu)建對象
*/
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自定義緩存移除監(jiān)聽器,需要實(shí)現(xiàn)RemovalListener接口并實(shí)現(xiàn)RemovalListener<K,V>接口,K,V為key和value的泛型
* Optional:主要用于解決空指針異常,簡潔判空
* notification.getCause():監(jiān)聽到的緩存失效原因
*/
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);
}
}
}
}總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用IDEA直接連接MySQL數(shù)據(jù)庫的方法
這篇文章主要介紹了如何使用IDEA直接連接MySQL數(shù)據(jù)庫,首先需要新建一個(gè)空項(xiàng)目,第一次連接 需要先下載驅(qū)動(dòng),文中給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-04-04
PowerJob的ProcessorLoader工作流程源碼解讀
這篇文章主要為大家介紹了PowerJob的ProcessorLoader工作流程源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Java數(shù)據(jù)結(jié)構(gòu)之簡單的連接點(diǎn)(link)實(shí)現(xiàn)方法示例
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之簡單的連接點(diǎn)(link)實(shí)現(xiàn)方法,涉及java指針指向節(jié)點(diǎn)的相關(guān)使用技巧,需要的朋友可以參考下2017-10-10
使用Java8實(shí)現(xiàn)觀察者模式的方法(上)
本文給大家介紹使用java8實(shí)現(xiàn)觀察者模式的方法,涉及到j(luò)ava8觀察者模式相關(guān)知識,對此感興趣的朋友一起學(xué)習(xí)吧2016-02-02
詳解如何在springcloud分布式系統(tǒng)中實(shí)現(xiàn)分布式鎖
最近在看分布式鎖的資料,本文就介紹一下利用springcloud結(jié)合redis實(shí)現(xiàn)分布式鎖,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11

