SpringBoot—實(shí)現(xiàn)n秒內(nèi)出現(xiàn)x個(gè)異常報(bào)警

報(bào)警工具:
SpringBoot集成釘釘報(bào)警sdk(解決Failed to introspect Class異常)

實(shí)現(xiàn)5秒內(nèi)出現(xiàn)10個(gè)異常時(shí)觸發(fā)報(bào)警祷安。需要使用Redis來進(jìn)行全局控制蘸鲸。

思路:
借助Redis的zSet集合朵夏,獲取一定時(shí)間范圍內(nèi)的set集合妄辩。判斷set個(gè)個(gè)數(shù)是否滿足條件碍舍,若滿足則觸發(fā)報(bào)警埂奈。

注意點(diǎn):

  1. 防止多次報(bào)警:加阻塞性的分布式鎖若贮,一個(gè)線程處理時(shí)戳吝,其他線程等待浩销,若線程觸發(fā)報(bào)警后,清空redis听哭。
  2. 報(bào)警觸發(fā)機(jī)制:每次收到新的異常時(shí)慢洋,觸發(fā)報(bào)警邏輯。
  3. 業(yè)務(wù)配置的優(yōu)先級(jí)要高于全局配置陆盘。
  4. 因?yàn)槭褂脄Set存儲(chǔ)普筹,所有防止數(shù)據(jù)結(jié)構(gòu)自帶的去重邏輯。

1. 配置類

配置類中可以配置全局配置以及各個(gè)業(yè)務(wù)的配置隘马。

@Component
@ConfigurationProperties(prefix = "monitor.config")
@Data
public class MonitorProperties {

    /**
     * 全局監(jiān)控配置斑芜。
     */
    private MonitorInfo globeMonitor;

    /**
     * key:模塊的名字,模塊優(yōu)先全局配置
     */
    private Map<String, MonitorInfo> monitorMapping;


    /**
     * 釘釘監(jiān)控報(bào)警的相關(guān)信息
     */
    @Data
    public static class MonitorInfo {

        /**
         * 是否打開全局的監(jiān)控報(bào)警(默認(rèn)關(guān)閉)
         */
        private boolean openMonitor = false;

        /**
         * 全局的監(jiān)控報(bào)警-間隔時(shí)間(默認(rèn)4分鐘),單位:ms
         */
        private long monitorIntervalTimeMillis = 4 * 60 * 1000L;

        /**
         * 全局的監(jiān)控報(bào)警-間隔時(shí)間內(nèi)的異常數(shù)閾值祟霍,在間隔時(shí)間內(nèi)若超過改閾值杏头,便會(huì)報(bào)警。(默認(rèn)30個(gè))
         */
        private int errorCount = 30;


        /**
         * 監(jiān)控報(bào)警的釘釘群地址(必須配置沸呐,若不配置醇王,則不會(huì)開啟報(bào)警)
         */
        private String webHook;

        /**
         * (選填)監(jiān)控報(bào)警的釘釘群機(jī)器人@的手機(jī)號(hào)(填寫手機(jī)號(hào),會(huì)自動(dòng)@通知群的同事)崭添。填寫多個(gè)時(shí)寓娩,使用,分割
         */
        private String atMobiles;
    }

}

yml的配置:

monitor:
  config:
    # 1. 全局監(jiān)控報(bào)警
    globe-monitor:
      # 開啟監(jiān)控報(bào)警
      open-monitor: true
      # 間隔時(shí)間6s
      monitor-interval-time-millis: 6000
      # 異常數(shù)量閾值,在監(jiān)控時(shí)間間隔內(nèi),若觸發(fā)閾值棘伴,會(huì)發(fā)起報(bào)警
      error-count: 10
      # 釘釘報(bào)警的機(jī)器人地址(必須配置寞埠,若不配置,則不會(huì)開啟報(bào)警)
      web-hook: xxx
      # (選填)監(jiān)控報(bào)警的釘釘群機(jī)器人@的手機(jī)號(hào)(填寫手機(jī)號(hào)焊夸,會(huì)自動(dòng)@通知群的同事)仁连。填寫多個(gè)時(shí),使用,分割
      at-mobiles: 
    # 2. 各業(yè)務(wù)監(jiān)控報(bào)警(優(yōu)先級(jí)高于全局監(jiān)控報(bào)警)
    monitor-mapping:
      # MsgTypeEnum枚舉值的名字
      XxxType:
        open-monitor: true
        error-count: 30

使用@ConfigurationProperties注解時(shí)阱穗,注意在啟動(dòng)類加@EnableConfigurationProperties注解饭冬,pom文件增加:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
</dependency>

2. Redis的工具類

Redis的工具類需要提供阻塞式的分布式鎖。

@Service
@Slf4j
public class RedisService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final String SUCCESS = "OK";


    public StringRedisTemplate getStringRedisTemplate(){
        return stringRedisTemplate;
    }

    /**
     * 加鎖的方法
     */
    public Boolean vSetIfAbsent(String key, String value, long timeoutMillisecond) {
        return stringRedisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                String result;
                Object nativeConnection = connection.getNativeConnection();
                if (nativeConnection instanceof JedisCluster) {
                    result = ((JedisCluster) nativeConnection).set(key, value, "NX", "PX", timeoutMillisecond);
                    return SUCCESS.equals(result);
                } else if (nativeConnection instanceof Jedis) {
                    result = ((Jedis) nativeConnection).set(key, value, "NX", "PX", timeoutMillisecond);
                    return SUCCESS.equals(result);
                } else {
                    return false;
                }

            }
        });
    }

    /**
     * 加鎖揪阶,若加鎖失敗昌抠,便一直重試。重試的等待時(shí)間為waitMillisecond控制鲁僚。
     *
     * @param key               - key名稱
     * @param expireMillisecond - 鎖成功后的有效期,毫秒
     * @param waitMillisecond   - 沒有獲取到鎖時(shí)炊苫,保持阻塞的最大時(shí)間
     * @return 加鎖成功后持有的字符串。
     */
    public String lock(String key, long expireMillisecond, long waitMillisecond) {
        //當(dāng)前時(shí)間
        long currentTimeMillis = System.currentTimeMillis();
        //失效時(shí)間
        long invalidTimeMillis = currentTimeMillis + waitMillisecond;

        String lockKey = "lock:" + key;
        String lockValue = UUID.randomUUID().toString();
        //加鎖失敗冰沙,返回的是false侨艾,那么sleep一段時(shí)間,再次嘗試加鎖
        boolean keySet = vSetIfAbsent(lockKey, lockValue, expireMillisecond);
        //鎖失敗并且沒有達(dá)到最大失效時(shí)間
        while (!keySet && System.currentTimeMillis() < invalidTimeMillis) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                log.error("", e);
            }
            keySet = vSetIfAbsent(lockKey, lockValue, expireMillisecond);
        }
        //加鎖成功倦淀,返回加鎖的字符串。
        if (keySet) {
            return lockValue;
        }
        return null;
    }

    /**
     * 解鎖
     *
     * @param key
     */
    public void unlock(String key, String value) {
        if (StringUtils.isBlank(value)) {
            return;
        }
        String lockKey = "lock:" + key;
        String lockValueRedis = stringRedisTemplate.opsForValue().get(lockKey);
        if (StringUtils.equals(lockValueRedis, value)) {
            stringRedisTemplate.delete(lockKey);
        }
    }
}

3. 報(bào)警類

@Component
public class MonitorService {

    @Autowired
    private MonitorProperties monitorProperties;

    @Autowired
    private RedisService redisService;
    /**
     * 推送消息的模板
     */
    private static final String alertTemplate = "【報(bào)警】\n" + "[異常類型]   %s \n"
            + "于[%d]秒內(nèi)發(fā)生[%d]次錯(cuò)誤声畏,\n";

    /**
     * 接受到異常類
     *
     * @param e 異常類
     */
    public void exceptionMonitor(Exception e, String type) {
        //是否開啟異常報(bào)警
        if (errorEntryMonitor(e)) {
            //讀取配置
            MonitorProperties.MonitorInfo monitorInfo = openMonitorByProperties(type);
            //存在配置且合法
            if (monitorInfo != null) {
                //redis加鎖撞叽,防止多次通知
                String lockKey = "monitor:" + type;
                String lockValue = redisService.lock(lockKey, 30 * 1000, 5 * 1000);
                try {
                    //加鎖成功
                    if (lockValue != null) {
                        //查詢異常
                        String monitorKey = "monitor:score:" + type;
                        long maxScore = System.currentTimeMillis();
                        long minScore = maxScore - monitorInfo.getMonitorIntervalTimeMillis();
                        //獲取間隔時(shí)間的異常類信息(注入,此處是set可能去重插龄,加入時(shí)應(yīng)該防止去重)
                        Set<String> errors = redisService.getStringRedisTemplate().opsForZSet().rangeByScore(monitorKey, minScore, maxScore);
                        //獲取到報(bào)警的內(nèi)容愿棋,為防止去重,增加id
                        String monitorContent = monitorContent(e);
                        //當(dāng)異常數(shù)量滿足時(shí)
                        if (errors != null && errors.size() == monitorInfo.getErrorCount() - 1) {
                            //存儲(chǔ)最新一次的異常信息
                            errors.add(monitorContent);
                            //獲取到原始的異常集合(去重前綴id)
                            Set<String> originalSets = errors.stream().
                                    map(k -> k.split(":")[1]).
                                    filter(k -> !"null".equals(k)).
                                    collect(Collectors.toSet());
                            //報(bào)警信息
                            String message;
                            if (originalSets.stream().anyMatch(StringUtils::isNotBlank)) {
                                //詳細(xì)信息是真正由用戶控制
                                message = String.format(alertTemplate + "均牢,詳細(xì)信息%s", type, monitorInfo.getMonitorIntervalTimeMillis() / 1000,
                                        monitorInfo.getErrorCount(), JSON.toJSONString(originalSets));
                            } else {
                                message = String.format(alertTemplate, type, monitorInfo.getMonitorIntervalTimeMillis() / 1000,
                                        monitorInfo.getErrorCount());
                            }
                            //報(bào)警
                            DingtalkUtils.dingtalk(monitorInfo.getWebHook(), message, monitorInfo.getAtMobiles());
                            //報(bào)警成功后糠雨,移除Redis的數(shù)據(jù)
                            redisService.getStringRedisTemplate().opsForZSet().removeRangeByScore(monitorKey, minScore, maxScore);
                        } else {
                            //,直接放入到Redis中
                            redisService.getStringRedisTemplate().opsForZSet().add(monitorKey, monitorContent, maxScore);
                            //設(shè)置key的失效時(shí)間
                            redisService.getStringRedisTemplate().expire(monitorKey,monitorInfo.getMonitorIntervalTimeMillis(), TimeUnit.MILLISECONDS);
                        }

                    }
                } finally {
                    //釋放分布式鎖
                    redisService.unlock(lockKey, lockValue);
                }

            }
        }
    }

    /**
     * 異常監(jiān)控的前綴信息徘跪,在回顯的時(shí)候甘邀,會(huì)移除掉前綴信息
     * 此處+uuid是為了防止被set去重(可以換其他id)
     *
     * @return 前綴信息
     */
    private String monitorContent(Exception e) {
        return UUID.randomUUID().toString() + ":" + errorMonitorContent(e);
    }


    /**
     * 真正報(bào)警的內(nèi)容報(bào)警的內(nèi)容
     */
    protected String errorMonitorContent(Exception e) {
        return null;
    }


    /**
     * 判斷是否開啟異常報(bào)警
     */
    protected boolean errorEntryMonitor(Exception e) {
        return true;
    }

    /**
     * 根據(jù)配置決定是否開啟監(jiān)控
     */
    protected MonitorProperties.MonitorInfo openMonitorByProperties(String type) {
        //讀取個(gè)性化配置
        Map<String, MonitorProperties.MonitorInfo> monitorMapping = monitorProperties.getMonitorMapping();
        MonitorProperties.MonitorInfo monitorInfo;
        //若個(gè)性化沒有配置,讀取全局配置
        if (monitorMapping == null) {
            monitorInfo = monitorProperties.getGlobeMonitor();
        } else {
            monitorInfo = monitorMapping.getOrDefault(type, monitorProperties.getGlobeMonitor());
        }
        //此時(shí)是開啟的狀態(tài)垮庐,判斷其他參數(shù)是否合法
        if (monitorInfo != null && monitorInfo.isOpenMonitor()) {
            if (monitorInfo.getErrorCount() != 0
                    && monitorInfo.getMonitorIntervalTimeMillis() != 0 &&
                    StringUtils.isNotBlank(monitorInfo.getWebHook())) {
                return monitorInfo;
            }
        }
        return null;
    }

}

注:繼承釘釘報(bào)警可以參考SpringBoot集成釘釘報(bào)警sdk(解決Failed to introspect Class異常)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末松邪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子哨查,更是在濱河造成了極大的恐慌逗抑,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異邮府,居然都是意外死亡荧关,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門褂傀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忍啤,“玉大人,你說我怎么就攤上這事紊服√垂欤” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵欺嗤,是天一觀的道長参萄。 經(jīng)常有香客問我,道長煎饼,這世上最難降的妖魔是什么讹挎? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮吆玖,結(jié)果婚禮上筒溃,老公的妹妹穿的比我還像新娘。我一直安慰自己沾乘,他們只是感情好怜奖,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翅阵,像睡著了一般歪玲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掷匠,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天滥崩,我揣著相機(jī)與錄音,去河邊找鬼讹语。 笑死钙皮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的顽决。 我是一名探鬼主播短条,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼才菠!你這毒婦竟也來了慌烧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤鸠儿,失蹤者是張志新(化名)和其女友劉穎屹蚊,沒想到半個(gè)月后厕氨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汹粤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年命斧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘱兼。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡国葬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出芹壕,到底是詐尸還是另有隱情汇四,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布踢涌,位于F島的核電站通孽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏睁壁。R本人自食惡果不足惜背苦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望潘明。 院中可真熱鬧行剂,春花似錦、人聲如沸钳降。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遂填。三九已至铲觉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間城菊,已是汗流浹背备燃。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工碉克, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凌唬,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓漏麦,卻偏偏與公主長得像客税,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子撕贞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容