自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

初創(chuàng)公司5大Java服務(wù)困局,阿里工程師如何打破?

開發(fā) 后端
初創(chuàng)公司遇到的每一個問題都可能攸關(guān)生死。創(chuàng)業(yè)之初更應(yīng)該總結(jié)行業(yè)的常見問題,對比方案尋找最優(yōu)解。

[[281419]]

初創(chuàng)公司遇到的每一個問題都可能攸關(guān)生死。創(chuàng)業(yè)之初更應(yīng)該總結(jié)行業(yè)的常見問題,對比方案尋找最優(yōu)解。阿里巴巴地圖技術(shù)專家常意在技術(shù)圈摸爬滾打數(shù)年,接觸了各式各樣的Java服務(wù)端架構(gòu)。服務(wù)端問題見得多了,也就更能分辨出各種方案的優(yōu)劣。今天,常意總結(jié)了5大初創(chuàng)公司存在的Java服務(wù)端難題,并嘗試性地給出了一些解決方案,供大家交流參考。

1.系統(tǒng)不是分布式

1.1.單機(jī)版系統(tǒng)搶單案例

  1. // 搶取訂單函數(shù) 
  2. public synchronized void grabOrder(Long orderId, Long userId) { 
  3.     // 獲取訂單信息 
  4.     OrderDO order = orderDAO.get(orderId); 
  5.     if (Objects.isNull(order)) { 
  6.         throw new BizRuntimeException(String.format("訂單(%s)不存在", orderId)); 
  7.     } 
  8.  
  9.     // 檢查訂單狀態(tài) 
  10.     if (!Objects.equals(order.getStatus, OrderStatus.WAITING_TO_GRAB.getValue())) { 
  11.         throw new BizRuntimeException(String.format("訂單(%s)已被搶", orderId)); 
  12.     } 
  13.  
  14.     // 設(shè)置訂單被搶 
  15.     orderDAO.setGrabed(orderId, userId); 

以上代碼,在一臺服務(wù)器上運(yùn)行沒有任何問題。進(jìn)入函數(shù)grabOrder(搶取訂單)時,利用synchronized關(guān)鍵字把整個函數(shù)鎖定,要么進(jìn)入函數(shù)前訂單未被人搶取,從而搶單成功,要么進(jìn)入函數(shù)前訂單已被搶取導(dǎo)致?lián)寙问?,絕對不會出現(xiàn)進(jìn)入函數(shù)前訂單未被搶取而進(jìn)入函數(shù)后訂單又被搶取的情況。

但是,如果上面的代碼在兩臺服務(wù)器上同時運(yùn)行,由于Java的synchronized關(guān)鍵字只在一個虛擬機(jī)內(nèi)生效,所以就會導(dǎo)致兩個人能夠同時搶取一個訂單,但會以最后一個寫入數(shù)據(jù)庫的數(shù)據(jù)為準(zhǔn)。所以,大多數(shù)的單機(jī)版系統(tǒng),是無法作為分布式系統(tǒng)運(yùn)行的。

1.2.分布式系統(tǒng)搶單案例

添加分布式鎖,進(jìn)行代碼優(yōu)化:

  1. // 搶取訂單函數(shù) 
  2. public void grabOrder(Long orderId, Long userId) { 
  3.     Long lockId = orderDistributedLock.lock(orderId); 
  4.     try { 
  5.         grabOrderWithoutLock(orderId, userId); 
  6.     } finally { 
  7.         orderDistributedLock.unlock(orderId, lockId); 
  8.     } 
  9.  
  10. // 不帶鎖的搶取訂單函數(shù) 
  11. private void grabOrderWithoutLock(Long orderId, Long userId) { 
  12.     // 獲取訂單信息 
  13.     OrderDO order = orderDAO.get(orderId); 
  14.     if (Objects.isNull(order)) { 
  15.         throw new BizRuntimeException(String.format("訂單(%s)不存在", orderId)); 
  16.     } 
  17.  
  18.     // 檢查訂單狀態(tài) 
  19.     if (!Objects.equals(order.getStatus, OrderStatus.WAITING_TO_GRAB.getValue())) { 
  20.         throw new BizRuntimeException(String.format("訂單(%s)已被搶", orderId)); 
  21.     } 
  22.  
  23.     // 設(shè)置訂單被搶 
  24.     orderDAO.setGrabed(orderId, userId); 

優(yōu)化后的代碼,在調(diào)用函數(shù)grabOrderWithoutLock(不帶鎖的搶取訂單)前后,利用分布式鎖orderDistributedLock(訂單分布式鎖)進(jìn)行加鎖和釋放鎖,跟單機(jī)版的synchronized關(guān)鍵字加鎖效果基本一樣。

1.3.分布式系統(tǒng)的優(yōu)缺點(diǎn)

分布式系統(tǒng)(Distributed System)是支持分布式處理的軟件系統(tǒng),是由通信網(wǎng)絡(luò)互聯(lián)的多處理機(jī)體系結(jié)構(gòu)上執(zhí)行任務(wù)的系統(tǒng),包括分布式操作系統(tǒng)、分布式程序設(shè)計語言及其編譯系統(tǒng)、分布式文件系統(tǒng)分布式數(shù)據(jù)庫系統(tǒng)等。

分布式系統(tǒng)的優(yōu)點(diǎn):

  • 可靠性、高容錯性:一臺服務(wù)器的崩潰,不會影響其它服務(wù)器,其它服務(wù)器仍能提供服務(wù)。
  • 可擴(kuò)展性:如果系統(tǒng)服務(wù)能力不足,可以水平擴(kuò)展更多服務(wù)器。
  • 靈活性:可以很容易的安裝、實(shí)施、擴(kuò)容和升級系統(tǒng)。
  • 性能高:擁有多臺服務(wù)器的計算能力,比單臺服務(wù)器處理速度更快。
  • 性價比高:分布式系統(tǒng)對服務(wù)器硬件要求很低,可以選用廉價服務(wù)器搭建分布式集群,從而得到更好的性價比。

分布式系統(tǒng)的缺點(diǎn):

  • 排查難度高:由于系統(tǒng)分布在多臺服務(wù)器上,故障排查和問題診斷難度較高。
  • 軟件支持少:分布式系統(tǒng)解決方案的軟件支持較少。
  • 建設(shè)成本高:需要多臺服務(wù)器搭建分布式系統(tǒng)。

曾經(jīng)有不少的朋友咨詢我:"找外包做移動應(yīng)用,需要注意哪些事項(xiàng)?"

首先,確定是否需要用分布式系統(tǒng)。軟件預(yù)算有多少?預(yù)計用戶量有多少?預(yù)計訪問量有多少?是否只是業(yè)務(wù)前期試水版?單臺服務(wù)器能否解決?是否接收短時間宕機(jī)?……如果綜合考慮,單機(jī)版系統(tǒng)就可以解決的,那就不要采用分布式系統(tǒng)了。因?yàn)閱螜C(jī)版系統(tǒng)和分布式系統(tǒng)的差別很大,相應(yīng)的軟件研發(fā)成本的差別也很大。

其次,確定是否真正的分布式系統(tǒng)。分布式系統(tǒng)最大的特點(diǎn),就是當(dāng)系統(tǒng)服務(wù)能力不足時,能夠通過水平擴(kuò)展的方式,通過增加服務(wù)器來增加服務(wù)能力。然而,單機(jī)版系統(tǒng)是不支持水平擴(kuò)展的,強(qiáng)行擴(kuò)展就會引起一系列數(shù)據(jù)問題。由于單機(jī)版系統(tǒng)和分布式系統(tǒng)的研發(fā)成本差別較大,市面上的外包團(tuán)隊大多用單機(jī)版系統(tǒng)代替分布式系統(tǒng)交付。

那么,如何確定你的系統(tǒng)是真正意義上的分布式系統(tǒng)呢?從軟件上來說,是否采用了分布式軟件解決方案;從硬件上來說,是否采用了分布式硬件部署方案。

1.4.分布式軟件解決方案

作為一個合格的分布式系統(tǒng),需要根據(jù)實(shí)際需求采用相應(yīng)的分布式軟件解決方案。

1.4.1分布式鎖

分布式鎖是單機(jī)鎖的一種擴(kuò)展,主要是為了鎖住分布式系統(tǒng)中的物理塊或邏輯塊,用以此保證不同服務(wù)之間的邏輯和數(shù)據(jù)的一致性。

目前,主流的分布式鎖實(shí)現(xiàn)方式有3種:

  1. 基于數(shù)據(jù)庫實(shí)現(xiàn)的分布式鎖;
  2. 基于Redis實(shí)現(xiàn)的分布式鎖;
  3. 基于Zookeeper實(shí)現(xiàn)的分布式鎖。

1.4.2分布式消息

分布式消息中間件是支持在分布式系統(tǒng)中發(fā)送和接受消息的軟件基礎(chǔ)設(shè)施。常見的分布式消息中間件有ActiveMQ、RabbitMQ、Kafka、MetaQ等。

MetaQ(全稱Metamorphosis)是一個高性能、高可用、可擴(kuò)展的分布式消息中間件,思路起源于LinkedIn的Kafka,但并不是Kafka的一個拷貝。MetaQ具有消息存儲順序?qū)憽⑼掏铝看蠛椭С直镜睾蚗A事務(wù)等特性,適用于大吞吐量、順序消息、廣播和日志數(shù)據(jù)傳輸?shù)葓鼍啊?/p>

1.4.3數(shù)據(jù)庫分片分組

針對大數(shù)據(jù)量的數(shù)據(jù)庫,一般會采用"分片分組"策略:

分片(shard):主要解決擴(kuò)展性問題,屬于水平拆分。引入分片,就引入了數(shù)據(jù)路由和分區(qū)鍵的概念。其中,分表解決的是數(shù)據(jù)量過大的問題,分庫解決的是數(shù)據(jù)庫性能瓶頸的問題。

分組(group):主要解決可用性問題,通過主從復(fù)制的方式實(shí)現(xiàn),并提供讀寫分離策略用以提高數(shù)據(jù)庫性能。

1.4.4分布式計算

分布式計算( Distributed computing )是一種"把需要進(jìn)行大量計算的工程數(shù)據(jù)分割成小塊,由多臺計算機(jī)分別計算;在上傳運(yùn)算結(jié)果后,將結(jié)果統(tǒng)一合并得出數(shù)據(jù)結(jié)論"的科學(xué)。

當(dāng)前的高性能服務(wù)器在處理海量數(shù)據(jù)時,其計算能力、內(nèi)存容量等指標(biāo)都遠(yuǎn)遠(yuǎn)無法達(dá)到要求。在大數(shù)據(jù)時代,工程師采用廉價的服務(wù)器組成分布式服務(wù)集群,以集群協(xié)作的方式完成海量數(shù)據(jù)的處理,從而解決單臺服務(wù)器在計算與存儲上的瓶頸。Hadoop、Storm以及Spark是常用的分布式計算中間件,Hadoop是對非實(shí)時數(shù)據(jù)做批量處理的中間件,Storm和Spark是對實(shí)時數(shù)據(jù)做流式處理的中間件。

除此之外,還有更多的分布式軟件解決方案,這里就不再一一介紹了。

1.5分布式硬件部署方案

介紹完服務(wù)端的分布式軟件解決方案,就不得不介紹一下服務(wù)端的分布式硬件部署方案。這里,只畫出了服務(wù)端常見的接口服務(wù)器、MySQL數(shù)據(jù)庫、Redis緩存,而忽略了其它的云存儲服務(wù)、消息隊列服務(wù)、日志系統(tǒng)服務(wù)……

1.5.1一般單機(jī)版部署方案

 

架構(gòu)說明:只有1臺接口服務(wù)器、1個MySQL數(shù)據(jù)庫、1個可選Redis緩存,可能都部署在同一臺服務(wù)器上。

適用范圍:適用于演示環(huán)境、測試環(huán)境以及不怕宕機(jī)且日PV在5萬以內(nèi)的小型商業(yè)應(yīng)用。

1.5.2中小型分布式硬件部署方案

 

架構(gòu)說明:通過SLB/Nginx組成一個負(fù)載均衡的接口服務(wù)器集群,MySQL數(shù)據(jù)庫和Redis緩存采用了一主一備(或多備)的部署方式。

適用范圍:適用于日PV在500萬以內(nèi)的中小型商業(yè)應(yīng)用。

1.5.3大型分布式硬件部署方案

架構(gòu)說明:通過SLB/Nginx組成一個負(fù)載均衡的接口服務(wù)器集群,利用分片分組策略組成一個MySQL數(shù)據(jù)庫集群和Redis緩存集群。

適用范圍:適用于日PV在500萬以上的大型商業(yè)應(yīng)用。

2.多線程使用不正確

多線程最主要目的就是"最大限度地利用CPU資源",可以把串行過程變成并行過程,從而提高了程序的執(zhí)行效率。

2.1一個慢接口案例

假設(shè)在用戶登錄時,如果是新用戶,需要創(chuàng)建用戶信息,并發(fā)放新用戶優(yōu)惠券。例子代碼如下:

  1. // 登錄函數(shù)(示意寫法) 
  2. public UserVO login(String phoneNumber, String verifyCode) { 
  3.     // 檢查驗(yàn)證碼 
  4.     if (!checkVerifyCode(phoneNumber, verifyCode)) { 
  5.         throw new ExampleException("驗(yàn)證碼錯誤"); 
  6.     } 
  7.  
  8.     // 檢查用戶存在 
  9.     UserDO user = userDAO.getByPhoneNumber(phoneNumber); 
  10.     if (Objects.nonNull(user)) { 
  11.         return transUser(user); 
  12.     } 
  13.  
  14.     // 創(chuàng)建新用戶 
  15.     return createNewUser(user); 
  16.  
  17. // 創(chuàng)建新用戶函數(shù) 
  18. private UserVO createNewUser(String phoneNumber) { 
  19.     // 創(chuàng)建新用戶 
  20.     UserDO user = new UserDO(); 
  21.     ... 
  22.     userDAO.insert(user); 
  23.  
  24.     // 綁定優(yōu)惠券 
  25.     couponService.bindCoupon(user.getId(), CouponType.NEW_USER); 
  26.  
  27.     // 返回新用戶 
  28.     return transUser(user); 

其中,綁定優(yōu)惠券(bindCoupon)是給用戶綁定新用戶優(yōu)惠券,然后再給用戶發(fā)送推送通知。如果隨著優(yōu)惠券數(shù)量越來越多,該函數(shù)也會變得越來越慢,執(zhí)行時間甚至超過1秒,并且沒有什么優(yōu)化空間。現(xiàn)在,登錄(login)函數(shù)就成了名副其實(shí)的慢接口,需要進(jìn)行接口優(yōu)化。

2.2采用多線程優(yōu)化

通過分析發(fā)現(xiàn),綁定優(yōu)惠券(bindCoupon)函數(shù)可以異步執(zhí)行。首先想到的是采用多線程解決該問題,代碼如下:

  1. // 創(chuàng)建新用戶函數(shù) 
  2. private UserVO createNewUser(String phoneNumber) { 
  3.     // 創(chuàng)建新用戶 
  4.     UserDO user = new UserDO(); 
  5.     ... 
  6.     userDAO.insert(user); 
  7.  
  8.     // 綁定優(yōu)惠券 
  9.     executorService.execute(()->couponService.bindCoupon(user.getId(), CouponType.NEW_USER)); 
  10.  
  11.     // 返回新用戶 
  12.     return transUser(user); 

現(xiàn)在,在新線程中執(zhí)行綁定優(yōu)惠券(bindCoupon)函數(shù),使用戶登錄(login)函數(shù)性能得到很大的提升。但是,如果在新線程執(zhí)行綁定優(yōu)惠券函數(shù)過程中,系統(tǒng)發(fā)生重啟或崩潰導(dǎo)致線程執(zhí)行失敗,用戶將永遠(yuǎn)獲取不到新用戶優(yōu)惠券。除非提供用戶手動領(lǐng)取優(yōu)惠券頁面,否則就需要程序員后臺手工綁定優(yōu)惠券。所以,用采用多線程優(yōu)化慢接口,并不是一個完善的解決方案。

2.3采用消息隊列優(yōu)化

如果要保證綁定優(yōu)惠券函數(shù)執(zhí)行失敗后能夠重啟執(zhí)行,可以采用數(shù)據(jù)庫表、Redis隊列、消息隊列的等多種解決方案。由于篇幅優(yōu)先,這里只介紹采用MetaQ消息隊列解決方案,并省略了MetaQ相關(guān)配置僅給出了核心代碼。

消息生產(chǎn)者代碼:

  1. // 創(chuàng)建新用戶函數(shù) 
  2. private UserVO createNewUser(String phoneNumber) { 
  3.     // 創(chuàng)建新用戶 
  4.     UserDO user = new UserDO(); 
  5.     ... 
  6.     userDAO.insert(user); 
  7.  
  8.     // 發(fā)送優(yōu)惠券消息 
  9.     Long userId = user.getId(); 
  10.     CouponMessageDataVO data = new CouponMessageDataVO(); 
  11.     data.setUserId(userId); 
  12.     data.setCouponType(CouponType.NEW_USER); 
  13.     Message message = new Message(TOPIC, TAG, userId, JSON.toJSONBytes(data)); 
  14.     SendResult result = metaqTemplate.sendMessage(message); 
  15.     if (!Objects.equals(result, SendStatus.SEND_OK)) { 
  16.         log.error("發(fā)送用戶({})綁定優(yōu)惠券消息失敗:{}", userId, JSON.toJSONString(result)); 
  17.     } 
  18.  
  19.     // 返回新用戶 
  20.     return transUser(user); 

注意:可能出現(xiàn)發(fā)生消息不成功,但是這種概率相對較低。

消息消費(fèi)者代碼:

  1. // 優(yōu)惠券服務(wù)類 
  2. @Slf4j 
  3. @Service 
  4. public class CouponService extends DefaultMessageListener<String> { 
  5.     // 消息處理函數(shù) 
  6.     @Override 
  7.     @Transactional(rollbackFor = Exception.class) 
  8.     public void onReceiveMessages(MetaqMessage<String> message) { 
  9.         // 獲取消息體 
  10.         String body = message.getBody(); 
  11.         if (StringUtils.isBlank(body)) { 
  12.             log.warn("獲取消息({})體為空", message.getId()); 
  13.             return
  14.         } 
  15.  
  16.         // 解析消息數(shù)據(jù) 
  17.         CouponMessageDataVO data = JSON.parseObject(body, CouponMessageDataVO.class); 
  18.         if (Objects.isNull(data)) { 
  19.             log.warn("解析消息({})體為空", message.getId()); 
  20.             return
  21.         } 
  22.  
  23.         // 綁定優(yōu)惠券 
  24.         bindCoupon(data.getUserId(), data.getCouponType()); 
  25.     } 

解決方案優(yōu)點(diǎn):采集MetaQ消息隊列優(yōu)化慢接口解決方案的優(yōu)點(diǎn):

  1. 如果系統(tǒng)發(fā)生重啟或崩潰,導(dǎo)致消息處理函數(shù)執(zhí)行失敗,不會確認(rèn)消息已消費(fèi);由于MetaQ支持多服務(wù)訂閱同一隊列,該消息可以轉(zhuǎn)到別的服務(wù)進(jìn)行消費(fèi),亦或等到本服務(wù)恢復(fù)正常后再進(jìn)行消費(fèi)。
  2. 消費(fèi)者可多服務(wù)、多線程進(jìn)行消費(fèi)消息,即便消息處理時間較長,也不容易引起消息積壓;即便引起消息積壓,也可以通過擴(kuò)充服務(wù)實(shí)例的方式解決。
  3. 如果需要重新消費(fèi)該消息,只需要在MetaQ管理平臺上點(diǎn)擊"消息驗(yàn)證"即可。

3.流程定義不合理

3.1.原有的采購流程

這是一個簡易的采購流程,由庫管系統(tǒng)發(fā)起采購,采購員開始采購,采購員完成采購,同時回流采集訂單到庫管系統(tǒng)。

 

其中,完成采購動作的核心代碼如下:

  1. /** 完成采購動作函數(shù)(此處省去獲取采購單/驗(yàn)證狀態(tài)/鎖定采購單等邏輯) */ 
  2. public void finishPurchase(PurchaseOrder order) { 
  3.     // 完成相關(guān)處理 
  4.     ...... 
  5.  
  6.     // 回流采購單(調(diào)用HTTP接口) 
  7.     backflowPurchaseOrder(order); 
  8.  
  9.     // 設(shè)置完成狀態(tài) 
  10.     purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue()); 

由于函數(shù)backflowPurchaseOrder(回流采購單)調(diào)用了HTTP接口,可能引起以下問題:

  1. 該函數(shù)可能耗費(fèi)時間較長,導(dǎo)致完成采購接口成為慢接口;
  2. 該函數(shù)可能失敗拋出異常,導(dǎo)致客戶調(diào)用完成采購接口失敗。

3.2.優(yōu)化的采購流程

通過需求分析,把"采購員完成采購并回流采集訂單"動作拆分為"采購員完成采購"和"回流采集訂單"兩個獨(dú)立的動作,把"采購?fù)瓿?quot;拆分為"采購?fù)瓿?quot;和"回流完成"兩個獨(dú)立的狀態(tài),更方便采購流程的管理和實(shí)現(xiàn)。

 

拆分采購流程的動作和狀態(tài)后,核心代碼如下:

  1. /** 完成采購動作函數(shù)(此處省去獲取采購單/驗(yàn)證狀態(tài)/鎖定采購單等邏輯) */ 
  2. public void finishPurchase(PurchaseOrder order) { 
  3.     // 完成相關(guān)處理 
  4.     ...... 
  5.  
  6.     // 設(shè)置完成狀態(tài) 
  7.     purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue()); 
  8.  
  9. /** 執(zhí)行回流動作函數(shù)(此處省去獲取采購單/驗(yàn)證狀態(tài)/鎖定采購單等邏輯) */ 
  10. public void executeBackflow(PurchaseOrder order) { 
  11.     // 回流采購單(調(diào)用HTTP接口) 
  12.     backflowPurchaseOrder(order); 
  13.  
  14.     // 設(shè)置回流狀態(tài) 
  15.     purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue()); 

其中,函數(shù)executeBackflow(執(zhí)行回流)由定時作業(yè)觸發(fā)執(zhí)行。如果回流采購單失敗,采購單狀態(tài)并不會修改為"已回流";等下次定時作業(yè)執(zhí)行時,將會繼續(xù)執(zhí)行回流動作;直到回流采購單成功為止。

3.3.有限狀態(tài)機(jī)介紹

3.3.1概念

有限狀態(tài)機(jī)(Finite-state machine,F(xiàn)SM),又稱有限狀態(tài)自動機(jī),簡稱狀態(tài)機(jī),是表示有限個狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動作等行為的一個數(shù)學(xué)模型。

3.3.2要素

狀態(tài)機(jī)可歸納為4個要素:現(xiàn)態(tài)、條件、動作、次態(tài)。

 

現(xiàn)態(tài):指當(dāng)前流程所處的狀態(tài),包括起始、中間、終結(jié)狀態(tài)。

條件:也可稱為事件;當(dāng)一個條件被滿足時,將會觸發(fā)一個動作并執(zhí)行一次狀態(tài)的遷移。

動作:當(dāng)條件滿足后要執(zhí)行的動作。動作執(zhí)行完畢后,可以遷移到新的狀態(tài),也可以仍舊保持原狀態(tài)。

次態(tài):當(dāng)條件滿足后要遷往的狀態(tài)。“次態(tài)”是相對于“現(xiàn)態(tài)”而言的,“次態(tài)”一旦被激活,就轉(zhuǎn)變成新的“現(xiàn)態(tài)”了。

3.3.3狀態(tài)

狀態(tài)表示流程中的持久狀態(tài),流程圖上的每一個圈代表一個狀態(tài)。

初始狀態(tài): 流程開始時的某一狀態(tài);中間狀態(tài): 流程中間過程的某一狀態(tài);終結(jié)狀態(tài): 流程完成時的某一狀態(tài)。

使用建議:

  1. 狀態(tài)必須是一個持久狀態(tài),而不能是一個臨時狀態(tài);
  2. 終結(jié)狀態(tài)不能是中間狀態(tài),不能繼續(xù)進(jìn)行流程流轉(zhuǎn);
  3. 狀態(tài)劃分合理,不要把多個狀態(tài)強(qiáng)制合并為一個狀態(tài);
  4. 狀態(tài)盡量精簡,同一狀態(tài)的不同情況可以用其它字段表示。

3.3.4動作

動作的三要素:角色、現(xiàn)態(tài)、次態(tài),流程圖上的每一條線代表一個動作。

角色: 誰發(fā)起的這個操作,可以是用戶、定時任務(wù)等;現(xiàn)態(tài): 觸發(fā)動作時當(dāng)前的狀態(tài),是執(zhí)行動作的前提條件;次態(tài): 完成動作后達(dá)到的狀態(tài),是執(zhí)行動作的最終目標(biāo)。

使用建議:

  • 每個動作執(zhí)行前,必須檢查當(dāng)前狀態(tài)和觸發(fā)動作狀態(tài)的一致性;
  • 狀態(tài)機(jī)的狀態(tài)更改,只能通過動作進(jìn)行,其它操作都是不符合規(guī)范的;
  • 需要添加分布式鎖保證動作的原子性,添加數(shù)據(jù)庫事務(wù)保證數(shù)據(jù)的一致性;
  • 類似的動作(比如操作用戶、請求參數(shù)、動作含義等)可以合并為一個動作,并根據(jù)動作執(zhí)行結(jié)果轉(zhuǎn)向不同的狀態(tài)。

4.系統(tǒng)間交互不科學(xué)

4.1.直接通過數(shù)據(jù)庫交互

在一些項(xiàng)目中,系統(tǒng)間交互不通過接口調(diào)用和消息隊列,而是通過數(shù)據(jù)庫直接訪問。問其原因,回答道:"項(xiàng)目工期太緊張,直接訪問數(shù)據(jù)庫,簡單又快捷"。

還是以上面的采購流程為例——采購訂單由庫管系統(tǒng)發(fā)起,由采購系統(tǒng)負(fù)責(zé)采購,采購?fù)瓿珊笸ㄖ獛旃芟到y(tǒng),庫管系統(tǒng)進(jìn)入入庫操作。采購系統(tǒng)采購?fù)瓿珊?,通知庫管系統(tǒng)數(shù)據(jù)庫的代碼如下:

  1. /** 執(zhí)行回流動作函數(shù)(此處省去獲取采購單/驗(yàn)證狀態(tài)/鎖定采購單等邏輯) */ 
  2. public void executeBackflow(PurchaseOrder order) { 
  3.     // 完成原始采購單 
  4.     rawPurchaseOrderDAO.setStatus(order.getRawId(), RawPurchaseOrderStatus.FINISHED.getValue()); 
  5.  
  6.     // 設(shè)置回流狀態(tài) 
  7.     purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue()); 

其中,通過rawPurchaseOrderDAO(原始采購單DAO)直接訪問庫管系統(tǒng)的數(shù)據(jù)庫表,并設(shè)置原始采購單狀態(tài)為已完成。

一般情況下,直接通過數(shù)據(jù)訪問的方式是不會有問題的。但是,一旦發(fā)生競態(tài),就會導(dǎo)致數(shù)據(jù)不同步。有人會說,可以考慮使用同一分布式鎖解決該問題。是的,這種解決方案沒有問題,只是又在系統(tǒng)間共享了分布式鎖。

  1. 直接通過數(shù)據(jù)庫交互的缺點(diǎn):
  2. 直接暴露數(shù)據(jù)庫表,容易產(chǎn)生數(shù)據(jù)安全問題;
  3. 多個系統(tǒng)操作同一數(shù)據(jù)庫表,容易造成數(shù)據(jù)庫表數(shù)據(jù)混亂;
  4. 操作同一個數(shù)據(jù)庫表的代碼,分布在不同的系統(tǒng)中,不便于管理和維護(hù);

具有數(shù)據(jù)庫表這樣的強(qiáng)關(guān)聯(lián),無法實(shí)現(xiàn)系統(tǒng)間的隔離和解耦。

4.2.通過Dubbo接口交互

由于采購系統(tǒng)和庫管系統(tǒng)都是內(nèi)部系統(tǒng),可以通過類似Dubbo的RPC接口進(jìn)行交互。

庫管系統(tǒng)代碼:

  1. /** 采購單服務(wù)接口 */ 
  2. public interface PurchaseOrderService { 
  3.     /** 完成采購單函數(shù) */ 
  4.     public void finishPurchaseOrder(Long orderId); 
  5. /** 采購單服務(wù)實(shí)現(xiàn) */ 
  6. @Service("purchaseOrderService"
  7. public class PurchaseOrderServiceImpl implements PurchaseOrderService { 
  8.     /** 完成采購單函數(shù) */ 
  9.     @Override 
  10.     @Transactional(rollbackFor = Exception.class) 
  11.     public void finishPurchaseOrder(Long orderId) { 
  12.         // 相關(guān)處理 
  13.         ... 
  14.  
  15.         // 完成采購單 
  16.         purchaseOrderService.finishPurchaseOrder(order.getRawId()); 
  17.     } 

其中,庫管系統(tǒng)通過Dubbo把PurchaseOrderServiceImpl(采購單服務(wù)實(shí)現(xiàn))以PurchaseOrderService(采購單服務(wù)接口)定義的接口服務(wù)暴露給采購系統(tǒng)。這里,省略了Dubbo開發(fā)服務(wù)接口相關(guān)配置。

采購系統(tǒng)代碼:

  1. /** 執(zhí)行回流動作函數(shù)(此處省去獲取采購單/驗(yàn)證狀態(tài)/鎖定采購單等邏輯) */ 
  2. public void executeBackflow(PurchaseOrder order) { 
  3.     // 完成采購單 
  4.     purchaseOrderService.finishPurchaseOrder(order.getRawId()); 
  5.  
  6.     // 設(shè)置回流狀態(tài) 
  7.     purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue()); 

其中,purchaseOrderService(采購單服務(wù))為庫管系統(tǒng)PurchaseOrderService(采購單服務(wù))在采購系統(tǒng)中的Dubbo服務(wù)客戶端存根,通過該服務(wù)調(diào)用庫管系統(tǒng)的服務(wù)接口函數(shù)finishPurchaseOrder(完成采購單函數(shù))。

這樣,采購系統(tǒng)和庫管系統(tǒng)自己的強(qiáng)關(guān)聯(lián),通過Dubbo就簡單地實(shí)現(xiàn)了系統(tǒng)隔離和解耦。當(dāng)然,除了采用Dubbo接口外,還可以采用HTTPS、HSF、WebService等同步接口調(diào)用方式,也可以采用MetaQ等異步消息通知方式。

4.3常見系統(tǒng)間交互協(xié)議

4.3.1同步接口調(diào)用

同步接口調(diào)用是以一種阻塞式的接口調(diào)用機(jī)制。常見的交互協(xié)議有:

  • HTTP/HTTPS接口;
  • WebService接口;
  • Dubbo/HSF接口;
  • CORBA接口。

4.3.2異步消息通知

異步消息通知是一種通知式的信息交互機(jī)制。當(dāng)系統(tǒng)發(fā)生某種事件時,會主動通知相應(yīng)的系統(tǒng)。常見的交互協(xié)議有:

  • MetaQ的消息通知;
  • CORBA消息通知。

4.4.常見系統(tǒng)間交互方式

4.4.1請求-應(yīng)答

 

適用范圍:適合于簡單的耗時較短的接口同步調(diào)用場景,比如Dubbo接口同步調(diào)用。

4.4.2通知-確認(rèn)

 

適用范圍:適合于簡單的異步消息通知場景,比如MetaQ消息通知。

4.4.3請求-應(yīng)答-查詢-返回

 

適用范圍:適合于復(fù)雜的耗時較長的接口同步調(diào)用場景,比如提交作業(yè)任務(wù)并定期查詢?nèi)蝿?wù)結(jié)果。

4.4.4請求-應(yīng)答-回調(diào)

 


適用范圍:適合于復(fù)雜的耗時較長的接口同步調(diào)用和異步回調(diào)相結(jié)合的場景,比如支付寶的訂單支付。

4.4.5請求-應(yīng)答-通知-確認(rèn)

 

適用范圍:適合于復(fù)雜的耗時較長的接口同步調(diào)用和異步消息通知相結(jié)合的場景,比如提交作業(yè)任務(wù)并等待完成消息通知。

4.4.6通知-確認(rèn)-通知-確認(rèn)

 

適用范圍:適合于復(fù)雜的耗時較長的異步消息通知場景。

5.數(shù)據(jù)查詢不分頁

在數(shù)據(jù)查詢時,由于未能對未來數(shù)據(jù)量做出正確的預(yù)估,很多情況下都沒有考慮數(shù)據(jù)的分頁查詢。

5.1.普通查詢案例

以下是查詢過期訂單的代碼:

  1. /** 訂單DAO接口 */ 
  2. public interface OrderDAO { 
  3.     /** 查詢過期訂單函數(shù) */ 
  4.     @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day)"
  5.     public List<OrderDO> queryTimeout(); 
  6.  
  7. /** 訂單服務(wù)接口 */ 
  8. public interface OrderService { 
  9.     /** 查詢過期訂單函數(shù) */ 
  10.     public List<OrderVO> queryTimeout(); 

當(dāng)過期訂單數(shù)量很少時,以上代碼不會有任何問題。但是,當(dāng)過期訂單數(shù)量達(dá)到幾十萬上千萬時,以上代碼就會出現(xiàn)以下問題:

  • 數(shù)據(jù)量太大,導(dǎo)致服務(wù)端的內(nèi)存溢出;
  • 數(shù)據(jù)量太大,導(dǎo)致查詢接口超時、返回數(shù)據(jù)超時等;
  • 數(shù)據(jù)量太大,導(dǎo)致客戶端的內(nèi)存溢出。

所以,在數(shù)據(jù)查詢時,特別是不能預(yù)估數(shù)據(jù)量的大小時,需要考慮數(shù)據(jù)的分頁查詢。

這里,主要介紹"設(shè)置最大數(shù)量"和"采用分頁查詢"兩種方式。

5.2設(shè)置最大數(shù)量

"設(shè)置最大數(shù)量"是一種最簡單的分頁查詢,相當(dāng)于只返回第一頁數(shù)據(jù)。例子代碼如下:

  1. /** 訂單DAO接口 */ 
  2. public interface OrderDAO { 
  3.     /** 查詢過期訂單函數(shù) */ 
  4.     @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit 0, #{maxCount}"
  5.     public List<OrderDO> queryTimeout(@Param("maxCount"Integer maxCount); 
  6.  
  7. /** 訂單服務(wù)接口 */ 
  8. public interface OrderService { 
  9.     /** 查詢過期訂單函數(shù) */ 
  10.     public List<OrderVO> queryTimeout(Integer maxCount); 

適用于沒有分頁需求、但又擔(dān)心數(shù)據(jù)過多導(dǎo)致內(nèi)存溢出、數(shù)據(jù)量過大的查詢。

5.3采用分頁查詢

"采用分頁查詢"是指定startIndex(開始序號)和pageSize(頁面大小)進(jìn)行數(shù)據(jù)查詢,或者指定pageIndex(分頁序號)和pageSize(頁面大小)進(jìn)行數(shù)據(jù)查詢。例子代碼如下:

  1. /** 訂單DAO接口 */ 
  2. public interface OrderDAO { 
  3.     /** 統(tǒng)計過期訂單函數(shù) */ 
  4.     @Select("select count(*) from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day)"
  5.     public Long countTimeout(); 
  6.     /** 查詢過期訂單函數(shù) */ 
  7.     @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}"
  8.     public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize"Integer pageSize); 
  9.  
  10. /** 訂單服務(wù)接口 */ 
  11. public interface OrderService { 
  12.     /** 查詢過期訂單函數(shù) */ 
  13.     public PageData<OrderVO> queryTimeout(Long startIndex, Integer pageSize); 

適用于真正的分頁查詢,查詢參數(shù)startIndex(開始序號)和pageSize(頁面大小)可由調(diào)用方指定。

5.4分頁查詢隱藏問題

假設(shè),我們需要在一個定時作業(yè)(每5分鐘執(zhí)行一次)中,針對已經(jīng)超時的訂單(status=5,創(chuàng)建時間超時30天)進(jìn)行超時關(guān)閉(status=10)。實(shí)現(xiàn)代碼如下:

  1. /** 訂單DAO接口 */ 
  2. public interface OrderDAO { 
  3.     /** 查詢過期訂單函數(shù) */ 
  4.     @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}"
  5.     public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize"Integer pageSize); 
  6.     /** 設(shè)置訂單超時關(guān)閉 */ 
  7.     @Update("update t_order set status = 10 where id = #{orderId} and status = 5"
  8.     public Long setTimeoutClosed(@Param("orderId") Long orderId) 
  9.  
  10. /** 關(guān)閉過期訂單作業(yè)類 */ 
  11. public class CloseTimeoutOrderJob extends Job { 
  12.     /** 分頁數(shù)量 */ 
  13.     private static final int PAGE_COUNT = 100; 
  14.     /** 分頁大小 */ 
  15.     private static final int PAGE_SIZE = 1000; 
  16.     /** 作業(yè)執(zhí)行函數(shù) */ 
  17.     @Override 
  18.     public void execute() { 
  19.         for (int i = 0; i < PAGE_COUNT; i++) { 
  20.             // 查詢處理訂單 
  21.             List<OrderDO> orderList = orderDAO.queryTimeout(i * PAGE_COUNT, PAGE_SIZE); 
  22.             for (OrderDO order : orderList) { 
  23.                 // 進(jìn)行超時關(guān)閉 
  24.                 ...... 
  25.                 orderDAO.setTimeoutClosed(order.getId()); 
  26.             } 
  27.  
  28.             // 檢查處理完畢 
  29.             if(orderList.size() < PAGE_SIZE) { 
  30.                 break; 
  31.             } 
  32.         } 
  33.     } 

粗看這段代碼是沒有問題的,嘗試循環(huán)100次,每次取1000條過期訂單,進(jìn)行訂單超時關(guān)閉操作,直到?jīng)]有訂單或達(dá)到100次為止。但是,如果結(jié)合訂單狀態(tài)一起看,就會發(fā)現(xiàn)從第二次查詢開始,每次會忽略掉前startIndex(開始序號)條應(yīng)該處理的過期訂單。這就是分頁查詢存在的隱藏問題:

當(dāng)滿足查詢條件的數(shù)據(jù),在操作中不再滿足查詢條件時,會導(dǎo)致后續(xù)分頁查詢中前startIndex(開始序號)條滿足條件的數(shù)據(jù)被跳過。

可以采用"設(shè)置最大數(shù)量"的方式解決,代碼如下:

  1. /** 訂單DAO接口 */ 
  2. public interface OrderDAO { 
  3.     /** 查詢過期訂單函數(shù) */ 
  4.     @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit 0, #{maxCount}"
  5.     public List<OrderDO> queryTimeout(@Param("maxCount"Integer maxCount); 
  6.     /** 設(shè)置訂單超時關(guān)閉 */ 
  7.     @Update("update t_order set status = 10 where id = #{orderId} and status = 5"
  8.     public Long setTimeoutClosed(@Param("orderId") Long orderId) 
  9.  
  10. /** 關(guān)閉過期訂單作業(yè)(定時作業(yè)) */ 
  11. public class CloseTimeoutOrderJob extends Job { 
  12.     /** 分頁數(shù)量 */ 
  13.     private static final int PAGE_COUNT = 100; 
  14.     /** 分頁大小 */ 
  15.     private static final int PAGE_SIZE = 1000; 
  16.     /** 作業(yè)執(zhí)行函數(shù) */ 
  17.     @Override 
  18.     public void execute() { 
  19.         for (int i = 0; i < PAGE_COUNT; i++) { 
  20.             // 查詢處理訂單 
  21.             List<OrderDO> orderList = orderDAO.queryTimeout(PAGE_SIZE); 
  22.             for (OrderDO order : orderList) { 
  23.                 // 進(jìn)行超時關(guān)閉 
  24.                 ...... 
  25.                 orderDAO.setTimeoutClosed(order.getId()); 
  26.             } 
  27.  
  28.             // 檢查處理完畢 
  29.             if(orderList.size() < PAGE_SIZE) { 
  30.                 break; 
  31.             } 
  32.         } 
  33.     } 

 

責(zé)任編輯:武曉燕 來源: 阿里技術(shù)
相關(guān)推薦

2018-09-07 08:00:00

2011-09-16 09:23:41

軟件項(xiàng)目

2012-10-12 10:24:43

創(chuàng)業(yè)創(chuàng)業(yè)公司招聘

2009-10-30 09:36:10

GoogleLinux操作系統(tǒng)

2022-09-14 18:23:11

工程師面試Java

2018-06-22 15:59:46

2019-01-21 08:20:12

工程師思維職責(zé)

2020-03-23 08:02:37

阿里工程師能力

2019-09-17 14:27:37

數(shù)據(jù)平臺架構(gòu)

2018-10-29 08:20:26

Apache Flin工程師AI

2018-04-10 12:10:43

2010-09-13 17:38:47

Google的系統(tǒng)工程

2013-01-07 09:42:09

云性能亞馬遜實(shí)例AWS用例

2018-10-10 16:15:01

團(tuán)隊研發(fā)效率

2019-08-28 20:38:12

好代碼編寫代碼代碼質(zhì)量

2015-03-17 19:35:49

Xen漏洞阿里云

2020-05-20 07:00:00

機(jī)器學(xué)習(xí)人工智能AI

2021-07-07 11:23:56

云服務(wù)云技術(shù)云計算

2021-06-02 08:04:58

微服務(wù)初創(chuàng)公司

2019-08-28 10:23:05

技術(shù)人阿里工程師
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號