在spring boot中訊息推送系統設計與實現

蘇小林發表於2019-05-06

推送系統作為通用的元件,存在的價值主要有以下幾點

  1. 會被多個業務專案使用,推送系統獨立維護可降低維護成本
  2. 推送系統一般都是呼叫三方api進行推送,三方api一般會有呼叫頻率/次數限制,被推送的訊息需要走佇列來合理呼叫三方api,控制呼叫的頻率和次數
  3. 業務無關,一般推送系統設計成不需要關心業務邏輯

核心技術

  1. 訊息佇列
  2. 三方服務api呼叫
    1. 安卓app推送
    2. 蘋果app推送
    3. 微信小程式推送
    4. 郵件推送
    5. 釘釘推送
    6. 簡訊推送

訊息佇列選用阿里雲提供的rocketmq,官方文件:help.aliyun.com/document_de…

推送時序圖

推送系統互動時序圖

右鍵新視窗開啟可以檢視高清大圖

可以看到訊息推送系統接入的第三方服務比較多,三方推送的質量很難統一,就需要考慮訊息的推送的重試了

思路流程

為了控制併發,所有的推送都先發到rocketmq佇列裡,每次推送的個數通過控制佇列的消費的客戶端的數量來實現

安卓和蘋果都可以使用信鴿的推送服務

在spring boot中訊息推送系統設計與實現

信鴿推送需要客戶端進行整合,客戶端sdk參考:xg.qq.com/xg/ctr_inde…

目前信鴿個人開發者仍然是可以申請的,賬號建立後,建立andorid和ios專案

在spring boot中訊息推送系統設計與實現
記錄下這裡的 APP ID和SECRET KEY,服務端進行推送時需要這兩個引數

推送異常處理,推送異常時,需要重試,重試可以利用訊息佇列本身的重試機制,也可以自行實現重試邏輯

安卓app推送

官方文件:xg.qq.com/docs/androi…

程式碼庫:github.com/xingePush/x…

<!-- 信鴿推送客戶端 -->
<dependency>
    <groupId>com.github.xingePush</groupId>
    <artifactId>xinge</artifactId>
    <version>1.2.1</version>
</dependency>
複製程式碼

核心程式碼如下

Map<String, Object> messagePayload = new HashMap<String, Object>(1);
messagePayload.put("user_id", 1);
messagePayload.put("msg_title", "訊息標題");
messagePayload.put("msg_content", "訊息內容");
messagePayload.put("msg_type", 1);
messagePayload.put("data", Lists.newHashMap("order_id", 1));

XingeApp xinge = new XingeApp(androidAccessId, androidSecretKey);
MessageAndroid message = new MessageAndroid();
ClickAction action = new ClickAction();
action.setActionType(ClickAction.TYPE_ACTIVITY);
message.setAction(action);
message.setContent(JsonUtil.toJsonString(messagePayload));
message.setType(MessageAndroid.TYPE_MESSAGE);
message.setExpireTime(86400);
message.setCustom(new HashMap<String, Object>(1));
JSONObject response = xinge.pushSingleDevice("使用者的PushToken", message);
if (response.getInt(RET_CODE) != 0) {
    // 推送異常了,進行日誌記錄等
}
複製程式碼

蘋果app推送

使用pushy庫進行推送

<!-- IOS推送客戶端 -->
<dependency>
    <groupId>com.turo</groupId>
    <artifactId>pushy</artifactId>
    <version>0.13.3</version>
</dependency>
複製程式碼
Map<String, Object> aps = new HashMap<String, Object>(1);
aps.put("alert", "推送內容");
aps.put("sound", "default");
aps.put("badge", 1);

Map<String, Object> data = new HashMap<String, Object>(1);
data.put("msgTitle", "推送標題");
data.put("msgContent", "推送內容");
data.put("msgType", "1");
data.put("userId", "13288888888");
data.put("data", Lists.newHashMap("order_id", 1));

Map<String, Object> messagePayload = new HashMap<String, Object>(1);
messagePayload.put("aps", aps);
messagePayload.put("data", data);

ApnsClient apnsClient = new ApnsClientBuilder()
        .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST)
        .setClientCredentials(this.getClass().getClassLoader().getResourceAsStream("推送證照相對resources目錄的路徑"), "")
        .build();

String payload = JsonUtil.toJsonString(messagePayload);
String token = TokenUtil.sanitizeTokenString("app使用者的pushToken");

SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, "com.suxiaolin.app1", payload);

PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>>
        sendNotificationFuture = apnsClient.sendNotification(pushNotification);

final PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse =
        sendNotificationFuture.get();

if (pushNotificationResponse.isAccepted()) {
    System.out.println("Push notification accepted by APNs gateway.");
} else {
    System.out.println("Notification rejected by the APNs gateway: " +
            pushNotificationResponse.getRejectionReason());

    if (pushNotificationResponse.getTokenInvalidationTimestamp() != null) {
        System.out.println("\t…and the token is invalid as of " +
                pushNotificationResponse.getTokenInvalidationTimestamp());
    }
}
複製程式碼

使用信鴿庫進行推送

當然也可以使用信鴿提供的ios推送,邏輯和安卓app的推送差不多

ios的信鴿客戶端可以和android的客戶端共用,不需要引入新的jar包了

JSONObject messagePayload = new JSONObject();
Map<String, Object> custom = new HashMap<String, Object>();

messagePayload.put("title", "推送標題");
messagePayload.put("body", "推送內容");

messagePayload.put("user_id", 1);
messagePayload.put("msg_type", 1);
messagePayload.put("data", Lists.newArrayList("orderId", 1));

XingeApp xinge = new XingeApp(iosAccessId, iosSecretKey);
MessageIOS message = new MessageIOS();
message.setType(MessageIOS.TYPE_APNS_NOTIFICATION);
message.setExpireTime(86400);
message.setAlert(content);
message.setBadge(1);
message.setCategory("INVITE_CATEGORY");
message.setCustom(messagePayload);

response = xinge.pushSingleDevice("app使用者的pushToken", message, XingeApp.IOSENV_PROD);
if (response.getInt(RET_CODE) != 0) {
    // 推送異常了
}
複製程式碼

小程式推送

官方文件:mp.weixin.qq.com/wiki?t=reso…

在spring boot中訊息推送系統設計與實現
可以看到微信小程式推送介面是普通的post請求

小程式api地址:api.weixin.qq.com/cgi-bin/mes…

http請求,http請求庫可以參考:HttpUtil

釘釘推送

官方文件:open-doc.dingtalk.com/microapp/se… 程式碼示例

public static boolean send(String content, String title, Set<String> receivers) {
    try {
        HttpUtil.ResponseWrap result = HttpUtil.postWrap(ddUrl,
                "{\n"
                        + "     \"msgtype\": \"text\",\n"
                        + "     \"text\": {\"content\":\"" + title + "\r\n" + content + "\n|"
                        + receivers.stream().map(r -> "@" + r).collect(Collectors.joining(" ")) + "\"},\n"
                        + "    \"at\": {\n"
                        + "        \"atMobiles\": [" + receivers.stream().map(r -> "\"" + r + "\"").collect(Collectors.joining(",")) + "], \n"
                        + "        \"isAtAll\": false\n"
                        + "    }\n"
                        + " }");
        return result.getStatusCode() == 200;
    } catch (Exception e) {
        return false;
    }
}
複製程式碼

完整程式碼參考 DingTalkUtil.java

使用http請求就可以請求了

郵件推送

傳送郵件可以使用java的javax.mail庫,smtp協議傳送郵件

<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.7</version>
</dependency>
複製程式碼

示例程式碼參考:EmailSender.java

簡訊推送

簡訊服務商眾多,郵件一般有統一的smtp協議可以使用,簡訊沒有協議,但是一般使用http傳送簡訊 比如以下的簡訊服務商

  1. 253雲通訊:zz.253.com/api_doc/kai…
  2. 簡訊服務- 又拍雲: www.upyun.com/products/sm…
  3. 訊息&簡訊_MSGSMS_雲通訊_簡訊- 華為雲: www.huaweicloud.com/product/msg…

訊息佇列的特性

訊息佇列消費異常後會自動進行重試

一些注意的點

微信小程式每次支付可以生成一個推送碼,需要儲存到資料庫或者快取裡,並且每個碼只能推送3條訊息

因為訊息佇列的消費在訊息量大的時候具有一定的延時,這就為取消訊息推送提供了可能,可以為每條訊息生成一個唯一的uuid,取消的時候把這個uuid設計進redis裡,推送時檢查這個uuid是否在redis裡決定推送與否

雖然推送存在不可控制的異常,比如三方推送服務出現了異常,但是也存在呼叫方傳遞引數異常,可以推送介面呼叫的返回值判斷是否呼叫推送系統成功,也可以記錄到日誌裡,這樣在調查異常原因時就比較容易

訊息佇列預設的重試次數,消費時長是無法控制的,可以對訊息佇列的客戶端進行修改支援這個特性,參考:github.com/jibaole/spr… 核心邏輯是先給訊息設定一個最大消費次數和消費時長,然後當訊息消費次數和消費時長達到閾值時,直接置為成功

ios使用信鴿推送時,需要上傳開發證照和生產證照,這兩個證照至少需要上傳一個

在spring boot中訊息推送系統設計與實現

相關文章