前言
循環依賴分為2類:
- RPC服務間(dubbo、http)循環依賴
- 應用間循環依賴
-
Dubbo缺省會在啟動時檢查依賴的服務是否可用,不可用時會拋出異常,防止Spring初始化完成。這種情況我們就叫做RPC服務間循環依賴。出現了循環依賴,必須有一方先啟動。所以這種問題是一定需要解決的。
-
應用間循環依賴大致情況如下: A應用調用B應用的服務,B應用也會調用A應用的服務,無論是間接調用還是直接調用。 這種循環依賴剛開始不會出現問題 ,但隨着代碼變更,有可能會發展為RPC服務間循環依賴。
可以通過check=”false”關閉檢查來避免 Dubbo的循環依賴的報錯,但是我認為這個只是權益之計。
應用間循環依賴
當前我們應用中並沒有出現RPC服務間循環調用,但是出現了應用間循環調用。下面就是這個🌰就是這種情況,我簡單描述下大致的情況。
- marketing-base會調用site-base服務中的SiteGroupService、SiteService接口用來查詢集團、店鋪、微店的信息。
- site-base里有監聽集團初始化的消息,然后執行集團初始化,過程中會調用marketing-base的服務以初始化文章、海報數據。
上面這個例子是應用間循環依賴,一不小心可能寫出暴雷的代碼。這里應該盡快把這個坑填掉。
處理方案
針對應用間循環依賴,大致的解決辦法
團隊協作模式:
- 重新划分歸屬,把職責給一個方向
- 共享內核(提出一個公共服務)
通信集成模式:
- MQ解耦服務間的依賴
由於上述的2個服務早已開發完畢,且比較成熟的應用,而且直接的依賴還是比較少的。此時MQ看起來是一個比較好的方案了。
這里考慮到site-base
比marketing-base
是更加基礎的服務,所以圖一中上邊的調用流程不變,下方更改為site-base
發送消息,然后marketing-base
來消費消息的方式。如圖二所示。
Show me Code
首先,我們需要向運維或者自己在單機環境下,新創建一個Topic。我這里新創建一個名叫: newcar_siteinit_basicinfo_test 的主題。
生產者配置
在config/user/beans中添加一個spring xml 配置文件 application-mq.xml。
application-mq.xml
1 <!--ons config-->
2 <bean id="producerConfig" class="com.souche.optimus.mq.aliyunons.ONSProducerConnConfig">
3 <property name="accessKey" value="${ons.access.key}"/>
4 <property name="secretKey" value="${ons.secret.key}"/>
5 </bean>
6
7 <!-- 集團初始化 發送者 -->
8 <bean id="groupInitProducerInvoker" class="com.souche.optimus.mq.aliyunons.ONSProducerInvoker">
9 <property name="producerId" value="${ons.groupinit.producer.id}"/>
10 <property name="config" ref="producerConfig"/>
11 </bean>
12
13 <bean id="groupInitProducer" class="com.souche.optimus.mq.aliyunons.ONSProducer">
14 <property name="topic" value="${ons.groupinit.topic}"/>
15 <property name="invoker" ref="groupInitProducerInvoker"/>
16 </bean>
ons.properties
增加ons.properties 在config/optimus/properties文件夾下
1ons.access.key = K8pfCPRU6gL2lldi
2ons.secret.key = U3lYVGl3L9nb23cEMogWcUVziLJ2T7
3
4#初始化主題
5ons.groupinit.topic = newcar_siteinit_basicinfo_test
6ons.groupinit.producer.id = PID_newcar_siteinit_basicinfo_test
生產者邏輯代碼
1private void sendMsg(SiteGroupInfoDO siteGroupInfoDO) {
2 if (siteGroupInfoDO.getId() == null) {
3 return;
4 }
5 Map<String, Object> map = new HashMap<>();
6 map.put("id",siteGroupInfoDO.getId());
7 map.put("groupCode",siteGroupInfoDO.getGroupCode());
8 map.put("groupName",siteGroupInfoDO.getGroupName());
9 map.put("appName",siteGroupInfoDO.getAppName());
10 map.put("domainSuffix",siteGroupInfoDO.getDomainSuffix());
11 String uuid = UUIDUtil.getID();
12 mqProducer.send(map, uuid, CommonConstant.MQ_TAG);
13 LOGGER.info("發送消息:body: {}, keys: {}, tag: {}", map, uuid, CommonConstant.MQ_TAG);
14 }
- 每個消息都需要有一個唯一的業務ID, 即第二個參數, 也是強制要求傳入的. 可以用訂單ID, 用戶ID, 如果不好確定就直接用UUIDUtil.getId()
- 第三個參數, 盡量用一個隊列的不同的tag去區分一個業務/系統中的不同消息, 盡量不要用*或者默認的值
消費者配置
application-mq.xml
1 <bean id="contentPlatformMsgConsumer" class="com.souche.marketing.base.biz.mq.ContentPlatformMsgConsumer"/>
2 <bean id="tgcArticleInvoker" class="com.souche.optimus.mq.aliyunons.ONSConsumerInvoker">
3 <property name="config" ref="consumerConfig"/>
4 <property name="reciver" ref="contentPlatformMsgConsumer"/>
5 <property name="consumerId" value="${mq.ons.consumer.newcar.article.id}"/>
6 <property name="topic" value="${mq.ons.consumer.newcar.article.topic}"/>
7 <!--<property name="tag" value="${mq.aliyun.car.center.tag}"/> <!–tag選填, 如果不填將監聽該topic下的所有消息–>-->
8 <property name="enabled" value="true"/>
9 </bean>
這里有一個很重要的點是,我們的消費者ID必須從阿里雲控制台上建立或者聯系運維創建,圖示如下:
ons.properties
1ons.access.key = K8pfCPRU6gL2lldi
2ons.secret.key = U3lYVGl3L9nb23cEMogWcUVziLJ2T7
3
4#初始化主題
5mq.ons.consumer.newcar.siteinit.topic = newcar_siteinit_basicinfo_test
6mq.ons.consumer.newcar.siteinit.id = GID_newcar_siteinit_basicinfo_test
消費者邏輯代碼
1/**
2 * @author james mu
3 * @date 2019/11/7 10:16
4 */
5public class GroupInitMsgConsumer implements MQConsumer {
6
7 public static final Logger LOGGER = LoggerFactory.getLogger(GroupInitMsgConsumer.class);
8
9 @Resource(name = "articleServiceForManageProvider")
10 private ArticleServiceForManage articleServiceForManage;
11
12 @Resource(name = "posterServiceForAppProvider")
13 private PosterServiceForApp posterServiceForApp;
14
15 @Override
16 public ConsumeResult onRecived(Map<String, Object> map) {
17 if (CollectionUtils.isEmpty(map)) {
18 LOGGER.warn("msg is empty");
19 return ConsumeResult.CommitMessage;
20 }
21 JSONObject msg = new JSONObject(map);
22 Integer groupId = msg.getInteger("id");
23 String groupCode = msg.getString("groupCode");
24 String groupName = msg.getString("groupName");
25 String appName = msg.getString("appName");
26 String domainSuffix = msg.getString("domainSuffix");
27 LOGGER.info("groupId: {}, groupCode: {}, groupName: {}, appName: {}, domainSuffix: {}", groupId, groupCode, groupName, appName, domainSuffix);
28
29 if (groupId == null) {
30 LOGGER.warn("param is miss");
31 return ConsumeResult.CommitMessage;
32 } else {
33 articleServiceForManage.importSubjectForGroup(groupId);
34 SiteGroupInfoDTO siteGroupInfoDTO = new SiteGroupInfoDTO();
35 siteGroupInfoDTO.setId(groupId);
36 siteGroupInfoDTO.setGroupCode(groupCode);
37 siteGroupInfoDTO.setGroupName(groupName);
38 siteGroupInfoDTO.setAppName(appName);
39 siteGroupInfoDTO.setDomainSuffix(domainSuffix);
40 posterServiceForApp.initPosterByGroup(siteGroupInfoDTO);
41 }
42 return ConsumeResult.CommitMessage;
43 }
44
45}
在消費消息的業務代碼中,大家一定要注意處理冪等性的問題,防止多次消費消息,導致業務的出錯。在此,相信大家已經了解怎么清除循環依賴的思路和處理了。