阿里妹導讀:初創公司遇到的每個問題均可能攸關生死。創業之初更應該總結行業的常見問題,對比方案尋找最優解。阿里巴巴地圖技術專家常意在技術圈摸爬滾打數年,接觸了各式各樣的Java服務端架構。服務端問題見得多了,也就更能分辨出各類方案的優劣。今天,常意總結了5大初創公司存在的Java服務端難題,並嘗試性地給出了一些解決方案,供你們交流參考。
1.1.單機版系統搶單案例程序員
// 搶取訂單函數 public synchronized void grabOrder(Long orderId, Long userId) { // 獲取訂單信息 OrderDO order = orderDAO.get(orderId); if (Objects.isNull(order)) { throw new BizRuntimeException(String.format("訂單(%s)不存在", orderId)); } // 檢查訂單狀態 if (!Objects.equals(order.getStatus, OrderStatus.WAITING_TO_GRAB.getValue())) { throw new BizRuntimeException(String.format("訂單(%s)已被搶", orderId)); } // 設置訂單被搶 orderDAO.setGrabed(orderId, userId); }
以上代碼,在一臺服務器上運行沒有任何問題。進入函數grabOrder(搶取訂單)時,利用synchronized關鍵字把整個函數鎖定,要麼進入函數前訂單未被人搶取,從而搶單成功,要麼進入函數前訂單已被搶取致使搶單失敗,絕對不會出現進入函數前訂單未被搶取而進入函數後訂單又被搶取的狀況。數據庫
可是,若是上面的代碼在兩臺服務器上同時運行,因爲Java的synchronized關鍵字只在一個虛擬機內生效,因此就會致使兩我的可以同時搶取一個訂單,但會以最後一個寫入數據庫的數據爲準。因此,大多數的單機版系統,是沒法做爲分佈式系統運行的。緩存
1.2.分佈式系統搶單案例安全
添加分佈式鎖,進行代碼優化:服務器
// 搶取訂單函數 public void grabOrder(Long orderId, Long userId) { Long lockId = orderDistributedLock.lock(orderId); try { grabOrderWithoutLock(orderId, userId); } finally { orderDistributedLock.unlock(orderId, lockId); } } // 不帶鎖的搶取訂單函數 private void grabOrderWithoutLock(Long orderId, Long userId) { // 獲取訂單信息 OrderDO order = orderDAO.get(orderId); if (Objects.isNull(order)) { throw new BizRuntimeException(String.format("訂單(%s)不存在", orderId)); } // 檢查訂單狀態 if (!Objects.equals(order.getStatus, OrderStatus.WAITING_TO_GRAB.getValue())) { throw new BizRuntimeException(String.format("訂單(%s)已被搶", orderId)); } // 設置訂單被搶 orderDAO.setGrabed(orderId, userId); }
優化後的代碼,在調用函數grabOrderWithoutLock(不帶鎖的搶取訂單)先後,利用分佈式鎖orderDistributedLock(訂單分佈式鎖)進行加鎖和釋放鎖,跟單機版的synchronized關鍵字加鎖效果基本同樣。網絡
1.3.分佈式系統的優缺點多線程
分佈式系統(Distributed System)是支持分佈式處理的軟件系統,是由通訊網絡互聯的多處理機體系結構上執行任務的系統,包括分佈式操做系統、分佈式程序設計語言及其編譯系統、分佈式文件系統分佈式數據庫系統等。架構
分佈式系統的優勢:併發
分佈式系統的缺點:負載均衡
曾經有很多的朋友諮詢我:"找外包作移動應用,須要注意哪些事項?"
首先,肯定是否須要用分佈式系統。軟件預算有多少?預計用戶量有多少?預計訪問量有多少?是否只是業務前期試水版?單臺服務器可否解決?是否接收短期宕機?……若是綜合考慮,單機版系統就能夠解決的,那就不要採用分佈式系統了。由於單機版系統和分佈式系統的差異很大,相應的軟件研發成本的差異也很大。
其次,肯定是否真正的分佈式系統。分佈式系統最大的特色,就是當系統服務能力不足時,可以經過水平擴展的方式,經過增長服務器來增長服務能力。然而,單機版系統是不支持水平擴展的,強行擴展就會引發一系列數據問題。因爲單機版系統和分佈式系統的研發成本差異較大,市面上的外包團隊大多用單機版系統代替分佈式系統交付。
那麼,如何肯定你的系統是真正意義上的分佈式系統呢?從軟件上來講,是否採用了分佈式軟件解決方案;從硬件上來講,是否採用了分佈式硬件部署方案。
1.4.分佈式軟件解決方案
做爲一個合格的分佈式系統,須要根據實際需求採用相應的分佈式軟件解決方案。
1.4.1分佈式鎖
分佈式鎖是單機鎖的一種擴展,主要是爲了鎖住分佈式系統中的物理塊或邏輯塊,用以此保證不一樣服務之間的邏輯和數據的一致性。
目前,主流的分佈式鎖實現方式有3種:
1.4.2分佈式消息
分佈式消息中間件是支持在分佈式系統中發送和接受消息的軟件基礎設施。常見的分佈式消息中間件有ActiveMQ、RabbitMQ、Kafka、MetaQ等。
MetaQ(全稱Metamorphosis)是一個高性能、高可用、可擴展的分佈式消息中間件,思路起源於LinkedIn的Kafka,但並非Kafka的一個拷貝。MetaQ具備消息存儲順序寫、吞吐量大和支持本地和XA事務等特性,適用於大吞吐量、順序消息、廣播和日誌數據傳輸等場景。
1.4.3數據庫分片分組
針對大數據量的數據庫,通常會採用"分片分組"策略:
分片(shard):主要解決擴展性問題,屬於水平拆分。引入分片,就引入了數據路由和分區鍵的概念。其中,分表解決的是數據量過大的問題,分庫解決的是數據庫性能瓶頸的問題。
分組(group):主要解決可用性問題,經過主從複製的方式實現,並提供讀寫分離策略用以提升數據庫性能。
1.4.4分佈式計算
分佈式計算( Distributed computing )是一種"把須要進行大量計算的工程數據分割成小塊,由多臺計算機分別計算;在上傳運算結果後,將結果統一合併得出數據結論"的科學。
當前的高性能服務器在處理海量數據時,其計算能力、內存容量等指標都遠遠沒法達到要求。在大數據時代,工程師採用廉價的服務器組成分佈式服務集羣,以集羣協做的方式完成海量數據的處理,從而解決單臺服務器在計算與存儲上的瓶頸。Hadoop、Storm以及Spark是經常使用的分佈式計算中間件,Hadoop是對非實時數據作批量處理的中間件,Storm和Spark是對實時數據作流式處理的中間件。
除此以外,還有更多的分佈式軟件解決方案,這裏就再也不一一介紹了。
1.5分佈式硬件部署方案
介紹完服務端的分佈式軟件解決方案,就不得不介紹一下服務端的分佈式硬件部署方案。這裏,只畫出了服務端常見的接口服務器、MySQL數據庫、Redis緩存,而忽略了其它的雲存儲服務、消息隊列服務、日誌系統服務……
1.5.1通常單機版部署方案
架構說明:只有1臺接口服務器、1個MySQL數據庫、1個可選Redis緩存,可能都部署在同一臺服務器上。
適用範圍:適用於演示環境、測試環境以及不怕宕機且日PV在5萬之內的小型商業應用。
1.5.2中小型分佈式硬件部署方案
架構說明:經過SLB/Nginx組成一個負載均衡的接口服務器集羣,MySQL數據庫和Redis緩存採用了一主一備(或多備)的部署方式。
適用範圍:適用於日PV在500萬之內的中小型商業應用。
1.5.3大型分佈式硬件部署方案
架構說明:經過SLB/Nginx組成一個負載均衡的接口服務器集羣,利用分片分組策略組成一個MySQL數據庫集羣和Redis緩存集羣。
適用範圍:適用於日PV在500萬以上的大型商業應用。
多線程最主要目的就是"最大限度地利用CPU資源",能夠把串行過程變成並行過程,從而提升了程序的執行效率。
2.1一個慢接口案例
假設在用戶登陸時,若是是新用戶,須要建立用戶信息,併發放新用戶優惠券。例子代碼以下:
// 登陸函數(示意寫法) public UserVO login(String phoneNumber, String verifyCode) { // 檢查驗證碼 if (!checkVerifyCode(phoneNumber, verifyCode)) { throw new ExampleException("驗證碼錯誤"); } // 檢查用戶存在 UserDO user = userDAO.getByPhoneNumber(phoneNumber); if (Objects.nonNull(user)) { return transUser(user); } // 建立新用戶 return createNewUser(user); } // 建立新用戶函數 private UserVO createNewUser(String phoneNumber) { // 建立新用戶 UserDO user = new UserDO(); ... userDAO.insert(user); // 綁定優惠券 couponService.bindCoupon(user.getId(), CouponType.NEW_USER); // 返回新用戶 return transUser(user); }
其中,綁定優惠券(bindCoupon)是給用戶綁定新用戶優惠券,而後再給用戶發送推送通知。若是隨着優惠券數量愈來愈多,該函數也會變得愈來愈慢,執行時間甚至超過1秒,而且沒有什麼優化空間。如今,登陸(login)函數就成了名副其實的慢接口,須要進行接口優化。
2.2採用多線程優化
經過分析發現,綁定優惠券(bindCoupon)函數能夠異步執行。首先想到的是採用多線程解決該問題,代碼以下:
// 建立新用戶函數 private UserVO createNewUser(String phoneNumber) { // 建立新用戶 UserDO user = new UserDO(); ... userDAO.insert(user); // 綁定優惠券 executorService.execute(()->couponService.bindCoupon(user.getId(), CouponType.NEW_USER)); // 返回新用戶 return transUser(user); }
如今,在新線程中執行綁定優惠券(bindCoupon)函數,使用戶登陸(login)函數性能獲得很大的提高。可是,若是在新線程執行綁定優惠券函數過程當中,系統發生重啓或崩潰致使線程執行失敗,用戶將永遠獲取不到新用戶優惠券。除非提供用戶手動領取優惠券頁面,不然就須要程序員後臺手工綁定優惠券。因此,用採用多線程優化慢接口,並非一個完善的解決方案。
2.3採用消息隊列優化
若是要保證綁定優惠券函數執行失敗後可以重啓執行,能夠採用數據庫表、Redis隊列、消息隊列的等多種解決方案。因爲篇幅優先,這裏只介紹採用MetaQ消息隊列解決方案,並省略了MetaQ相關配置僅給出了核心代碼。
消息生產者代碼:
// 建立新用戶函數 private UserVO createNewUser(String phoneNumber) { // 建立新用戶 UserDO user = new UserDO(); ... userDAO.insert(user); // 發送優惠券消息 Long userId = user.getId(); CouponMessageDataVO data = new CouponMessageDataVO(); data.setUserId(userId); data.setCouponType(CouponType.NEW_USER); Message message = new Message(TOPIC, TAG, userId, JSON.toJSONBytes(data)); SendResult result = metaqTemplate.sendMessage(message); if (!Objects.equals(result, SendStatus.SEND_OK)) { log.error("發送用戶({})綁定優惠券消息失敗:{}", userId, JSON.toJSONString(result)); } // 返回新用戶 return transUser(user); }
注意:可能出現發生消息不成功,可是這種機率相對較低。
消息消費者代碼:
// 優惠券服務類 @Slf4j @Service public class CouponService extends DefaultMessageListener<String> { // 消息處理函數 @Override @Transactional(rollbackFor = Exception.class) public void onReceiveMessages(MetaqMessage<String> message) { // 獲取消息體 String body = message.getBody(); if (StringUtils.isBlank(body)) { log.warn("獲取消息({})體爲空", message.getId()); return; } // 解析消息數據 CouponMessageDataVO data = JSON.parseObject(body, CouponMessageDataVO.class); if (Objects.isNull(data)) { log.warn("解析消息({})體爲空", message.getId()); return; } // 綁定優惠券 bindCoupon(data.getUserId(), data.getCouponType()); } }
解決方案優勢:採集MetaQ消息隊列優化慢接口解決方案的優勢:
3.1.原有的採購流程
這是一個簡易的採購流程,由庫管系統發起採購,採購員開始採購,採購員完成採購,同時迴流採集訂單到庫管系統。
其中,完成採購動做的核心代碼以下:
/** 完成採購動做函數(此處省去獲取採購單/驗證狀態/鎖定採購單等邏輯) */ public void finishPurchase(PurchaseOrder order) { // 完成相關處理 ...... // 迴流採購單(調用HTTP接口) backflowPurchaseOrder(order); // 設置完成狀態 purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue()); }
因爲函數backflowPurchaseOrder(迴流採購單)調用了HTTP接口,可能引發如下問題:
3.2.優化的採購流程
經過需求分析,把"採購員完成採購並回流採集訂單"動做拆分爲"採購員完成採購"和"迴流採集訂單"兩個獨立的動做,把"採購完成"拆分爲"採購完成"和"迴流完成"兩個獨立的狀態,更方便採購流程的管理和實現。
拆分採購流程的動做和狀態後,核心代碼以下:
/** 完成採購動做函數(此處省去獲取採購單/驗證狀態/鎖定採購單等邏輯) */ public void finishPurchase(PurchaseOrder order) { // 完成相關處理 ...... // 設置完成狀態 purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue()); } /** 執行迴流動做函數(此處省去獲取採購單/驗證狀態/鎖定採購單等邏輯) */ public void executeBackflow(PurchaseOrder order) { // 迴流採購單(調用HTTP接口) backflowPurchaseOrder(order); // 設置迴流狀態 purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue()); }
其中,函數executeBackflow(執行迴流)由定時做業觸發執行。若是迴流採購單失敗,採購單狀態並不會修改成"已迴流";等下次定時做業執行時,將會繼續執行迴流動做;直到迴流採購單成功爲止。
3.3.有限狀態機介紹
3.3.1概念
有限狀態機(Finite-state machine,FSM),又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的一個數學模型。
3.3.2要素
狀態機可概括爲4個要素:現態、條件、動做、次態。
現態:指當前流程所處的狀態,包括起始、中間、終結狀態。
條件:也可稱爲事件;當一個條件被知足時,將會觸發一個動做並執行一次狀態的遷移。
動做:當條件知足後要執行的動做。動做執行完畢後,能夠遷移到新的狀態,也能夠仍舊保持原狀態。
次態:當條件知足後要遷往的狀態。「次態」是相對於「現態」而言的,「次態」一旦被激活,就轉變成新的「現態」了。
3.3.3狀態
狀態表示流程中的持久狀態,流程圖上的每個圈表明一個狀態。
初始狀態: 流程開始時的某一狀態;
中間狀態: 流程中間過程的某一狀態;
終結狀態: 流程完成時的某一狀態。
使用建議:
6.3.4動做
動做的三要素:角色、現態、次態,流程圖上的每一條線表明一個動做。
角色: 誰發起的這個操做,能夠是用戶、定時任務等;
現態: 觸發動做時當前的狀態,是執行動做的前提條件;
次態: 完成動做後達到的狀態,是執行動做的最終目標。
使用建議:
4.1.直接經過數據庫交互
在一些項目中,系統間交互不經過接口調用和消息隊列,而是經過數據庫直接訪問。問其緣由,回答道:"項目工期太緊張,直接訪問數據庫,簡單又快捷"。
仍是以上面的採購流程爲例——採購訂單由庫管系統發起,由採購系統負責採購,採購完成後通知庫管系統,庫管系統進入入庫操做。採購系統採購完成後,通知庫管系統數據庫的代碼以下:
/** 執行迴流動做函數(此處省去獲取採購單/驗證狀態/鎖定採購單等邏輯) */ public void executeBackflow(PurchaseOrder order) { // 完成原始採購單 rawPurchaseOrderDAO.setStatus(order.getRawId(), RawPurchaseOrderStatus.FINISHED.getValue()); // 設置迴流狀態 purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue()); }
其中,經過rawPurchaseOrderDAO(原始採購單DAO)直接訪問庫管系統的數據庫表,並設置原始採購單狀態爲已完成。
通常狀況下,直接經過數據訪問的方式是不會有問題的。可是,一旦發生競態,就會致使數據不一樣步。有人會說,能夠考慮使用同一分佈式鎖解決該問題。是的,這種解決方案沒有問題,只是又在系統間共享了分佈式鎖。
直接經過數據庫交互的缺點:
4.2.經過Dubbo接口交互
因爲採購系統和庫管系統都是內部系統,能夠經過相似Dubbo的RPC接口進行交互。
庫管系統代碼:
/** 採購單服務接口 */ public interface PurchaseOrderService { /** 完成採購單函數 */ public void finishPurchaseOrder(Long orderId); } /** 採購單服務實現 */ @Service("purchaseOrderService") public class PurchaseOrderServiceImpl implements PurchaseOrderService { /** 完成採購單函數 */ @Override @Transactional(rollbackFor = Exception.class) public void finishPurchaseOrder(Long orderId) { // 相關處理 ... // 完成採購單 purchaseOrderService.finishPurchaseOrder(order.getRawId()); } }
其中,庫管系統經過Dubbo把PurchaseOrderServiceImpl(採購單服務實現)以PurchaseOrderService(採購單服務接口)定義的接口服務暴露給採購系統。這裏,省略了Dubbo開發服務接口相關配置。
採購系統代碼:
/** 執行迴流動做函數(此處省去獲取採購單/驗證狀態/鎖定採購單等邏輯) */ public void executeBackflow(PurchaseOrder order) { // 完成採購單 purchaseOrderService.finishPurchaseOrder(order.getRawId()); // 設置迴流狀態 purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue()); }
其中,purchaseOrderService(採購單服務)爲庫管系統PurchaseOrderService(採購單服務)在採購系統中的Dubbo服務客戶端存根,經過該服務調用庫管系統的服務接口函數finishPurchaseOrder(完成採購單函數)。
這樣,採購系統和庫管系統本身的強關聯,經過Dubbo就簡單地實現了系統隔離和解耦。固然,除了採用Dubbo接口外,還能夠採用HTTPS、HSF、WebService等同步接口調用方式,也能夠採用MetaQ等異步消息通知方式。
4.3常見系統間交互協議
4.3.1同步接口調用
同步接口調用是以一種阻塞式的接口調用機制。常見的交互協議有:
4.3.2異步消息通知
異步消息通知是一種通知式的信息交互機制。當系統發生某種事件時,會主動通知相應的系統。常見的交互協議有:
4.4.常見系統間交互方式
4.4.1請求-應答
適用範圍:適合於簡單的耗時較短的接口同步調用場景,好比Dubbo接口同步調用。
4.4.2通知-確認
適用範圍:適合於簡單的異步消息通知場景,好比MetaQ消息通知。
4.4.3請求-應答-查詢-返回
適用範圍:適合於複雜的耗時較長的接口同步調用場景,好比提交做業任務並按期查詢任務結果。
4.4.4請求-應答-回調
適用範圍:適合於複雜的耗時較長的接口同步調用和異步回調相結合的場景,好比支付寶的訂單支付。
4.4.5請求-應答-通知-確認
適用範圍:適合於複雜的耗時較長的接口同步調用和異步消息通知相結合的場景,好比提交做業任務並等待完成消息通知。
4.4.6通知-確認-通知-確認
適用範圍:適合於複雜的耗時較長的異步消息通知場景。
在數據查詢時,因爲未能對將來數據量作出正確的預估,不少狀況下都沒有考慮數據的分頁查詢。
5.1.普通查詢案例
如下是查詢過時訂單的代碼:
/** 訂單DAO接口 */ public interface OrderDAO { /** 查詢過時訂單函數 */ @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day)") public List<OrderDO> queryTimeout(); } /** 訂單服務接口 */ public interface OrderService { /** 查詢過時訂單函數 */ public List<OrderVO> queryTimeout(); }
當過時訂單數量不多時,以上代碼不會有任何問題。可是,當過時訂單數量達到幾十萬上千萬時,以上代碼就會出現如下問題:
因此,在數據查詢時,特別是不能預估數據量的大小時,須要考慮數據的分頁查詢。
這裏,主要介紹"設置最大數量"和"採用分頁查詢"兩種方式。
5.2設置最大數量
"設置最大數量"是一種最簡單的分頁查詢,至關於只返回第一頁數據。例子代碼以下:
/** 訂單DAO接口 */ public interface OrderDAO { /** 查詢過時訂單函數 */ @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit 0, #{maxCount}") public List<OrderDO> queryTimeout(@Param("maxCount") Integer maxCount); } /** 訂單服務接口 */ public interface OrderService { /** 查詢過時訂單函數 */ public List<OrderVO> queryTimeout(Integer maxCount); }
適用於沒有分頁需求、但又擔憂數據過多致使內存溢出、數據量過大的查詢。
5.3採用分頁查詢
"採用分頁查詢"是指定startIndex(開始序號)和pageSize(頁面大小)進行數據查詢,或者指定pageIndex(分頁序號)和pageSize(頁面大小)進行數據查詢。例子代碼以下:
/** 訂單DAO接口 */ public interface OrderDAO { /** 統計過時訂單函數 */ @Select("select count(*) from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day)") public Long countTimeout(); /** 查詢過時訂單函數 */ @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}") public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize") Integer pageSize); } /** 訂單服務接口 */ public interface OrderService { /** 查詢過時訂單函數 */ public PageData<OrderVO> queryTimeout(Long startIndex, Integer pageSize); }
適用於真正的分頁查詢,查詢參數startIndex(開始序號)和pageSize(頁面大小)可由調用方指定。
5.4分頁查詢隱藏問題
假設,咱們須要在一個定時做業(每5分鐘執行一次)中,針對已經超時的訂單(status=5,建立時間超時30天)進行超時關閉(status=10)。實現代碼以下:
/** 訂單DAO接口 */ public interface OrderDAO { /** 查詢過時訂單函數 */ @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit #{startIndex}, #{pageSize}") public List<OrderDO> queryTimeout(@Param("startIndex") Long startIndex, @Param("pageSize") Integer pageSize); /** 設置訂單超時關閉 */ @Update("update t_order set status = 10 where id = #{orderId} and status = 5") public Long setTimeoutClosed(@Param("orderId") Long orderId) } /** 關閉過時訂單做業類 */ public class CloseTimeoutOrderJob extends Job { /** 分頁數量 */ private static final int PAGE_COUNT = 100; /** 分頁大小 */ private static final int PAGE_SIZE = 1000; /** 做業執行函數 */ @Override public void execute() { for (int i = 0; i < PAGE_COUNT; i++) { // 查詢處理訂單 List<OrderDO> orderList = orderDAO.queryTimeout(i * PAGE_COUNT, PAGE_SIZE); for (OrderDO order : orderList) { // 進行超時關閉 ...... orderDAO.setTimeoutClosed(order.getId()); } // 檢查處理完畢 if(orderList.size() < PAGE_SIZE) { break; } } } }
粗看這段代碼是沒有問題的,嘗試循環100次,每次取1000條過時訂單,進行訂單超時關閉操做,直到沒有訂單或達到100次爲止。可是,若是結合訂單狀態一塊兒看,就會發現從第二次查詢開始,每次會忽略掉前startIndex(開始序號)條應該處理的過時訂單。這就是分頁查詢存在的隱藏問題:
當知足查詢條件的數據,在操做中再也不知足查詢條件時,會致使後續分頁查詢中前startIndex(開始序號)條知足條件的數據被跳過。
能夠採用"設置最大數量"的方式解決,代碼以下:
/** 訂單DAO接口 */ public interface OrderDAO { /** 查詢過時訂單函數 */ @Select("select * from t_order where status = 5 and gmt_create < date_sub(current_timestamp, interval 30 day) limit 0, #{maxCount}") public List<OrderDO> queryTimeout(@Param("maxCount") Integer maxCount); /** 設置訂單超時關閉 */ @Update("update t_order set status = 10 where id = #{orderId} and status = 5") public Long setTimeoutClosed(@Param("orderId") Long orderId) } /** 關閉過時訂單做業(定時做業) */ public class CloseTimeoutOrderJob extends Job { /** 分頁數量 */ private static final int PAGE_COUNT = 100; /** 分頁大小 */ private static final int PAGE_SIZE = 1000; /** 做業執行函數 */ @Override public void execute() { for (int i = 0; i < PAGE_COUNT; i++) { // 查詢處理訂單 List<OrderDO> orderList = orderDAO.queryTimeout(PAGE_SIZE); for (OrderDO order : orderList) { // 進行超時關閉 ...... orderDAO.setTimeoutClosed(order.getId()); } // 檢查處理完畢 if(orderList.size() < PAGE_SIZE) { break; } } } }
雙11福利來了!先來康康#怎麼買雲服務器最便宜# [並不簡單]參團購買指定配置雲服務器僅86元/年,開團拉新享三重禮:1111紅包+瓜分百萬現金+31%返現,爆款必買清單,還有iPhone 11 Pro、衛衣、T恤等你來抽,立刻來試試手氣!https://www.aliyun.com/1111/2019/home?utm_content=g_1000083110
本文做者: 常意
本文來自雲棲社區合做夥伴「阿里技術」,如需轉載請聯繫原做者。