REVIEW
王氏陽明のマインド有名なマスターは明代、「教育記録」に行きます:
ロードなしのVCDやDVD、VCDとやDVDは、人に見られませんでした。この部屋のように、人々は、私は非常に大きい規模を見て、早く来ます。長い、その後、カラム壁などで、一つ一つは、それを理解します。いくつかのwenzao列として、長く、そしてすべてはそれを見ます。しかし、ちょうど部屋。
はい、そこに実際に任意の理論的な知識のVCDやポイントのDVDを、意識のちょうど別の人のレベル。私たちは様々なオプションの長所と短所を区別することができ、スタートアップ企業に年間のために戦ったJavaサーバ・アーキテクチャのさまざまな連絡、はるかに自然に深い理解を参照してください。ここで、著者は、Javaサーバのスタートアップがいくつかの問題が存在してまとめたものであり、いくつかの未成熟なソリューションを提供しようとしました。
1.システムは、分散されていません
インターネットの発展に伴い、コンピュータシステムは、長い間一緒に仕事を複数のマシンに独立して、スタンドアロンの仕事から移行されています。コンピュータクラスタは、理論に基づいて大規模かつ複雑な分散アプリケーションサービスを構築し、方法に存在し、それが長い間人気があり、広く応用されています。しかし、まだ多くのスタートアップソフトウェアシステムがにとどまりある「スタンドアローン。」
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キーワードのみを仮想マシン内で有効になりますので、2人が同時に注文を取るためにラッシュにつながる可能性があるため、二つのサーバ上で同時に実行するが、データベースは、最後の1に書き込まれている場合データ対象。だから、システムのスタンドアロンバージョンのほとんどは、分散システムとして動作していません。
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);
}
最適化されたコード、分散ロックorderDistributedLock(受注分散ロック)を使用してロックをロックし、解放するために、前と後の関数grabOrderWithoutLock(注文を取らないようにロックされたラッシュ)を呼び出し、synchronizedキーワードで基本的な効果のスタンドアロンバージョンをロック同じ。
分散システムの1.3長所と短所
分散システム(分散システム)分散処理をサポートするためのソフトウェアシステムでは、分散オペレーティングシステム、分散型プログラミング言語とコンパイラシステム、分布などのタスクを、ネットワークマルチプロセッサアーキテクチャ通信のシステムによって実行されますファイルシステムは、データベース・システムを配布しました。
分散システムの利点:
- 信頼性、耐障害性:
一つのサーバの崩壊は、他のサーバーは、まだサービスを提供することができ、他のサーバーには影響しません。
- スケーラビリティ:
システムサービス機能場合は、追加のサーバーは、水平方向に拡張することができます。
- 柔軟性:
簡単に、インストールの実施形態では、拡張やシステムをアップグレードすることができます。
- 高性能:
これは、コンピューティングパワーを複数のサーバー、単一のサーバよりも速い処理速度を持っています。
- 費用対効果:
分散システムサーバのハードウェア要件は、あなたがお金のためのより良い値が得られ、分散した低コストのサーバークラスタを構築するために使用することができ、非常に低いです。
分散型システムの欠点:
- 難易度の高いトラブルシューティング:
システムは、トラブルシューティングおよび問題診断がより難しく、複数のサーバに分散されているためです。
- 以下のソフトウェアのサポート:
分散システム・ソフトウェア・ソリューションは、以下をサポートします。
- 高い建設費:
分散システムを構築するために、複数のサーバーを必要としています。
私の友人のアドバイスがたくさんありました:「アウトソーシングをお探しのモバイルアプリケーションは、どのような事項に注意を払う必要がありますか?。」
まず、分散システムの必要性を判断します。どのように多くのソフトウェアの予算?どのように多くのユーザー達すると予想されていますか?どのくらいのトラフィックを期待されていますか?それだけで水事業の早期バージョンをテストすることですか?単一のサーバは解決することができますか?短いダウンタイムを受信するかどうか?......一緒になった場合は、スタンドアローンのシステムが解決することができ、その後、分散システムを採用していません。非常に異なるスタンドアロンおよび分散システムなので、ソフトウェアの開発コストを対応する差分も高いです。
第二に、真のシステムを分散するかどうかを判断します。分散システムの最大の特徴が不十分システム容量がある場合、サービスはを通じて、ある水平拡張サービス容量を増加させるためにサーバを追加することにより、方法の。しかし、展開のレベルをサポートしていないシステムのスタンドアロンバージョンは、拡張がデータ一連の問題につながる余儀なくされました。R&D費の差は、スタンドアロンおよび分散システムが大きいため、市場は場所にアウトソーシングチームのほとんどは、スタンドアロンシステムと分散システムを提供します。だから、どのようにあなたのシステムがそれの本当の意味での分散システムであることを確認するには?ソフトウェアから、採用するかどうかを分散ソフトウェア・ソリューションを、それがハードウェアからのもので、採用するかどうかを分散ハードウェアの展開シナリオを。
1.4。分散型ソフトウェアソリューション
分散システムとしての資格、実際の需要に基づいて、適切な分散型ソフトウェアソリューションを採用する必要があります。
1.4.1。分散ロック
分散単一のロックは、主に、異なるサービスロジックとデータとの整合性を確保するために用いて、分散システムの物理または論理ブロックをロックするために、ロック拡張です。
現時点では、分散ロックの実装の主流は、3種類があります。
- 分散ロック・データベースの実装。
- 分散ロックRedisの実装。
- 飼育係のロックベースの実装を配布しました。
1.4.2。分散型のメッセージ
分散システム・ソフトウェア・インフラストラクチャでメッセージを送受信サポートするために、メッセージングミドルウェアを配布しました。一般的な分散型メッセージングミドルウェアは、その上のActiveMQ、RabbitMQの、カフカ、MetaQとしています。
MetaQ(変態フルネーム)は、高性能、高可用性、スケーラブルな分散型メッセージングミドルウェア、カフカのLinkedIn発祥のアイデアが、カフカのではないコピーです。MetaQメッセージは、シーケンシャル書き込みストレージ、スループット、およびこのような大規模なスループットのために、同じ大きさのローカルおよびXAトランザクション・サポート機能、注文メッセージ、放送データ伝送を有し、シーンを記録します。
1.4.3データベースのパケットの断片化
大量のデータのためのデータベース、通常は「スライスグループ」戦略:
フラグメント(シャード):主な水平解像度に属する、スケーラビリティの問題を解決します。断片を導入、それがデータのルーティングおよびパーティショニングキーの概念を導入しています。このうち、サブテーブルは、過剰なデータの問題を解決するために、サブライブラリーは、データベースのパフォーマンスのボトルネックの問題を解決します。
グループ(グループ):によって解決する主な問題の可用性、レプリケーションマスタの実装、および提供する別の読み取りと書き込み、データベースのパフォーマンスを向上させるための戦略を。
1.4.4。分散コンピューティング
分散コンピューティング(分散コンピューティング)の一種である「それぞれ複数のコンピュータで計算小さな断片に計算集約型のエンジニアリングデータの必要性、;操作の結果をアップロードした後、データの統合の結果は結論描かれた」科学のを。
現在の高性能サーバの電源、メモリ容量や他の指標を計算し、大量のデータを処理する要件を満たすことができないはるか。ビッグデータの時代では、エンジニアは、単一のサーバーのボトルネックを解決し、コンピュータに保存されているように、大量のデータの処理を完了するために、協調的な方法でクラスタ化するためのサービスクラスタの分散安いサーバーを使用しています。Hadoopの、嵐とスパークは、一般的な分散コンピューティングミドルウェアで、Hadoopがバッチ処理ミドルウェア、嵐とスパークは、リアルタイムのストリーミングデータミドルウェアのためにやっている行うには非リアルタイムデータです。
また、より多くのソフトウェア・ソリューションが配布され、もはや一つ一つが導入されません。
1.5。分散型ハードウェアの展開
分散型ソフトウェアソリューションのサービス側の紹介、ハードウェアは、分散サーバ側の展開を導入する必要があります。他のクラウドストレージサービスを無視してここでは、唯一、一般的なサーバー・インターフェース・サーバ、MySQLデータベース、Redisのキャッシュを描画するために、メッセージは、サービス、ログシステムサービスをキューに入れ......
1.5.1一般的なスタンドアロン展開
アーキテクチャの説明:
わずか1駅・インターフェース・サーバ、MySQLデータベース、オプションのRedisのキャッシュは、同じサーバー上に展開していることがあります。
スコープ:
プレゼンテーション環境、テスト環境および50,000未満でダウンタイムと日本のPV小規模なビジネスアプリケーションを恐れていないに適しています。
1.5.2。SME分散ハードウェアの展開
アーキテクチャの説明:
SLB / nginxのインターフェースによってサーバの負荷分散クラスタ、MySQLデータベースRedisのキャッシュとデバイス(または装置)の使用を主展開成ります。
スコープ:
PVは500万内の日本における中小商業のアプリケーションに適しています。
1.5.3。大規模な分散ハードウェアの展開
アーキテクチャの説明:
クラスタとMySQLデータベースRedisのキャッシュクラスタを形成するために、スライスグループの戦略を用いて、負荷分散サーバクラスタからなるSLB / nginxのインターフェースによって。
スコープ:
PVの日5億以上の大規模商業用途に適しています。
2.誤って使用するマルチスレッド
主な目的は、「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秒より遅いと遅くなる実行時間となり、最適化の余地はありません。さて、ログイン(ログイン)関数は、正真正銘のインターフェイスを最適化する必要がある、インタフェースが遅くなっています。
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)関数を実行します。あなたは新しいスレッドにバインド処理クーポン機能を実行する場合は、系統発生的に再起動またはクラッシュは、ユーザーが永遠に新しいユーザーのクーポンを取得することはありません、スレッドの実行が失敗する原因となります。ユーザーがない限り、手動でクーポンページを受け取り、または手動でクーポンの背景をバインドするために、プログラマを必要とします。したがって、マルチスレッドと低速インターフェースを最適化するために、これは完璧な解決策ではありません。
使用して、メッセージキュー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消息队列优化慢接口解决方案的优点:
- 如果系统发生重启或崩溃,导致消息处理函数执行失败,不会确认消息已消费;由于MetaQ支持多服务订阅同一队列,该消息可以转到别的服务进行消费,亦或等到本服务恢复正常后再进行消费。
- 消费者可多服务、多线程进行消费消息,即便消息处理时间较长,也不容易引起消息积压;即便引起消息积压,也可以通过扩充服务实例的方式解决。
- 如果需要重新消费该消息,只需要在MetaQ管理平台上点击"消息验证"即可。
3.流程定义不合理
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.状态
状态表示流程中的持久状态,流程图上的每一个圈代表一个状态。
初始状态: 流程开始时的某一状态;
中间状态: 流程中间过程的某一状态;
终结状态: 流程完成时的某一状态。
使用建议:
- 状态必须是一个持久状态,而不能是一个临时状态;
- 终结状态不能是中间状态,不能继续进行流程流转;
- 状态划分合理,不要把多个状态强制合并为一个状态;
- 状态尽量精简,同一状态的不同情况可以用其它字段表示。
3.3.4.动作
动作的三要素:角色、现态、次态,流程图上的每一条线代表一个动作。
角色: 谁发起的这个操作,可以是用户、定时任务等;
现态: 触发动作时当前的状态,是执行动作的前提条件;
次态: 完成动作后达到的状态,是执行动作的最终目标。
使用建议:
- 每个动作执行前,必须检查当前状态和触发动作状态的一致性;
- 状态机的状态更改,只能通过动作进行,其它操作都是不符合规范的;
- 需要添加分布式锁保证动作的原子性,添加数据库事务保证数据的一致性;
- 类似的动作(比如操作用户、请求参数、动作含义等)可以合并为一个动作,并根据动作执行结果转向不同的状态。
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.同步接口调用
同步接口调用是以一种阻塞式的接口调用机制。常见的交互协议有:
- HTTP/HTTPS接口;
- WebService接口;
- Dubbo/HSF接口;
- CORBA接口。
4.3.2.异步消息通知
异步消息通知是一种通知式的信息交互机制。当系统发生某种事件时,会主动通知相应的系统。常见的交互协议有:
- MetaQ的消息通知;
- CORBA消息通知。
4.4.常见系统间交互方式
4.4.1.请求-应答
适用范围:
适合于简单的耗时较短的接口同步调用场景,比如Dubbo接口同步调用。
4.4.2.通知-确认
适用范围:
适合于简单的异步消息通知场景,比如MetaQ消息通知。
4.4.3.请求-应答-查询-返回
适用范围:
适合于复杂的耗时较长的接口同步调用场景,比如提交作业任务并定期查询任务结果。
4.4.4.请求-应答-回调
适用范围:
适合于复杂的耗时较长的接口同步调用和异步回调相结合的场景,比如支付宝的订单支付。
4.4.5.请求-应答-通知-确认
适用范围:
适合于复杂的耗时较长的接口同步调用和异步消息通知相结合的场景,比如提交作业任务并等待完成消息通知。
4.4.6.通知-确认-通知-确认
适用范围:
适合于复杂的耗时较长的异步消息通知场景。
5.数据查询不分页
在数据查询时,由于未能对未来数据量做出正确的预估,很多情况下都没有考虑数据的分页查询。
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.3.分页查询隐藏问题
假设,我们需要在一个定时作业(每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;
}
}
}
}
后记
本文是《那些年,我们见过的Java服务端“乱象”》的姐妹篇,前文主要介绍的是Java服务端规范上的问题,而本文主要介绍的是Java服务端方案上的问题。
今年そのチームの下に「KKのカープール」「E-世代ドライブ」に、この文書と同じように、一度に代わって年間の郷愁は、ドライバーが夜の護衛遅く返す駆動、兄弟と戦った逃しました。深く後悔、「KKのカープールは、」適切に開発するために十分な時間ではなく、新興国た、同社のアームが廃止されました。良いニュースは、人々の心を「KKは相乗り」、今となっていると言われている「市民社会。」