最近的工作中心是容易擴容,剛剛把其它一個業務使用 sharding jdbc 把容量規劃完成。因爲系統採用的數據方案是:單寫老庫 -> 雙寫老庫分片庫 -> 單寫分片庫,使用 apollo 配置中心來切換數據寫入規則。當有異常時可以進行方案回滾這種平滑的數據過渡方案,在上線過程中不可避免的會有中心狀態,雙寫老庫和分片庫。
1、數據對比的方案變遷
在系統處於中心狀態的過程中,老庫數據和分片庫數據的一致性尤爲重要。所以需要對老庫和分片庫的數據進行對比。數據對比方案經歷過以下幾個階段:
- 系統設計的最初時候,我的方案是人工定時從老庫和分片庫的從庫和下載某時間段的數據,然後通過程序對比(當前沒有考慮到可以使用 sharding-proxy)
- 項目總監提出沒有必要通過數據對比,只需要通過工具 Beyong Compare,定時從老庫和分片庫下載時間段數據通過 Beyong Compare 進行數據對比
- 上一個階段裏面有一個關鍵詞,那就是 定時。所以就對上面一個方案進行了優化,通過使用定時任務框架 Elastic Job,定時從老庫和分片庫(Sharding-proxy) 拉取數據進行比對,然後把對比結果寫入系統指定目錄中。通過查看目錄的日誌判斷中間狀態是否有問題
- 上一個階段裏面還是需要人工通過跳板機登陸到服務器上查看日誌比對結果,這種方案還是比較麻煩。其實我們日常使用的辦公軟件是釘釘,可以通過釘釘的羣機器人來進行業務報警。當我們通過 Elastic Job 中定時比對好結果的時候,通過可通過釘釘提供的 Webhook 來發放數據對比信息到業務羣。
系統方案一步一步進行優化,最終通過定時任務加釘釘羣報警方式來檢測在系統處於中間狀態時,老庫和新庫的數據是否一致。
2、創建釘釘羣機器人
2.1 釘釘羣機器人
羣機器人是釘釘爲用戶提供的智能羣助手,幫助羣裏溝通協同更加高效。在【羣設置】 - 【智能羣助手】可以找到。目前羣裏支持最多添加6個機器人。
可以使用釘釘官方機器人小釘,也可以添加Github等機器人,還可以添加企業機器人或者自定義機器人。機器人是高效安全的,您請放心使用。
羣成員可以在【手機釘釘】-【羣聊】-【羣設置】-【智能羣助手】-【添加更多【選擇機器人添加】。
2.2 獲取自定義機器人webhook
步驟一,打開機器人管理頁面。以PC端爲例,打開PC端釘釘,點擊頭像,選擇“機器人管理”。
步驟二,在機器人管理頁面選擇“自定義”機器人,輸入機器人名字並選擇要發送消息的羣,同時可以爲機器人設置機器人頭像。
步驟三,完成必要的安全設置(至少選擇一種),勾選 我已閱讀並同意《自定義機器人服務及免責條款》,點擊“完成”
步驟四,完成安全設置後,複製出機器人的Webhook地址,可用於向這個羣發送消息,格式如下:
https://oapi.dingtalk.com/robot/send?access_token=XXXXXX
更多信息請查看 釘釘開發文檔。
步驟五,在 Linux 服務器上進行測試。
curl 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text","text": {"content": "compare 測試數據,可忽略"}}'
釘釘羣收到該信息,說明地址和數據格式沒有問題。
3、數據對比服務開發
數據對比服務開發,主要的工作就是開發一個 Elastic Job Simple 類型的定時任務,並配置完成。包含任務註冊中心 zookeeper 以及 job 定義的配置。定時任務的邏輯主要是以下幾個步驟:
- 查詢老庫數據,如果數據爲空直接返回
- 查詢分片庫數據,並把分片庫數據轉換爲以業務主鍵爲 id 爲 key,業務對象爲 value 的 Map。
- 遍歷查詢出老庫的數據,通過老庫的業務主鍵查詢分片庫查詢結果轉換的 Map。如果查詢結果爲空,就把老庫的結果添加到 StringBuilder 當中,並追加分片庫數據爲空。如果查詢結果不爲空,對比兩個對象的值,如果不相等就把新庫的對象信息與新庫的對象信息添加到 StringBuilder 當中。在業務比對的開始階段添加一個標記位,判斷是否全部數據是否一致。當分片庫數據爲空或者老庫數據與分片庫數據不一致時,這個標記位設置爲 false。當標記位爲 true,StringBuilder 追加新庫與老庫數據完全一致。
- 發送 StringBuilder 信息到釘釘
發送釘釘信息的類:
3.1 DingMessageRequest
發送釘釘消息請求類。
DingMessageRequest
@Getter
@Setter
public class DingMessageRequest {
private String msgtype;
private MessageText text;
}
3.2 MessageText
具體消息包裝類
MessageText
@Getter
@Setter
public class MessageText {
private String content;
}
3.3 RestTemplateConfig
http 發送模板配置類,使用的是 okhttp。
@Configuration
public class RestTemplateConfig {
@Bean("restTemplate")
public RestTemplate restTemplate(){
return new RestTemplate(requestFactory());
}
private OkHttp3ClientHttpRequestFactory requestFactory(){
return new OkHttp3ClientHttpRequestFactory(okHttpClient());
}
private OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory(), x509TrustManager())
.retryOnConnectionFailure(false)
.connectionPool(connectionPoll())
.connectTimeout(10L, TimeUnit.SECONDS)
.readTimeout(10L, TimeUnit.SECONDS)
.build();
}
private X509TrustManager x509TrustManager() {
return new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
}
private SSLSocketFactory sslSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
return sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return null;
}
private ConnectionPool connectionPoll() {
return new ConnectionPool(200, 5, TimeUnit.MINUTES);
}
}
3.4 釘釘消息響應類
當發送消息到 webhook 地址時,如果響應的 errcode 爲 0 表示消息發送成功
DingMessageResponse
@Getter
@Setter
public class DingMessageResponse {
private int errcode;
private String errmsg;
}
3.5 釘釘消息發送類
這個類主要是發送消息到羣 webhook,並且處理響應結果
@Slf4j
@Service("dingDingMessageProcessor")
public class DingDingMessageProcessor implements MessageProcessor {
@Resource
private RestTemplate restTemplate;
@Value("${sharding.dingding.group}")
private String shardingDingDingGroup;
@Override
public void process(String fileType, String message) {
log.info("DingDingServiceImpl#sendMessage message is {}", message);
DingMessageRequest request = new DingMessageRequest();
request.setMsgtype("text");
MessageText text = new MessageText();
text.setContent(message);
request.setText(text);
DingMessageResponse response = restTemplate.postForObject(shardingDingDingGroup, request, DingMessageResponse.class);
if(response == null) {
log.error("DingDingServiceImpl#sendMessage send message response is null");
}
if(response.getErrcode() == 0){
log.info("DingDingServiceImpl#sendMessage send success");
} else {
log.error("DingDingServiceImpl#sendMessage send fail {}", response.getErrmsg());
}
}
}
釘釘對比結果顯示:
- 正常結果
- 異常結果
參考文章: