[從原始碼學設計]螞蟻金服SOFARegistry之訊息匯流排非同步處理
0x00 摘要
SOFARegistry 是螞蟻金服開源的一個生產級、高時效、高可用的服務註冊中心。
本系列文章重點在於分析設計和架構,即利用多篇文章,從多個角度反推總結 DataServer 或者 SOFARegistry 的實現機制和架構思路,讓大家藉以學習阿里如何設計。
本文為第五篇,介紹SOFARegistry訊息匯流排的非同步處理。
0x01 為何分離
前文我們講述了SOFARegistry的訊息匯流排,本文我們講講一個變種 DataChangeEventCenter。
DataChangeEventCenter 是被獨立出來的,專門處理資料變化相關的訊息。
為什麼要分離呢?因為:
- 從架構說,DataChangeEventCenter 是專門處理資料變化訊息,這是一種解耦;
- 從技術上來說,DataChangeEventCenter 也和 EventCenter 有具體實現技巧的不同,所以需要分開處理;
- 但更深入的原因是業務場景不同,下面分析中我們可以看出,DataChangeEventCenter 和業務耦合的相當緊密;
0x02 業務領域
2.1 應用場景
DataChangeEventCenter 的獨特業務場景如下:
- 需要提供歸併功能。即短期內會有多個通知來到,不需要逐一處理,只處理最後一個即可;
- 非同步處理訊息;
- 需要保證訊息順序;
- 有延遲操作;
- 需要提高處理能力,並行處理;
因此,DataChangeEventCenter 程式碼和業務聯絡相當緊密,前文的 EventCenter 已經不適合了。
2.2 延遲和歸併
關於延遲和歸併操作,我們單獨說明下。
2.2.1 業務特點
螞蟻金服業務的一個特點是:通過連線敏感的特性對服務當機做到秒級發現。
因此 SOFARegistry 在健康檢測的設計方面決定“服務資料與服務釋出者的實體連線繫結在一起,斷連馬上清資料”,簡稱此特點叫做連線敏感性。連線敏感性是指在 SOFARegistry 裡所有 Client 都與 SessionServer 保持長連線,每條長連線都設定基於 SOFABolt 的連線心跳,如果長連線斷連客戶端立即發起重新建連,時刻保持 Client 與 SessionServer 之間可靠的連線。
2.2.2 問題
但帶來了一個問題就是:可能因為網路問題,短期內會出現大量重新建連操作。比如只是網路問題導致連線斷開,實際的服務程式沒有當機,此時客戶端立即發起重新連線 SessionServer 並且重新註冊所有服務資料。
但是 假如此過程耗時足夠短暫(例如 500ms 內發生斷連和重連),服務訂閱者應該感受不到服務下線。從而 SOFARegistry 內部應該做相應處理。
2.2.3 解決
SOFARegistry 內部做了歸併和延遲操作來保證使用者不受影響。比如 DataServer 內部的資料通過 mergeDatum 延遲合併變更的 Publisher 服務資訊,version 是合併後最新的版本號。
對於 DataChangeEventCenter,就是通過訊息的延遲和歸併來協助完成這個功能。
2.3 螞蟻金服實現
下面是 DataChangeEventCenter 總體的功能描述:
- 當有資料釋出者 publisher 上下線時,會分別觸發 publishDataProcessor 或 unPublishDataHandler;
- Handler 首先會判斷當前節點的狀態:
- 若是非工作狀態則返回請求失敗;
- 若是工作狀態,Handler 會往 dataChangeEventCenter 中新增一個資料變更事件,則觸發資料變化事件中心 DataChangeEventCenter 的 onChange 方法。用於非同步地通知事件變更中心資料的變更;
- 事件變更中心收到該事件之後,會往佇列中加入事件。此時 dataChangeEventCenter 會根據不同的事件型別非同步地對上下線資料進行相應的處理;
- 與此同時,DataChangeHandler 會把這個事件變更資訊通過 ChangeNotifier 對外發布,通知其他節點進行資料同步;
0x03 DataChangeEventCenter
3.1 總述
DataChangeEventCenter具體分成四部分:
- Event Center:組織成訊息中心;
- Event Queue:用於多路分別處理,增加處理能力;
- Event Task:每一個Queue內部啟動一個執行緒,用於非同步處理,增加處理能力;
- Event Handler:用於處理內部ChangeData;
接下來我們一一介紹,因為 DataChangeEventCenter 和業務結合緊密,所以我們會深入結合業務進行講解。
3.2 DataChangeEventCenter
3.2.1 定義
DataChangeEventCenter 中維護著一個 DataChangeEventQueue 佇列陣列,這是核心。陣列中的每個元素是一個事件佇列。具體定義如下:
public class DataChangeEventCenter {
/**
* count of DataChangeEventQueue
*/
private int queueCount;
/**
* queues of DataChangeEvent
*/
private DataChangeEventQueue[] dataChangeEventQueues;
@Autowired
private DataServerConfig dataServerConfig;
@Autowired
private DatumCache datumCache;
}
3.2.2 訊息型別
DataChangeEventCenter 專門處理 IDataChangeEvent 型別訊息,其具體實現為三種:
- public class ClientChangeEvent implements IDataChangeEvent
- public class DataChangeEvent implements IDataChangeEvent
- public class DatumSnapshotEvent implements IDataChangeEvent
這些不同型別的訊息可以放入同一個佇列,具體放入哪個佇列,是根據特定判別方式來決定,比如根據Publisher的DataInfoId來做hash,以此決定放入哪個Queue。
即,當對應 handler 的 onChange 方法被觸發時,會計算該變化服務的 dataInfoId 的 Hash 值,從而進一步確定出該服務註冊資料所在的佇列編號,進而把該變化的資料封裝成一個資料變化物件,傳入到佇列中。
3.2.3 初始化
在初始化函式中,構建了EventQueue,每一個Queue啟動了一個執行緒,用來處理訊息。
@PostConstruct
public void init() {
if (isInited.compareAndSet(false, true)) {
queueCount = dataServerConfig.getQueueCount();
dataChangeEventQueues = new DataChangeEventQueue[queueCount];
for (int idx = 0; idx < queueCount; idx++) {
dataChangeEventQueues[idx] = new DataChangeEventQueue(idx, dataServerConfig, this,datumCache);
dataChangeEventQueues[idx].start();
}
}
}
3.2.4 Put 訊息
put訊息比較簡單,具體如何判別應該把Event放入哪一個Queue是根據具體方式來判斷,比如根據Publisher的DataInfoId來做hash,以此決定放入哪個Queue:
int idx = hash(publisher.getDataInfoId());
Datum datum = new Datum(publisher, dataCenter);
dataChangeEventQueues[idx].onChange(new DataChangeEvent(DataChangeTypeEnum.MERGE,
DataSourceTypeEnum.PUB, datum));
3.2.5 如何處理訊息
具體是通過 dataChangeEventQueues.onChange 來做處理,比如如下幾個函式,分別處理不同的訊息型別。具體都是找到queue,然後呼叫:
public void onChange(Publisher publisher, String dataCenter) {
int idx = hash(publisher.getDataInfoId());
Datum datum = new Datum(publisher, dataCenter);
if (publisher instanceof UnPublisher) {
datum.setContainsUnPub(true);
}
if (publisher.getPublishType() != PublishType.TEMPORARY) {
dataChangeEventQueues[idx].onChange(new DataChangeEvent(DataChangeTypeEnum.MERGE,
DataSourceTypeEnum.PUB, datum));
} else {
dataChangeEventQueues[idx].onChange(new DataChangeEvent(DataChangeTypeEnum.MERGE,
DataSourceTypeEnum.PUB_TEMP, datum));
}
}
public void onChange(ClientChangeEvent event) {
for (DataChangeEventQueue dataChangeEventQueue : dataChangeEventQueues) {
dataChangeEventQueue.onChange(event);
}
}
public void onChange(DatumSnapshotEvent event) {
for (DataChangeEventQueue dataChangeEventQueue : dataChangeEventQueues) {
dataChangeEventQueue.onChange(event);
}
}
public void sync(DataChangeTypeEnum changeType, DataSourceTypeEnum sourceType, Datum datum) {
int idx = hash(datum.getDataInfoId());
DataChangeEvent event = new DataChangeEvent(changeType, sourceType, datum);
dataChangeEventQueues[idx].onChange(event);
}
3.3 DataChangeEvent
因為 DataChangeEvent 最常用,所以我們單獨拿出來說明。
DataChangeEvent會根據DataChangeTypeEnum和DataSourceTypeEnum來進行區分,就是處理型別和訊息來源。
DataChangeTypeEnum具體分為:
- MERGE,如果變更型別是MERGE,則會更新快取中需要更新的新Datum,並且更新版本號;
- COVER,如果變更型別是 COVER,則會覆蓋原有的快取;
DataSourceTypeEnum 具體分為:
- PUB :pub by client;
- PUB_TEMP :pub temporary data;
- SYNC:sync from dataservers in other datacenter;
- BACKUP:from dataservers in the same datacenter;
- CLEAN:local dataInfo check,not belong this node schedule remove;
- SNAPSHOT:Snapshot data, after renew finds data inconsistent;
具體定義如下:
public class DataChangeEvent implements IDataChangeEvent {
/**
* type of changed data, MERGE or COVER
*/
private DataChangeTypeEnum changeType;
private DataSourceTypeEnum sourceType;
/**
* data changed
*/
private Datum datum;
}
3.4 DataChangeEventQueue
DataChangeEventQueue 是這個子模組的核心,用於多路分別處理,增加處理能力。每一個Queue內部啟動一個執行緒,用於非同步處理,也能增加處理能力。
3.4.1 核心變數
這裡的核心是:
-
BlockingQueue
eventQueue; -
Map<String, Map<String, ChangeData>> CHANGE_DATA_MAP_FOR_MERGE = new ConcurrentHashMap<>();
-
DelayQueue
CHANGE_QUEUE = new DelayQueue();
講解如下:
- 可以看到,這裡操作的資料型別是ChangeData,把Datum轉換成 ChangeData 可以把訊息處理方式 或者 來源統一起來處理;
- eventQueue 用來儲存投放的訊息,所有訊息block在queue上,這可以保證訊息的順序處理;
- CHANGE_DATA_MAP_FOR_MERGE。顧名思義,主要處理訊息歸併。這是按照 dataCenter,dataInfoId 作為維度,分別儲存 ChangeData,可以理解為一個矩陣Map,使用putIfAbsent方法新增鍵值對,如果map集合中沒有該key對應的值,則直接新增,並返回null,如果已經存在對應的值,則依舊為原來的值。這樣如果短期內向map中新增多個訊息,這樣就對多餘的訊息做了歸併;
- CHANGE_QUEUE 的作用是用於統一處理投放的ChangeData,無論是哪個 data center的資料,都會統一在這裡處理;這裡需要注意的是使用了DelayQueue來進行延遲操作,就是我們之前業務中提到的延遲操作;
具體定義如下:
public class DataChangeEventQueue {
private final String name;
/**
* a block queue that stores all data change events
*/
private final BlockingQueue<IDataChangeEvent> eventQueue;
private final Map<String, Map<String, ChangeData>> CHANGE_DATA_MAP_FOR_MERGE = new ConcurrentHashMap<>();
private final DelayQueue<ChangeData> CHANGE_QUEUE = new DelayQueue();
private final int notifyIntervalMs;
private final int notifyTempDataIntervalMs;
private final ReentrantLock lock = new ReentrantLock();
private final int queueIdx;
private DataServerConfig dataServerConfig;
private DataChangeEventCenter dataChangeEventCenter;
private DatumCache datumCache;
}
3.4.2 啟動和引擎
DataChangeEventQueue#start 方法在 DataChangeEventCenter 初始化的時候被一個新的執行緒呼叫,該執行緒會源源不斷地從佇列中獲取新增事件,並且進行分發。新增資料會由此新增進節點內,實現分片。因為 eventQueue 是一個 BlockingQueue,所以可以使用while (true)來控制。
當event被取出之後,會根據 DataChangeScopeEnum.DATUM 的不同,會做不同的處理。
- 如果是DataChangeScopeEnum.DATUM,則判斷dataChangeEvent.getSourceType();
- 如果是 DataSourceTypeEnum.PUB_TEMP,則addTempChangeData,就是往CHANGE_QUEUE新增ChangeData;
- 如果不是,則handleDatum;
- 如果是DataChangeScopeEnum.CLIENT,則handleClientOff((ClientChangeEvent) event);
- 如果是DataChangeScopeEnum.SNAPSHOT,則handleSnapshot((DatumSnapshotEvent) event);
具體程式碼如下:
public void start() {
Executor executor = ExecutorFactory
.newSingleThreadExecutor(String.format("%s_%s", DataChangeEventQueue.class.getSimpleName(), getName()));
executor.execute(() -> {
while (true) {
try {
IDataChangeEvent event = eventQueue.take();
DataChangeScopeEnum scope = event.getScope();
if (scope == DataChangeScopeEnum.DATUM) {
DataChangeEvent dataChangeEvent = (DataChangeEvent) event;
//Temporary push data will be notify as soon as,and not merge to normal pub data;
if (dataChangeEvent.getSourceType() == DataSourceTypeEnum.PUB_TEMP) {
addTempChangeData(dataChangeEvent.getDatum(), dataChangeEvent.getChangeType(),
dataChangeEvent.getSourceType());
} else {
handleDatum(dataChangeEvent.getChangeType(), dataChangeEvent.getSourceType(),
dataChangeEvent.getDatum());
}
} else if (scope == DataChangeScopeEnum.CLIENT) {
handleClientOff((ClientChangeEvent) event);
} else if (scope == DataChangeScopeEnum.SNAPSHOT) {
handleSnapshot((DatumSnapshotEvent) event);
}
}
}
});
}
具體如下圖:
+----------------------------+
| DataChangeEventCenter |
| |
| +-----------------------+ |
| | DataChangeEventQueue[]| |
| +-----------------------+ |
+----------------------------+
|
|
v
+------------------+------------------------+
| DataChangeEventQueue |
| |
| +---------------------------------------+ |
| | | |
| | BlockingQueue<IDataChangeEvent> +-------------+
| | | | |
| | | | +-v---------+
| | Map<String, Map<String, ChangeData<> | | <--> | |
| | | | | Executor |
| | | | | |
| | start +------------------------------> | |
| | | | +-+---------+
| | | | |
| | DelayQueue<ChangeData> <-------------------+
| | | |
| +---------------------------------------+ |
+-------------------------------------------+
3.4.3 ChangeData
handleDatum 具體處理是把Datum轉換為 ChangeData來處理,
為什麼要轉換成 ChangeData來儲存呢。
因為無論是訊息處理方式或者來源,都有不同的型別。比如在 NotifyFetchDatumHandler . fetchDatum 函式中,會先從其他 data server 獲取 Datum,然後會根據 Datum 向dataChangeEventCenter中投放訊息,通知本 Data Server 進行 BACKUP 操作,型別是 COVER 型別。
轉換成 ChangeData就可以把訊息處理方式或者來源統一起來處理。
使用者會儲存一個包含 datum 的訊息。
dataChangeEventCenter.sync(DataChangeTypeEnum.COVER, DataSourceTypeEnum.BACKUP, datum);
DataChangeEventQueue 會從 DataChangeEvent 中獲取 Datum,然後把 Datum 轉換為 ChangeData,儲存起來。
private void handleDatum(DataChangeTypeEnum changeType, DataSourceTypeEnum sourceType,
Datum targetDatum) {
//get changed datum
ChangeData changeData = getChangeData(targetDatum.getDataCenter(),
targetDatum.getDataInfoId(), sourceType, changeType);
Datum cacheDatum = changeData.getDatum();
if (changeType == DataChangeTypeEnum.COVER || cacheDatum == null) {
changeData.setDatum(targetDatum);
}
}
ChangeData 定義如下:
public class ChangeData implements Delayed {
/** data changed */
private Datum datum;
/** change time */
private Long gmtCreate;
/** timeout */
private long timeout;
private DataSourceTypeEnum sourceType;
private DataChangeTypeEnum changeType;
}
3.4.4 處理Datum
3.4.4.1 加入Datum
這裡是處理真實ChangeData快取,以及新加入的Datum。
- 首先從 CHANGE_DATA_MAP_FOR_MERGE 獲取之前儲存的變更的ChangeData,如果沒有,就生成一個加入,此時要為後續可能的歸併做準備;
- 拿到ChangeData之後
- 如果變更型別是 COVER,則會覆蓋原有的快取。changeData.setDatum(targetDatum);
- 否則是MERGE,則會更新快取中需要更新的新Datum,並且更新版本號;
具體如下:
private void handleDatum(DataChangeTypeEnum changeType, DataSourceTypeEnum sourceType,
Datum targetDatum) {
lock.lock();
try {
//get changed datum
ChangeData changeData = getChangeData(targetDatum.getDataCenter(),
targetDatum.getDataInfoId(), sourceType, changeType);
Datum cacheDatum = changeData.getDatum();
if (changeType == DataChangeTypeEnum.COVER || cacheDatum == null) {
changeData.setDatum(targetDatum);
} else {
Map<String, Publisher> targetPubMap = targetDatum.getPubMap();
Map<String, Publisher> cachePubMap = cacheDatum.getPubMap();
for (Publisher pub : targetPubMap.values()) {
String registerId = pub.getRegisterId();
Publisher cachePub = cachePubMap.get(registerId);
if (cachePub != null) {
// if the registerTimestamp of cachePub is greater than the registerTimestamp of pub, it means
// that pub is not the newest data, should be ignored
if (pub.getRegisterTimestamp() < cachePub.getRegisterTimestamp()) {
continue;
}
// if pub and cachePub both are publisher, and sourceAddress of both are equal,
// and version of cachePub is greater than version of pub, should be ignored
if (!(pub instanceof UnPublisher) && !(cachePub instanceof UnPublisher)
&& pub.getSourceAddress().equals(cachePub.getSourceAddress())
&& cachePub.getVersion() > pub.getVersion()) {
continue;
}
}
cachePubMap.put(registerId, pub);
cacheDatum.setVersion(targetDatum.getVersion());
}
}
} finally {
lock.unlock();
}
}
3.4.4.2 提出Datum
當提取時候,使用take函式,從CHANGE_QUEUE 和 CHANGE_DATA_MAP_FOR_MERGE 提出ChangeData。
public ChangeData take() throws InterruptedException {
ChangeData changeData = CHANGE_QUEUE.take();
lock.lock();
try {
removeMapForMerge(changeData);
return changeData;
} finally {
lock.unlock();
}
}
具體提取Datum會在DataChangeHandler。
3.5 DataChangeHandler
DataChangeHandler 會定期提取DataChangeEventCenter中的訊息,然後進行處理,主要功能就是執行ChangeNotifier 來通知相關模組:hi,這裡有新資料變化來到了,兄弟們走起來。
3.5.1 類定義
public class DataChangeHandler {
@Autowired
private DataServerConfig dataServerConfig;
@Autowired
private DataChangeEventCenter dataChangeEventCenter;
@Autowired
private DatumCache datumCache;
@Resource
private List<IDataChangeNotifier> dataChangeNotifiers;
}
3.5.2 執行引擎ChangeNotifier
DataChangeHandler 會遍歷 DataChangeEventCenter 中所有 DataChangeEventQueue,然後從 DataChangeEventQueue 之中取出ChangeData,針對每一個ChangeData,生成一個ChangeNotifier。
每個ChangeNotifier都是一個處理執行緒。
每個 dataChangeEventQueue 生成了 5 個 ChangeNotifier。
@PostConstruct
public void start() {
DataChangeEventQueue[] queues = dataChangeEventCenter.getQueues();
int queueCount = queues.length;
Executor executor = ExecutorFactory.newFixedThreadPool(queueCount, DataChangeHandler.class.getSimpleName());
Executor notifyExecutor = ExecutorFactory
.newFixedThreadPool(dataServerConfig.getQueueCount() * 5, this.getClass().getSimpleName());
for (int idx = 0; idx < queueCount; idx++) {
final DataChangeEventQueue dataChangeEventQueue = queues[idx];
final String name = dataChangeEventQueue.getName();
executor.execute(() -> {
while (true) {
final ChangeData changeData = dataChangeEventQueue.take();
notifyExecutor.execute(new ChangeNotifier(changeData, name));
}
});
}
}
3.5.3 Notify
我們回顧下業務:
當有資料釋出者 publisher 上下線時,會分別觸發 publishDataProcessor 或 unPublishDataHandler ,Handler 會往 dataChangeEventCenter 中新增一個資料變更事件,用於非同步地通知事件變更中心資料的變更。事件變更中心收到該事件之後,會往佇列中加入事件。此時 dataChangeEventCenter 會根據不同的事件型別非同步地對上下線資料進行相應的處理。
對於 ChangeData,會生成 ChangeNotifier 進行處理。會把這個事件變更資訊通過 ChangeNotifier 對外發布,通知其他節點進行資料同步。
private class ChangeNotifier implements Runnable {
private ChangeData changeData;
private String name;
@Override
public void run() {
if (changeData instanceof SnapshotData) {
......
} else {
Datum datum = changeData.getDatum();
String dataCenter = datum.getDataCenter();
String dataInfoId = datum.getDataInfoId();
DataSourceTypeEnum sourceType = changeData.getSourceType();
DataChangeTypeEnum changeType = changeData.getChangeType();
if (changeType == DataChangeTypeEnum.MERGE
&& sourceType != DataSourceTypeEnum.BACKUP
&& sourceType != DataSourceTypeEnum.SYNC) {
//update version for pub or unPub merge to cache
//if the version product before merge to cache,it may be cause small version override big one
datum.updateVersion();
}
long version = datum.getVersion();
try {
if (sourceType == DataSourceTypeEnum.CLEAN) {
if (datumCache.cleanDatum(dataCenter, dataInfoId)) {
......
}
} else if (sourceType == DataSourceTypeEnum.PUB_TEMP) {
notifyTempPub(datum, sourceType, changeType);
} else {
MergeResult mergeResult = datumCache.putDatum(changeType, datum);
Long lastVersion = mergeResult.getLastVersion();
if (lastVersion != null
&& lastVersion.longValue() == LocalDatumStorage.ERROR_DATUM_VERSION) {
return;
}
//lastVersion null means first add datum
if (lastVersion == null || version != lastVersion) {
if (mergeResult.isChangeFlag()) {
notify(datum, sourceType, lastVersion);
}
}
}
}
}
}
}
notify函式會遍歷dataChangeNotifiers
private void notify(Datum datum, DataSourceTypeEnum sourceType, Long lastVersion) {
for (IDataChangeNotifier notifier : dataChangeNotifiers) {
if (notifier.getSuitableSource().contains(sourceType)) {
notifier.notify(datum, lastVersion);
}
}
}
對應的Bean是:
@Bean(name = "dataChangeNotifiers")
public List<IDataChangeNotifier> dataChangeNotifiers() {
List<IDataChangeNotifier> list = new ArrayList<>();
list.add(sessionServerNotifier());
list.add(tempPublisherNotifier());
list.add(backUpNotifier());
return list;
}
至於如何處理通知,我們後續會撰文處理。
至此,DataChangeEventCenter 整體邏輯如下圖所示
+----------------------------+
| DataChangeEventCenter |
| |
| +-----------------------+ |
| | DataChangeEventQueue[]| |
| +-----------------------+ |
+----------------------------+
|
|
v
+------------------+------------------------+
| DataChangeEventQueue |
| |
| +---------------------------------------+ |
| | | |
| | BlockingQueue<IDataChangeEvent> +-------------+
| | | | |
| | | | +-v---------+
| | Map<String, Map<String, ChangeData<> | | <--> | |
| | | | | Executor |
| | | | | |
| | start +------------------------------> | |
| | | | +-+---------+
| | | | |
+----------------+ DelayQueue<ChangeData> <-------------------+
| | | | |
| | +---------------------------------------+ |
| +-------------------------------------------+
|
|
| +--------------------------+
| take | | notify +-------------------+
+-------> | DataChangeHandler | +---------> |dataChangeNotifiers|
| | +-------------------+
+--------------------------+
手機如下圖:
0x04 結論
因為獨特的業務場景,所以阿里把 DataChangeEventCenter 單獨分離出來,滿足了以下業務需求。如果大家在實際工作中有類似的需求,可以參考借鑑,具體處理方式如下:
- 需要提高處理能力,並行處理;
- 用queue陣列實現,每一個Queue都可以處理訊息,增加處理能力;
- 非同步處理訊息;
- 每一個Queue內部啟動一個執行緒,用於非同步處理;
- 需要保證訊息順序;
- eventQueue 用來儲存投放的訊息,所有訊息block在queue上,這可以保證訊息的順序處理;
- 有延遲操作;
- 使用了DelayQueue來進行延遲操作;
- 需要歸併操作,即短期內會有多個通知來到,不需要逐一處理,只處理最後一個即可;
- 使用putIfAbsent方法新增鍵值對,如果map集合中沒有該key對應的值,則直接新增,並返回null,如果已經存在對應的值,則依舊為原來的值。這樣如果短期內向map中新增多個訊息,這樣就對多餘的訊息做了歸併;