电竞比分网-中国电竞赛事及体育赛事平台

分享

可靠通知系統(tǒng)設(shè)計(jì)與使用

 埃德溫會(huì)館 2017-08-07

轉(zhuǎn)至元數(shù)據(jù)結(jié)尾
轉(zhuǎn)至元數(shù)據(jù)起始
  • 需求

    NEX系統(tǒng)有很多需要調(diào)用外部系統(tǒng)進(jìn)行通知的需求,比如發(fā)送email,比如給通過(guò)rabbitmq發(fā)送消息給運(yùn)管平臺(tái)和營(yíng)銷平臺(tái),比如給營(yíng)銷平臺(tái)發(fā)送日消耗和充值記錄。這些通知有如下特點(diǎn):
  1. 主流程不依賴于通知返回結(jié)果,即通知失敗與否不影響主流程的完成
  2. 通知應(yīng)該在主流程完成之后再發(fā)出,需要避免出現(xiàn)主流程回滾而通知已經(jīng)被發(fā)出的情況
  3. 通知需要被可靠送達(dá),任何情況下不應(yīng)該存在通知沒(méi)有被送達(dá)的情況存在
  4. 接收方具有消息冪等性,即我們只關(guān)注通知是否被送達(dá),對(duì)于通知是否可能被重復(fù)發(fā)送,我們不做保證,通知的接收方需要采取措施保證冪等性

 

  • 挑戰(zhàn)

  1. 為了不影響主流程的完成,我們應(yīng)該在主流程事務(wù)提交之后再發(fā)送消息。但是通常這一點(diǎn)在程序中處理比較困難,比如要在Action中處理,相關(guān)的參數(shù)需要從service層傳輸?shù)絚ontroller層
  2. 主流程一旦完成就必須確保消息要被發(fā)出,不能允許主流程事務(wù)已經(jīng)提交而通知沒(méi)有被觸發(fā)的情況。很多意外情況可能導(dǎo)致主流程已經(jīng)提交而通知沒(méi)有發(fā)出,比如服務(wù)器突然宕機(jī),網(wǎng)絡(luò)出現(xiàn)問(wèn)題,數(shù)據(jù)庫(kù)連接不上,數(shù)據(jù)庫(kù)宕機(jī)等
  3. 如果發(fā)送失敗應(yīng)該有可以定時(shí)重發(fā)的機(jī)制。這就需要開發(fā)定時(shí)任務(wù)及將通知記錄到相關(guān)的存儲(chǔ)介質(zhì)中以便可以定時(shí)重發(fā),如果重試多次失敗還需要報(bào)警等。如果這樣的邏輯每次都要開發(fā)一遍,將造成許多重復(fù)的工作量。并且每個(gè)這樣的通知接口就設(shè)計(jì)一張表,一個(gè)定時(shí)任務(wù)也不可行,會(huì)造成系統(tǒng)難以維護(hù)
  4. 定時(shí)任務(wù)觸發(fā)的通知發(fā)送和主流程觸發(fā)的定時(shí)發(fā)送還可能存在沖突,需要采用樂(lè)觀鎖等機(jī)制以避免無(wú)謂的重復(fù)發(fā)送通知

 

  • 設(shè)計(jì)思想

  1. 通過(guò)使用Spring的TransactionSynchronizationManager將具體的通知操作延遲到當(dāng)前事務(wù)提交后才執(zhí)行
  2. 在主流程中將需要觸發(fā)的通知消息保存到消息表中,跟主流程在同一個(gè)事務(wù)中提交,保證只要主流程提交了,通知消息就被持久化了
  3. 使用定時(shí)任務(wù)定時(shí)檢查消息表中是否還存在未發(fā)出的消息,如果存在則根據(jù)重發(fā)策略(如最大重發(fā)次數(shù),重發(fā)間隔等)進(jìn)行重發(fā)
  4. 在發(fā)送消息時(shí)檢查表中狀態(tài)和發(fā)送次數(shù)的值是否發(fā)生改變,如果已經(jīng)發(fā)生改變說(shuō)明該次發(fā)送已經(jīng)被其他程序處理,則取消此次發(fā)送
  5. 將消息存儲(chǔ)在統(tǒng)一的通知消息表中,消息的參數(shù)可以直接以json數(shù)據(jù)的格式存儲(chǔ)到消息字段中。通知消息的重發(fā)策略,報(bào)警觸發(fā)條件,具體發(fā)送類則統(tǒng)一存放到通知定義表中

 

  • 具體設(shè)計(jì)

    • 典型流程






      • 表設(shè)計(jì)

        1. solid_notify_def(通知定義表)

        Field
        Type
        Comment
        idbigint(20) NOT NULL自增主鍵
        namevarchar(40) NOT NULL通知名稱
        impl_classvarchar(400) NOT NULL通知的實(shí)現(xiàn)類
        methodvarchar(20) NOT NULL通知的方法名稱
        max_retry_timesint(11) NOT NULL最大重試次數(shù),-1表示unlimited
        max_retry_intervalint(11) NOT NULL最大重試間隔,以分鐘為單位
        alert_trigger_typetinyint(4) NOT NULL報(bào)警觸發(fā)類型,0 - 最終失敗, 1 - 失敗, 2 - NEVER,3 - 失敗若干次后報(bào)警
        emailvarchar(400) NOT NULL調(diào)用失敗后通知人郵件,注意調(diào)用失敗是指經(jīng)過(guò)若干次重試后超過(guò)最大重試次數(shù)后失敗,逗號(hào)分隔
        descriptionvarchar(400) NULL對(duì)該通知定義的描述
        retry_intervalvarchar(400) NOT NULL重試間隔,可為固定長(zhǎng)度或列表或策略類或表達(dá)式,s - 秒,m - 分鐘,h - 小時(shí), d - 天
        alert_trigger_fail_timesint(11) NULL失敗多少次后觸發(fā)報(bào)警
        need_confirmtinyint(4) NOT NULL是否需要確認(rèn),0 - 不需要, 1 - 需要
        max_confirm_delayint(11) NOT NULL在需要確認(rèn)時(shí)經(jīng)過(guò)多少時(shí)間后認(rèn)為調(diào)用失敗,以秒為單位,0表示無(wú)限制等下去

         

        2. solid_notify_msg(通知消息表)

        Field
        Type
        Comment
        idbigint(20) NOT NULL自增主鍵
        def_idbigint(20) NOT NULL通知定義id
        paramtext NULL通知參數(shù)
        descriptiontext NULL描述信息,如該通知的一些上下文信息
        errorstext NULL調(diào)用失敗記錄的錯(cuò)誤信息
        statustinyint(4) NOT NULL該通知的狀態(tài),0 - 未發(fā)送, 1 - 發(fā)送成功, 2 - 發(fā)送失敗, 3 - 發(fā)送最終失敗,4 - 待確認(rèn)
        retry_timesint(11) NOT NULL重試次數(shù)
        last_call_timedatetime NULL最后調(diào)用時(shí)間
        nex_call_timedatetime NULL下次執(zhí)行時(shí)間
        create_timedatetime NOT NULL該通知最初創(chuàng)建時(shí)間

        3. solid_notify_msg_history(通知消息歷史表)

        和通知消息表表結(jié)構(gòu)完全一樣,歷史表主要用來(lái)將已經(jīng)是成功狀態(tài)或者最終失敗狀態(tài)的消息從通知消息表中移除,以免通知消息表中積攢太多消息影響性能。

         

    • 類設(shè)計(jì)


      其中addNotice方法實(shí)現(xiàn)如下:

      @Override
      @Transactional
      public SolidNotifyMsgWithBLOBs addNotice(String defName, String param, String description) {
          final SolidNotifyMsgWithBLOBs solidNotifyMsgWithBLOBs = new SolidNotifyMsgWithBLOBs();
          SolidNotifyDef solidNotifyDef = getDefByName(defName);
          solidNotifyMsgWithBLOBs.setDefId(solidNotifyDef.getId());
          solidNotifyMsgWithBLOBs.setParam(param);
          solidNotifyMsgWithBLOBs.setDescription(description);
          solidNotifyMsgWithBLOBs.setCreateTime(new Date());
          solidNotifyMsgWithBLOBs.setStatus(SolidNotifyMsgStatus.NOT_SEND.getValue());
          solidNotifyMsgWithBLOBs.setRetryTimes(0);
          solidNotifyMsgWithBLOBs.setNexCallTime(Calendar.getInstance().getTime());
          solidNotifyMsgMapper.insert(solidNotifyMsgWithBLOBs);
          TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
              @Override
              public void afterCommit() {
                  SolidNotifyDef solidNotifyDef = solidNotifyDefMapper.selectByPrimaryKey(solidNotifyMsgWithBLOBs.getDefId());
                  sendNotice(solidNotifyMsgWithBLOBs, solidNotifyDef);
              }
          });
          return solidNotifyMsgWithBLOBs;

      }

    • 時(shí)序圖


  • 使用方法


    1. 在服務(wù)方法中需要通知外部系統(tǒng)的地方調(diào)用SolidNotifyService::addNotice方法,并將參數(shù)轉(zhuǎn)化成json格式
    2. 在服務(wù)類或其他類中添加通知回調(diào)方法,在回調(diào)方法中實(shí)際調(diào)用外部系統(tǒng),若調(diào)用失敗則拋出異常,調(diào)用者會(huì)自動(dòng)處理該異常并更新對(duì)應(yīng)的通知消息記錄狀態(tài),注意回調(diào)方法只能有一個(gè)SolidNofityMsgWithBLOBs類型的參數(shù)
    3. 在solid_notify_def表中增加一條記錄,注意如果是服務(wù),對(duì)應(yīng)的impl_class填寫的應(yīng)該是接口名而不是實(shí)現(xiàn)類名
  • 應(yīng)用舉例

    1. 目前DspInfoServiceImpl::setDsp方法中需要同步相關(guān)dsp信息到運(yùn)管平臺(tái)(采用發(fā)送rabbitmq消息的方式),在該方法末尾調(diào)用了addNotice方法:
    solidNotifyService.addNotice("syncToOmp", JSONObject.toJSONString(dspInfo), "");其中"syncToOmp"為該通知定義的名稱
    2. 并增加方法DspInfoServiceImpl::syncToOmp作為回調(diào)方法:

    @Override
    public void syncToOmp(SolidNotifyMsgWithBLOBs solidNotifyMsgWithBLOBs) {
        try {
            DspInfo dspInfo = JSONObject.parseObject(solidNotifyMsgWithBLOBs.getParam(), DspInfo.class);
            sendToOmp(dspInfo);
        } catch (Exception e) {
            logger.error("syncToOmp failed", e);
            throw new NexException("syncToOmp failed", e);
        }
    }


    3. 在solid_notify_def表中增加了一條記錄:

    id
    name
    impl_class
    method
    max_retry_times
    max_retry_interval
    alert_trigger_type
    email
    description
    retry_interval
    1syncToOmpcom..b.nex.service.dspinfo.DspInfoServicesyncToOmp2-10bjlihaiwu@corp.同步dsp信息到運(yùn)管平臺(tái)5m
  • 注意事項(xiàng)與常見(jiàn)問(wèn)題

    1. solid_notify_msg中的description字段用于存儲(chǔ)一些上下文信息,可為空。比方說(shuō)在執(zhí)行過(guò)程中的一些結(jié)果信息希望存儲(chǔ)到description中
    2. 在回調(diào)方法中拋出異??梢宰屚ㄖ蝿?wù)以失敗結(jié)束,這時(shí)候會(huì)根據(jù)重試策略決定重試還是最終失敗。也可以主動(dòng)設(shè)置SolidNofityMsgWithBLOBs的status字段為最終失敗,此后該通知將不再發(fā)送并按照?qǐng)?bào)警策略決定是否觸發(fā)報(bào)警
    3. retry_interval為數(shù)組的情況可以用于一些重試間隔不均勻的場(chǎng)合,比如第一次是5秒鐘后重試,第二次是5分鐘后重試,第三次是一個(gè)小時(shí)候重試,第四次為一天后重試,則可以定義為[5s, 5m, 1h, 1d],下次執(zhí)行時(shí)間會(huì)在當(dāng)前時(shí)間的基礎(chǔ)上加上對(duì)應(yīng)次數(shù)的重試間隔
    4. 重試間隔目前還不支持表達(dá)式和策略類,稍后開發(fā)
    5. 回調(diào)方法中拋出任何異常都不會(huì)導(dǎo)致主流程回滾
    6. controller中如希望立即知道通知消息是否調(diào)用成功,可檢查addNotice方法返回的通知消息的狀態(tài),注意只能將原返回對(duì)象的引用直接傳遞回controller層再檢查,而不可在service層檢查或者復(fù)制,因?yàn)樵趕ervice調(diào)用結(jié)束后才會(huì)執(zhí)行回調(diào)并更新該返回對(duì)象
    7. 對(duì)于需要確認(rèn)的消息,如給消息隊(duì)列發(fā)送消息并且需要發(fā)送發(fā)確認(rèn)或者消費(fèi)者確認(rèn)的功能,可以使用need_confirm為1,此時(shí)發(fā)送后消息進(jìn)入待確認(rèn)狀態(tài),只有收到確認(rèn)消息才能視為成功,max_confirm_delay設(shè)置為最大允許的等待確認(rèn)時(shí)間,超過(guò)這個(gè)時(shí)間將開始重新發(fā)送

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多