點擊上方“芋道源碼”,選擇“設爲星標”
做積極的人,而不是積極廢人!
源碼精品專欄
摘要: 原創出處 http://www.iocoder.cn/Apollo/admin-server-send-release-message/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!
1. 概述
2. ReleaseMessage
3. MessageSender
4. ReleaseMessageListener
666. 彩蛋
1. 概述
老艿艿:本系列假定胖友已經閱讀過 《Apollo 官方 wiki 文檔》 。
本文接 《Apollo 源碼解析 —— Portal 發佈配置》 一文,分享配置發佈的第三步,Admin Service 發佈配置後,發送 ReleaseMessage 給各個Config Service 。
FROM 《Apollo配置中心設計》 的 2.1.1 發送ReleaseMessage的實現方式
Admin Service 在配置發佈後,需要通知所有的 Config Service 有配置發佈,從而 Config Service 可以通知對應的客戶端來拉取最新的配置。
從概念上來看,這是一個典型的消息使用場景,Admin Service 作爲 producer 發出消息,各個Config Service 作爲 consumer 消費消息。通過一個消息組件(Message Queue)就能很好的實現 Admin Service 和 Config Service 的解耦。
在實現上,考慮到 Apollo 的實際使用場景,以及爲了儘可能減少外部依賴,我們沒有采用外部的消息中間件,而是通過數據庫實現了一個簡單的消息隊列。
實現方式如下:
Admin Service 在配置發佈後會往 ReleaseMessage 表插入一條消息記錄,消息內容就是配置發佈的 AppId+Cluster+Namespace ,參見 DatabaseMessageSender 。
Config Service 有一個線程會每秒掃描一次 ReleaseMessage 表,看看是否有新的消息記錄,參見 ReleaseMessageScanner 。
Config Service 如果發現有新的消息記錄,那麼就會通知到所有的消息監聽器(ReleaseMessageListener),如 NotificationControllerV2 ,消息監聽器的註冊過程參見 ConfigServiceAutoConfiguration 。
NotificationControllerV2 得到配置發佈的 AppId+Cluster+Namespace 後,會通知對應的客戶端。
示意圖如下:
流程
本文分享第 1 + 2 + 3 步驟,在 apollo-biz
項目的 message
模塊實現。???? 第 4 步,我們在下一篇文章分享。
2. ReleaseMessage
com.ctrip.framework.apollo.biz.entity.ReleaseMessage
,不繼承 BaseEntity 抽象類,ReleaseMessage 實體。代碼如下:
@Entity
@Table(name = "ReleaseMessage")
public class ReleaseMessage {
/**
* 編號
*/
@Id
@GeneratedValue
@Column(name = "Id")
private long id;
/**
* 消息內容,通過 {@link com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator#generate(String, String, String)} 方法生成。
*/
@Column(name = "Message", nullable = false)
private String message;
/**
* 最後更新時間
*/
@Column(name = "DataChange_LastTime")
private Date dataChangeLastModifiedTime;
@PrePersist
protected void prePersist() {
if (this.dataChangeLastModifiedTime == null) {
dataChangeLastModifiedTime = new Date();
}
}
}
id
字段,編號,自增。message
字段,消息內容。通過 ReleaseMessageKeyGenerator 生成。胖友先跳到 「2.1 ReleaseMessageKeyGenerator」 看看具體實現。#dataChangeLastModifiedTime
字段,最後更新時間。#prePersist()
方法,若保存時,未設置該字段,進行補全。
2.1 ReleaseMessageKeyGenerator
com.ctrip.framework.apollo.biz.utils.ReleaseMessageKeyGenerator
,ReleaseMessage 消息內容( ReleaseMessage.message
)生成器。代碼如下:
public class ReleaseMessageKeyGenerator {
private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
public static String generate(String appId, String cluster, String namespace) {
return STRING_JOINER.join(appId, cluster, namespace);
}
}
#generate(...)
方法,將 appId
+ cluster
+ namespace
拼接,使用 ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR = "+"
作爲間隔,例如:"test+default+application"
。
因此,對於同一個 Namespace ,生成的消息內容是相同的。通過這樣的方式,我們可以使用最新的 ReleaseMessage
的 id
屬性,作爲 Namespace 是否發生變更的標識。而 Apollo 確實是通過這樣的方式實現,Client 通過不斷使用獲得到 ReleaseMessage
的 id
屬性作爲版本號,請求 Config Service 判斷是否配置發生了變化。???? 這裏胖友先留有一個印象,後面我們會再詳細介紹這個流程。
正因爲,ReleaseMessage 設計的意圖是作爲配置發生變化的通知,所以對於同一個 Namespace ,僅需要保留其最新的 ReleaseMessage 記錄即可。所以,在 「3.3 DatabaseMessageSender」 中,我們會看到,有後臺任務不斷清理舊的 ReleaseMessage 記錄。
2.2 ReleaseMessageRepository
com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository
,繼承 org.springframework.data.repository.PagingAndSortingRepository
接口,提供 ReleaseMessage 的數據訪問 給 Admin Service 和 Config Service 。代碼如下:
public interface ReleaseMessageRepository extends PagingAndSortingRepository<ReleaseMessage, Long> {
List<ReleaseMessage> findFirst500ByIdGreaterThanOrderByIdAsc(Long id);
ReleaseMessage findTopByOrderByIdDesc();
ReleaseMessage findTopByMessageInOrderByIdDesc(Collection<String> messages);
List<ReleaseMessage> findFirst100ByMessageAndIdLessThanOrderByIdAsc(String message, Long id);
@Query("select message, max(id) as id from ReleaseMessage where message in :messages group by message")
List<Object[]> findLatestReleaseMessagesGroupByMessages(@Param("messages") Collection<String> messages);
}
3. MessageSender
com.ctrip.framework.apollo.biz.message.MessageSender
,Message 發送者接口。代碼如下:
public interface MessageSender {
/**
* 發送 Message
*
* @param message 消息
* @param channel 通道(主題)
*/
void sendMessage(String message, String channel);
}
3.1 發佈配置
在 ReleaseController 的 #publish(...)
方法中,會調用 MessageSender#sendMessage(message, channel)
方法,發送 Message 。調用簡化代碼如下:
// send release message
// 獲得 Cluster 名
Namespace parentNamespace = namespaceService.findParentNamespace(namespace);
String messageCluster;
if (parentNamespace != null) { // 有父 Namespace ,說明是灰度發佈,使用父 Namespace 的集羣名
messageCluster = parentNamespace.getClusterName();
} else {
messageCluster = clusterName; // 使用請求的 ClusterName
}
// 發送 Release 消息
messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, messageCluster, namespaceName), Topics.APOLLO_RELEASE_TOPIC);
關於父 Namespace 部分的代碼,胖友看完灰度發佈的內容,再回過頭理解。
ReleaseMessageKeyGenerator#generate(appId, clusterName, namespaceName)
方法,生成 ReleaseMessage 的消息內容。使用 Topic 爲
Topics.APOLLO_RELEASE_TOPIC
。
3.2 Topics
com.ctrip.framework.apollo.biz.message.Topics
,Topic 枚舉。代碼如下:
public class Topics {
/**
* Apollo 配置發佈 Topic
*/
public static final String APOLLO_RELEASE_TOPIC = "apollo-release";
}
3.3 DatabaseMessageSender
com.ctrip.framework.apollo.biz.message.DatabaseMessageSender
,實現 MessageSender 接口,Message 發送者實現類,基於數據庫實現。
3.3.1 構造方法
/**
* 清理 Message 隊列 最大容量
*/
private static final int CLEAN_QUEUE_MAX_SIZE = 100;
/**
* 清理 Message 隊列
*/
private BlockingQueue<Long> toClean = Queues.newLinkedBlockingQueue(CLEAN_QUEUE_MAX_SIZE);
/**
* 清理 Message ExecutorService
*/
private final ExecutorService cleanExecutorService;
/**
* 是否停止清理 Message 標識
*/
private final AtomicBoolean cleanStopped;
@Autowired
private ReleaseMessageRepository releaseMessageRepository;
public DatabaseMessageSender() {
// 創建 ExecutorService 對象
cleanExecutorService = Executors.newSingleThreadExecutor(ApolloThreadFactory.create("DatabaseMessageSender", true));
// 設置 cleanStopped 爲 false
cleanStopped = new AtomicBoolean(false);
}
主要和清理 ReleaseMessage 相關的屬性。
3.3.2 sendMessage
1: @Override
2: @Transactional
3: public void sendMessage(String message, String channel) {
4: logger.info("Sending message {} to channel {}", message, channel);
5: // 僅允許發送 APOLLO_RELEASE_TOPIC
6: if (!Objects.equals(channel, Topics.APOLLO_RELEASE_TOPIC)) {
7: logger.warn("Channel {} not supported by DatabaseMessageSender!");
8: return;
9: }
10: // 【TODO 6001】Tracer 日誌
11: Tracer.logEvent("Apollo.AdminService.ReleaseMessage", message);
12: // 【TODO 6001】Tracer 日誌
13: Transaction transaction = Tracer.newTransaction("Apollo.AdminService", "sendMessage");
14: try {
15: // 保存 ReleaseMessage 對象
16: ReleaseMessage newMessage = releaseMessageRepository.save(new ReleaseMessage(message));
17: // 添加到清理 Message 隊列。若隊列已滿,添加失敗,不阻塞等待。
18: toClean.offer(newMessage.getId());
19: // 【TODO 6001】Tracer 日誌
20: transaction.setStatus(Transaction.SUCCESS);
21: } catch (Throwable ex) {
22: // 【TODO 6001】Tracer 日誌
23: logger.error("Sending message to database failed", ex);
24: transaction.setStatus(ex);
25: throw ex;
26: } finally {
27: // 【TODO 6001】Tracer 日誌
28: transaction.complete();
29: }
30: }
第 5 至 9 行:第 5 至 9 行:僅允許發送 APOLLO_RELEASE_TOPIC 。
第 16 行:調用
ReleaseMessageRepository#save(ReleaseMessage)
方法,保存 ReleaseMessage 對象。第 18 行:調用
toClean#offer(Long id)
方法,添加到清理 Message 隊列。若隊列已滿,添加失敗,不阻塞等待。關於 BlockingQueue 的知識,胖友可以看看 《阻塞隊列(BlockingQueue)》 。
3.3.3 清理 ReleaseMessage 任務
#initialize()
方法,通知 Spring 調用,初始化清理 ReleaseMessage 任務。代碼如下:
1: @PostConstruct
2: private void initialize() {
3: cleanExecutorService.submit(() -> {
4: // 若未停止,持續運行。
5: while (!cleanStopped.get() && !Thread.currentThread().isInterrupted()) {
6: try {
7: // 拉取
8: Long rm = toClean.poll(1, TimeUnit.SECONDS);
9: // 隊列非空,處理拉取到的消息
10: if (rm != null) {
11: cleanMessage(rm);
12: // 隊列爲空,sleep ,避免空跑,佔用 CPU
13: } else {
14: TimeUnit.SECONDS.sleep(5);
15: }
16: } catch (Throwable ex) {
17: // 【TODO 6001】Tracer 日誌
18: Tracer.logError(ex);
19: }
20: }
21: });
22: }
第 3 至 21 行:調用
ExecutorService#submit(Runnable)
方法,提交清理 ReleaseMessage 任務第 10 至 11 行:若拉取到消息編號,調用
#cleanMessage(Long id)
方法,處理拉取到的消息,即清理老消息們。第 13 至 15 行:若未拉取到消息編號,說明隊列爲空,sleep ,避免空跑,佔用 CPU 。
第 5 行:循環,直到停止。
第 8 行:調用
BlockingQueue#poll(long timeout, TimeUnit unit)
方法,拉取隊頭的消息編號。
#cleanMessage(Long id)
方法,清理老消息們。代碼如下:
1: private void cleanMessage(Long id) {
2: boolean hasMore = true;
3: // 查詢對應的 ReleaseMessage 對象,避免已經刪除。因爲,DatabaseMessageSender 會在多進程中執行。例如:1)Config Service + Admin Service ;2)N * Config Service ;3)N * Admin Service
4: // double check in case the release message is rolled back
5: ReleaseMessage releaseMessage = releaseMessageRepository.findOne(id);
6: if (releaseMessage == null) {
7: return;
8: }
9: // 循環刪除相同消息內容( `message` )的老消息
10: while (hasMore && !Thread.currentThread().isInterrupted()) {
11: // 拉取相同消息內容的 100 條的老消息
12: // 老消息的定義:比當前消息編號小,即先發送的
13: // 按照 id 升序
14: List<ReleaseMessage> messages = releaseMessageRepository.findFirst100ByMessageAndIdLessThanOrderByIdAsc(
15: releaseMessage.getMessage(), releaseMessage.getId());
16: // 刪除老消息
17: releaseMessageRepository.delete(messages);
18: // 若拉取不足 100 條,說明無老消息了
19: hasMore = messages.size() == 100;
20: // 【TODO 6001】Tracer 日誌
21: messages.forEach(toRemove -> Tracer.logEvent(
22: String.format("ReleaseMessage.Clean.%s", toRemove.getMessage()), String.valueOf(toRemove.getId())));
23: }
24: }
第 5 至 8 行:調用
ReleaseMessageRepository#findOne(id)
方法,查詢對應的 ReleaseMessage 對象,避免已經刪除。因爲,DatabaseMessageSender 會在多進程中執行。例如:1)Config Service + Admin Service ;2)N * Config Service ;3)N * Admin Service 。
爲什麼 Config Service 和 Admin Service 都會啓動清理任務呢????? 因爲 DatabaseMessageSender 添加了
@Component
註解,而 NamespaceService 注入了 DatabaseMessageSender 。而 NamespaceService 被apollo-adminservice
和apoll-configservice
項目都引用了,所以都會啓動該任務。第 10 至 23 行:循環刪除,相同消息內容(
ReleaseMessage.message
)的老消息,即 Namespace 的老消息。老消息的定義:比當前消息編號小,即先發送的。
第 14 至 15 行:調用
ReleaseMessageRepository#findFirst100ByMessageAndIdLessThanOrderByIdAsc(message, id)
方法,拉取相同消息內容的 100 條的老消息,按照 id 升序。第 17 行:調用
ReleaseMessageRepository#delete(messages)
方法,刪除老消息。第 19 行:若拉取不足 100 條,說明無老消息了。
第 21 至 22 行:【TODO 6001】Tracer 日誌
4. ReleaseMessageListener
com.ctrip.framework.apollo.biz.message.ReleaseMessageListener
,ReleaseMessage 監聽器接口。代碼如下:
public interface ReleaseMessageListener {
/**
* 處理 ReleaseMessage
*
* @param message
* @param channel 通道(主題)
*/
void handleMessage(ReleaseMessage message, String channel);
}
ReleaseMessageListener 實現子類如下圖:
例如,NotificationControllerV2 得到配置發佈的 AppId+Cluster+Namespace 後,會通知對應的客戶端。???? 具體的代碼實現,我們下一篇文章分享。
4.1 ReleaseMessageScanner
com.ctrip.framework.apollo.biz.message.ReleaseMessageScanner
,實現 org.springframework.beans.factory.InitializingBean
接口,ReleaseMessage 掃描器,被 Config Service 使用。
4.1.1 構造方法
@Autowired
private BizConfig bizConfig;
@Autowired
private ReleaseMessageRepository releaseMessageRepository;
/**
* 從 DB 中掃描 ReleaseMessage 表的頻率,單位:毫秒
*/
private int databaseScanInterval;
/**
* 監聽器數組
*/
private List<ReleaseMessageListener> listeners;
/**
* 定時任務服務
*/
private ScheduledExecutorService executorService;
/**
* 最後掃描到的 ReleaseMessage 的編號
*/
private long maxIdScanned;
public ReleaseMessageScanner() {
// 創建監聽器數組
listeners = Lists.newCopyOnWriteArrayList();
// 創建 ScheduledExecutorService 對象
executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ReleaseMessageScanner", true));
}
listeners
屬性,監聽器數組。通過#addMessageListener(ReleaseMessageListener)
方法,註冊 ReleaseMessageListener 。在 MessageScannerConfiguration 中,調用該方法,初始化 ReleaseMessageScanner 的監聽器們。代碼如下:@Configuration static class MessageScannerConfiguration { @Autowired private NotificationController notificationController; @Autowired private ConfigFileController configFileController; @Autowired private NotificationControllerV2 notificationControllerV2; @Autowired private GrayReleaseRulesHolder grayReleaseRulesHolder; @Autowired private ReleaseMessageServiceWithCache releaseMessageServiceWithCache; @Autowired private ConfigService configService; @Bean public ReleaseMessageScanner releaseMessageScanner() { ReleaseMessageScanner releaseMessageScanner = new ReleaseMessageScanner(); // 0. handle release message cache releaseMessageScanner.addMessageListener(releaseMessageServiceWithCache); // 1. handle gray release rule releaseMessageScanner.addMessageListener(grayReleaseRulesHolder); // 2. handle server cache releaseMessageScanner.addMessageListener(configService); releaseMessageScanner.addMessageListener(configFileController); // 3. notify clients releaseMessageScanner.addMessageListener(notificationControllerV2); releaseMessageScanner.addMessageListener(notificationController); return releaseMessageScanner; } }
4.1.2 初始化 Scan 任務
#afterPropertiesSet()
方法,通過 Spring 調用,初始化 Scan 任務。代碼如下:
1: @Override
2: public void afterPropertiesSet() {
3: // 從 ServerConfig 中獲得頻率
4: databaseScanInterval = bizConfig.releaseMessageScanIntervalInMilli();
5: // 獲得最大的 ReleaseMessage 的編號
6: maxIdScanned = loadLargestMessageId();
7: // 創建從 DB 中掃描 ReleaseMessage 表的定時任務
8: executorService.scheduleWithFixedDelay((Runnable) () -> {
9: // 【TODO 6001】Tracer 日誌
10: Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageScanner", "scanMessage");
11: try {
12: // 從 DB 中,掃描 ReleaseMessage 們
13: scanMessages();
14: // 【TODO 6001】Tracer 日誌
15: transaction.setStatus(Transaction.SUCCESS);
16: } catch (Throwable ex) {
17: // 【TODO 6001】Tracer 日誌
18: transaction.setStatus(ex);
19: logger.error("Scan and send message failed", ex);
20: } finally {
21: // 【TODO 6001】Tracer 日誌
22: transaction.complete();
23: }
24: }, databaseScanInterval, databaseScanInterval, TimeUnit.MILLISECONDS);
25: }
第 4 行:調用
BizConfig#releaseMessageScanIntervalInMilli()
方法,從 ServerConfig 中獲得頻率,單位:毫秒。可通過"apollo.message-scan.interval"
配置,默認:1000 ms 。第 6 行:調用
#loadLargestMessageId()
方法,獲得最大的 ReleaseMessage 的編號。代碼如下:/** * find largest message id as the current start point * * @return current largest message id */ private long loadLargestMessageId() { ReleaseMessage releaseMessage = releaseMessageRepository.findTopByOrderByIdDesc(); return releaseMessage == null ? 0 : releaseMessage.getId(); }
第 8 至 24 行:調用
ExecutorService#scheduleWithFixedDelay(Runnable)
方法,創建從 DB 中掃描 ReleaseMessage 表的定時任務。第 13 行:調用
#scanMessages()
方法,從 DB 中,掃描新的 ReleaseMessage 們。
#scanMessages()
方法,循環掃描消息,直到沒有新的 ReleaseMessage 爲止。代碼如下:
private void scanMessages() {
boolean hasMoreMessages = true;
while (hasMoreMessages && !Thread.currentThread().isInterrupted()) {
hasMoreMessages = scanAndSendMessages();
}
}
#scanAndSendMessages()
方法,掃描消息,並返回是否繼續有新的 ReleaseMessage 可以繼續掃描。代碼如下:
1: private boolean scanAndSendMessages() {
2: // 獲得大於 maxIdScanned 的 500 條 ReleaseMessage 記錄,按照 id 升序
3: // current batch is 500
4: List<ReleaseMessage> releaseMessages = releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
5: if (CollectionUtils.isEmpty(releaseMessages)) {
6: return false;
7: }
8: // 觸發監聽器
9: fireMessageScanned(releaseMessages);
10: // 獲得新的 maxIdScanned ,取最後一條記錄
11: int messageScanned = releaseMessages.size();
12: maxIdScanned = releaseMessages.get(messageScanned - 1).getId();
13: // 若拉取不足 500 條,說明無新消息了
14: return messageScanned == 500;
15: }
第 4 至 7 行:調用
ReleaseMessageRepository#findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned)
方法,獲得大於 maxIdScanned 的 500 條 ReleaseMessage 記錄,按照 id 升序。第 9 行:調用
#fireMessageScanned(List<ReleaseMessage> messages)
方法,觸發監聽器們。第 10 至 12 行:獲得新的
maxIdScanned
,取最後一條記錄。第 14 行:若拉取不足 500 條,說明無新消息了。
4.1.3 fireMessageScanned
#fireMessageScanned(List<ReleaseMessage> messages)
方法,觸發監聽器,處理 ReleaseMessage 們。代碼如下:
private void fireMessageScanned(List<ReleaseMessage> messages) {
for (ReleaseMessage message : messages) { // 循環 ReleaseMessage
for (ReleaseMessageListener listener : listeners) { // 循環 ReleaseMessageListener
try {
// 觸發監聽器
listener.handleMessage(message, Topics.APOLLO_RELEASE_TOPIC);
} catch (Throwable ex) {
Tracer.logError(ex);
logger.error("Failed to invoke message listener {}", listener.getClass(), ex);
}
}
}
}
666. 彩蛋
美滋滋,小乾貨一篇。
歡迎加入我的知識星球,一起探討架構,交流源碼。加入方式,長按下方二維碼噢:
已在知識星球更新源碼解析如下:
最近更新《芋道 SpringBoot 2.X 入門》系列,已經 20 餘篇,覆蓋了 MyBatis、Redis、MongoDB、ES、分庫分表、讀寫分離、SpringMVC、Webflux、權限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能測試等等內容。
提供近 3W 行代碼的 SpringBoot 示例,以及超 4W 行代碼的電商微服務項目。
獲取方式:點“在看”,關注公衆號並回復 666 領取,更多內容陸續奉上。