第VII章、並行プログラミング実際のアイテム

タスクの同時実行フレームワーク

何という建築家?

ソフトウェアプロジェクトの開発プロセスでは、計画やテキストの仕様の開発のための顧客の需要を変換すると、このプロジェクトの全体的な枠組みを開発するために、計画の男性を導くために開発チーム全体の完成は、建築家です。一般的には、ほとんどのシニア専門家と技術者は、建築家は、最初のシニアJava開発者でなければならないと言うことができるプロジェクトです。

主な任務

主にアーキテクチャ設計、ソフトウェア開発、特に含みます

1、ニーズを把握

プロジェクトの開発プロセスでは、建築家は、要求仕様の完全な介入では、要求仕様は、建築家によって承認されなければならないです。アーキテクトは、顧客ニーズの彼らの完全かつ正確な理解を確実にするために、繰り返しの交流を必要とアナリスト。

2、システムの分解

ユーザのニーズに基づいて、システムは、システム設計の全体は、異なるサービスまたは論理層を形成するために、より小さなサブシステムおよびコンポーネントに分解する方法、層状、層状必要。その後、建築家は、それぞれ他の層に層の界面との間の関係を決定します。システムアーキテクトの層化、「縦」の分解に、だけでなく、同じ論理ブロック・レベル、「水平」の分解だけでなく。
ソフトウェアアーキテクトは、この基本的なスキルを反映して、これは比較的複雑な作業です。

3、技術選択

ソフトウェアの全体的なアーキテクチャで最高潮に達するシステムの分解、一連の建築家、。技術の選択は、継続的にボトルネックやシステムの弱点を見つけることであるソフトウェアアーキテクチャに依存し、分割統治の使用は、キャッシュ、非同期、クラスタリングを意味し、徐々に解決、およびシステムの要件(パフォーマンス、セキュリティ、可用性、拡張性、拡張性に対処するためにバランスをとります... )プロセス。このようなアーキテクチャを形成します。

優れたアーキテクチャアーキテクチャのどのような?
答:それは、現在のビジネスやチームのメンバーに適用可能であり、かつ適切な前向きな(成長の最大6ヶ月)での良好なアーキテクチャを保持しています。
Webサーバーは、Windows上またはLinux上で実行していますか?MSSQL、OracleまたはMySQLを使用してデータベース?MVCやSpringと他の軽量フレームワークを使用する必要はありませんが必要ですか?フロントエンドリッチクライアントまたはシンクライアントモード?同様の作業は、この段階で行い、かつ、評価する必要があります。
評価に限ら製品や技術、建築家の選択は、何の決断はありません、最終的な決定は、プロジェクトマネージャーを分類しました。建築家は、重要な参考情報を提供するために、プロジェクトマネージャーのための技術的な解決策を提案し、プロジェクトマネージャは、プロジェクトの予算の実際の状況、人的資源、タイムスケジュール、および最終確認から秤量されます。

4、技術仕様の開発

プロジェクトの開発プロセスにおけるアーキテクトは、技術的な権威です。彼は、開発者のすべてを調整する必要があり、開発者は、そのアーキテクチャの意思に従い、開発者は様々な機能を実装していることを確認し、常に通信してきました。

5、コア、開発や難易度の主要課題

6、開発と管理

、企画の製品ライン、推定人的・時間資源責任分担を整理、エンジニア、リスク評価と管理のプロセスを導くために計画のマイルストーンを決定:通常、管理機能の数を取る必要があります。これらの事項は、製品の技術的なアーキテクチャ、機能モジュール部門を管理する必要があり、関係する技術的なリスクは建築家に精通し、または直接責任があります。

図7に示すように、通信と連携

内側と外側のプロジェクト様々な役割で、プロジェクトチームのコミュニケーションと協調、我々は上の人に対処する建築家でかなりの時間を言うことができます。人間関係は、インフラやプロジェクトの成功に不可欠です。

建築家の側面

効果

システムアーキテクチャ設計を担当するだけでなく、着陸、進化と発展、復興の推進のアーキテクチャの実装を担当。
消防士の役割、システム障害や「超自然現象」などの行為は、私たちが解決ロビーするように依頼します。
アーキテクトは、より深い理解、自社の技術のアイデアを促進することを望んで、他の人と知識を共有して喜ん技術で時には確固たる信念、の特定の領域を持っています。

効果

どんなに困難で複雑なプロジェクトで、限り良い建築家があるとして、あなたは、プロジェクトが正常に完了しなければならないことを確信できなくなります。優れた建築家は、より重要なのは勝つために意志であるだけでなく、技術および方法、プロジェクトチームにもたらします。この信念は、彼自身の累積ガス田と影響力の建築家です。
最も困難と挑戦的なモジュールで、通常は建築家の木材技術開発プロジェクトは、このようにプロジェクト全体の円滑な進行のために道を開きます。これらのモジュールは、基本的な枠組み、共通のコンポーネント、およびその他の共通サービスプラットフォーム製品が含まれます。大規模なインターネットアプリケーションでは、インフラストラクチャサービスは、データストレージおよび処理サービスのコアビジネスの膨大な量を負担し、挑戦的な作品がたくさんあります。したがって、私たちの実際の戦闘は、プロジェクトやパフォーマンスの最適化の基本的な枠組みを達成することです。

第二に、基本的な枠組みを達成するために

1、需要創出と分析

同社は、バッチ試験グループは、多くの場合、再行動放電の対象である、と編集の対象は、コンテンツの条件に応じて、テストグループは、ドキュメントのバッチは、オフラインで生成された、2つのプロジェクトグループを持っています。実際の製品のユーザーのオンライン調査で設定されたアーキテクチャは、これらの機能は、実際の使用で見つかった、ユーザーの応答が非常に遅く、タスクの提出後、任務の遂行を知らない、しないのですか?何をしてステップアップできますか?どのような成功?何が失敗しましたか?私たちはそうではありませんし、知ることができません。
アーキテクチャグループとフロントエンドWebが後でバックグラウンドタスクに提出しているため、彼らは言う、通信するために、実際の開発者は、複数のドキュメントや話題に処理されるので、それをスピードアップ。マルチスレッドを改善するためのヒントは、実際の開発者がマルチスレッド使用されていないと言う、使用する方法を知っているだけでなく、悪いと心配しないでください。以上を踏まえ、アーキテクチャグループは、ユーザーとビジネス開発上の痛みのポイントに対処するために、同社の基盤コンポーネントライブラリ内の同時タスク実行フレームワークを提供することを決定した:
1)バッチ式ミッションに統一された開発インターフェースを提供するために
2を)利用可能性にビジネス向けの開発者は、
バッチジョブ実行スケジュール3)の要件を照会することができます

何をすべきか2、

バッチジョブのようなフレームワーク同時実行を実現するために、我々は何をすべきかを分析する必要がありますか?

2.1、バッチジョブは、パフォーマンスを向上させるために:

私たちは、必ずしもビジネスの開発者に優しいとシンプルで使用するために、詳細ができるだけ背後のjava並行プログラミングの一部を遮蔽するために必要な、Javaの、マルチスレッドを使用したいので、彼らはキューをブロックし、容器によって複雑に理解する必要はありません、非同期知識のタスク、スレッドの安全性など、ちょうど彼らのビジネスプロセスに集中ができます。

2.2各バッチタスクは独自のコンテキストがあります。

プロジェクトグループは、同じ時間内に処理するバッチタスクので、オフラインでの文書作成になります別の学校のバッチがあるかもしれない、そのようなテストグループとして、より多くのがあるかもしれない、とそう、同じ時間仕事で教師があるだろう試験は、被験者の異なるグループになります各タスクの同時実行、セキュアコンテナ保存された属性情報を必要とし、

2.3、自動クリアがタスクを完了し、延滞されています

ステータス問い合わせを提供するために、システムは、クエリ内の各タスクのメモリにメンテナンススケジュール情報を必要としませんが、このクエリでは期間限定で、タスクがいくつかの時間を完了した後に、それはもはや状況照会がオンになって提供されるため、我々は自動的にクリアすると、期限切れのタスクは、ポーリングそれのタイミングで、完了している必要がありますか?
ここに画像を挿入説明

3、特定の実装

同時タスクは、フレームワークの実装の進捗状況を確認することができます

3.1、ユーザーのビジネスメソッドの結果?

実行の方法の結果は、いくつかの可能性がありますか?成功の三種類:結果の予想される流れに応じて、失敗:結果が意図したような処理ではありません。例外は:期待通りに予期しないエラーがスロー流れません。だから我々は、列挙を定義する3つのケースを表し、

public enum TaskResultType {
	success,/*方法执行完成,业务结果也正确*/
	failure,/*方法执行完成,业务结果错误*/
	exception/*方法执行抛出了异常*/
}

ビジネスメソッドの実装の結果を得るためには、ユーザー定義のオブジェクトの種類があり、多くの可能な戻り値、基本タイプ、システム定義のオブジェクトタイプが存在している、我々は一般的な用語で、この結果を表現する必要があります。一方、メソッドが失敗し、我々はまた、障害の原因をユーザーやビジネス開発スタッフに伝える必要があり、その後、我々は、タスククラスの結果を定義しました。

public class TaskResult<R> {
	// 方法执行结果
	private final TaskResultType resultType;
	// 方法执行后的结果数据
	private final R returnValue;
	// 如果方法失败,这里可以填充原因
	private final String reason;

3.2、どのようにユーザーのビジネスメソッドを実行するには?

私たちは何をするか、我々はフレームワークの実装を配置する必要があり、ビジネスユーザーの多種多様のためのフレームワークですか?もちろん、インターフェイスの定義は、私たちのフレームワークのみこのメソッドを実装します、我々は我々の方法はまた、ジェネリックを使用する必要があるとして意味、理由はユーザデータトラフィックの多様性のため、当然のことながら、このインタフェースを実装する必要があり、ビジネス側のフレームワークを使用します。

public interface ITaskProcesser<T,R> {
	TaskResult<R> executeTask(T data);
}

3.3、ユーザーはどのように自分の仕事のスケジュールとクエリタスクを提出するには?

フロントエンドユーザーではバックグラウンドに仕事(JOB)を提出し、我々は、ビジネス開発者のためのカプセル化メカニズムを提供する必要があり、パッケージ機構にジョブに関する情報を提出することができ、ユーザーは、このパッケージのメカニズムという、時間の進行状況を確認する必要があります買収、完了したタスクの除去のための私達のパッケージの内部メカニズムも責任しばらく。
私たちは、クラスJOBINFO、ユーザの作業の抽象カプセル化を定義し、このカプセル化メカニズムでは、ジョブは区別するために、そのようなジョブ名としての仕事では、関連する情報を含む複数のサブタスク(TASK)、このJOBINFOを含めることができます唯一の仕事をフレームでなく、重複が早く仕事を見つけるために提出避けるために、ジョブ名、作業のタスクリストに加えて、作業プロセッサタスクがその中に定義され、また、見つけることは簡単です。

public class JobInfo<R> {
	//工作名,用以区分框架中唯一的工作
	private final String jobName;
	// 工作中任务的长度
	private final int jobSize;
	// 处理工作中任务的处理器
	private final ITaskProcesser<?,?> taskProcesser;
	// 任务的成功次数
	private AtomicInteger successCount;
	// 工作中任务目前已经处理的次数
	private AtomicInteger taskProcessCount;
	// 存放每个任务的处理结果,供查询用
	private LinkedBlockingDeque<TaskResult<R>> taskResultQueues;
	// 保留的工作的结果信息供查询的时长
	private final long expireTime;

同時に、このような作業の進捗状況を問い合わせるなどJOBINFOジョブについてのかなりの数の方法が、ありますが、各タスク処理のためのクエリの結果、記録処理、各タスクの結果など
ここに画像を挿入説明
、私たちが完了するまでにCheckJobProcesserクラスに与えられます完了したタスクをクリアする責任があり、ポーリングメカニズムのタイミングは、エレガントではありませんので、我々はこの機能を実装することにしましたDelayQueue

public class CheckJobProcesser {
	// 存放任务的队列
	private static DelayQueue<ItemVo<String>> queue = new DelayQueue<ItemVo<String>>();

そして、その中に明確完了したタスクや関連作業のRunnableスレッドの定義。

private static class FetchJob implements Runnable{
	private static DelayQueue<ItemVo<String>> queue = CheckJobProcesser.queue;
	// 缓存的工作信息
	private static Map<String, JobInfo<?>> jobInfoMap = PendingJobPool.getMap();
	@Override
	public void run() {
		while(true) {
			try {
				ItemVo<String> item = queue.take();
				String jobName = (String)item.getData();
				jobInfoMap.remove(jobName);
				System.out.println(jobName+" 过期了,从缓存中清除");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
}
	 /**
	 * 	初始化队列中到期的任务
	 */
	static {
		Thread thread = new Thread(new FetchJob());
		thread.setDaemon(true);
		thread.start();
		 System.out.println("开启过期检查的守护线程......");
	}

Principalクラス3.4フレームワーク

主なカテゴリは、ビジネスクラスの主要な開発者によって使用されPendingJobPool、です。このクラスは、このような、提出するようにタスク(TASK)、タスク(TASK)同時実行、問い合わせのクエリインターフェイスおよびタスクの進行状況の実装とを保存するための作業(JOB)とタスク(TASK)として、スケジューリングを担当しています。

public class PendingJobPool {
	// 框架运行时的线程数,与机器的CPU数相同
	private static final int THREAD_COUNTS = Runtime.getRuntime().availableProcessors();
	// 用于存放任务的队列,供线程池使用
	private static BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<Runnable>(5000);
	// 线程池,固定大小,有界队列
	private static ExecutorService taskExcutor = new ThreadPoolExecutor(
			THREAD_COUNTS, THREAD_COUNTS, 60, TimeUnit.SECONDS, taskQueue);
	// 工作信息的存储容器
	private static ConcurrentHashMap<String,JobInfo<?>> jobInfoMap = new ConcurrentHashMap<String,JobInfo<?>>();
//	/*检查过期工作的处理器*/
//	private static CheckJobProcesser checkJob = CheckJobProcesser.getInstance();

ここに画像を挿入説明

3.5フローチャート

ここに画像を挿入説明
テスト
コードパッケージcom.chj.thread.capt09、および統合されたスプリングと参照、TaskFrameworkモジュール。
もちろん、春の統合と、その後は、単一部品のいくつかの実施形態は必要ありません。

第三に、実際のパフォーマンスの最適化

1.プロジェクトの背景と問題点

这个项目来自为电信教育系统设计开发的一套自适应的考试学习系统,面向的用户主要是职业学院的的老师和学生以及短时间脱产学习的在职人员。什么叫自适应呢?就是当时提出一种教育理念,对学员的学习要求经常考试进行检查,学员的成绩出来以后,老师会要求系统根据每个学员的考卷上错误的题目从容量为10万左右的题库中抽取题目,为每个学员生成一套各自个性化的考后复习和练习的离线练习册。所以,每次考完试,特别是比较大型的考试后,要求生成的离线文档数量是比较多的,一个考试2000多人,就要求生成2000多份文档。当时我们在做这个项目的时候,因为时间紧,人员少,很快做出第一版就上线运营了。

当然,大家可以想到,问题是很多的,但是反应最大,用户最不满意的就是这个离线文档生成的功能,用户最不满意的点:离线文档生成的速度非常慢,慢到什么程度呢?一份离线文档的生成平均时长在50~55秒左右,遇到成绩不好的学生,文档内容多的,生成甚至需要3分钟,大家可以算一下,2000人,平均55秒,全部生成完,需要2000*55=110000秒,大约是30个小时。

为什么如此之慢?这跟离线文档的生成机制密切相关,对于每一个题目要从保存题库的数据库中找到需要的题目,单个题目的表现形式如图,数据库中存储则采用类html形式保存,对于每个题目而言,解析题目文本,找到需要下载的图片,每道题目都含有大量或大型的图片需要下载,等到文档中所有题目图片下载到本地完成后,整个文档才能继续进行处理。

2、分析和改进

第一版的实现,服务器在接收到老师的请求后,就会把批量生成请求分解为一个个单独的任务,然后串行的完成。
ここに画像を挿入説明
于是在第二版的实现上,首先我们做了个服务拆分,将生成离线文档的功能拆了出来成为了单独的服务,对外提供RPC接口,在WEB服务器接收到了老师们提出的批量生成离线文档的要求以后,将请求拆分后再一一调用离线文档生成RPC服务,这个RPC服务在实现的时候有一个缓冲的机制,会将收到的请求进行缓存,然后迅速返回一个结果给调用者,告诉调用者已经收到了请求,这样WEB服务器也可以很快的对用户的请求进行应答。

所以我们有了第一次改进,参见com.chj.thread.capt10.RpcServiceWebV1。

public class RpcServiceWebV1 {
	// 处理文档生成的线程池 IO密集型 故而大小设置为CPU核心数*2
	private static ExecutorService docMakeService = Executors.newFixedThreadPool(Consts.THREAD_COUNT*2);
	// 处理文档上传的线程池
	private static ExecutorService docUploadService = Executors.newFixedThreadPool(Consts.THREAD_COUNT*2);
	private static CompletionService<String> docCompletingServcie = new ExecutorCompletionService(docMakeService);
	private static CompletionService<String> docUploadCompletingServcie = new ExecutorCompletionService<String>(docUploadService);

  public static void main(String[] args) throws InterruptedException, ExecutionException {
	int docCount = 60;
	 System.out.println("题库开始初始化...........");
     SL_QuestionBank.initBank();
     System.out.println("题库初始化完成。");
     List<SrcDocVo> docList = CreatePendingDocs.makePendingDoc(docCount);
     long startTotal = System.currentTimeMillis();
     for(SrcDocVo doc : docList) {
    	 docCompletingServcie.submit(new MakeDocTask(doc));
     }
     for(int i=0; i<docCount; i++) {
    	Future<String> future  = docCompletingServcie.take();
    	docUploadCompletingServcie.submit(new UploadTask(future.get()));
     }
     // 展示时间
     for(int i=0; i<docCount; i++) {
    	 docUploadCompletingServcie.take().get();
     }
     System.out.println("共耗时:"+(System.currentTimeMillis()-startTotal)+"ms");
}

/**
 * 生成文档的工作任务
 */
private static class MakeDocTask implements Callable<String>{
	private SrcDocVo pendingDocVo;
	public MakeDocTask(SrcDocVo pendingDocVo) {
		this.pendingDocVo = pendingDocVo;
	}
	@Override
	public String call() throws Exception {
		long start = System.currentTimeMillis();
		// 普通生成方式
		String result1 = ProduceDocService.makeDoc(pendingDocVo);
		// 题目并行化方式
		String result = ProduceDocService.makeDocAsyn(pendingDocVo);
		System.out.println("文档"+result+"生成耗时:"+(System.currentTimeMillis()-start)+"ms");
        return result;
	}
}

/**
 * 上传文档的工作任务
 */
private static class UploadTask implements Callable<String>{
	private String fileName;
	public UploadTask(String fileName) {
		this.fileName = fileName;
	}
	@Override
	public String call() throws Exception {
		long start = System.currentTimeMillis();
		String result = ProduceDocService.upLoadDoc(fileName);
		System.out.println("已上传至[" + result + "]耗时:" + (System.currentTimeMillis() - start) + "ms");
		return result;
	}
}

}
我们看这个离线文档,每份文档的生成独立性是很高的,天生就适用于多线程并发进行。所以在RPC服务实现的时候,使用了生产者消费者模式,RPC接口的实现收到了一个调用方的请求时,会把请求打包放入一个容器,然后会有多个线程进行消费处理,也就是生成每个具体文档。

当文档生成后,再使用一次生产者消费者模式,投入另一个阻塞队列,由另外的一组线程负责进行上传。当上传成功完成后,由上传线程返回文档的下载地址,表示当前文档已经成功完成。
文档具体的下载地址则由WEB服务器单独去数据库或者缓存中去查询。
ここに画像を挿入説明
对于每个离线文档生成本身,我们来看看它的业务:

  • 1)从容量为10万左右的题库中为每个学生抽取适合他的题目。
  • 2)每道题目都含有大量的图片需要下载到本地,和文字部分一起渲染。

但是我们仔细考察整个系统的业务就会发现,我们是在一次考试后为学员生成自适应的练习册,换句话说,不管考试考察的内容如何,学生的成绩如何,每次考试的知识点是有限的,而从这些知识点中可以抽取的相关联的题目数也总是有限的,不同的学生之间所需要的题目会有很大的重复性。

举个例子我们为甲学生因为他考卷上的错误部分抽取了80个题目,有很大的概率其他学生跟甲学生错误的地方会有重复,相对应的题目也会有重复。对于这部分题目,我们是完全没有必要重复处理的,包括从数据库中重新获取题目、解析和下载图片。这也是我们可供优化的一大突破点。

其次,一篇练习册是由很多的题目组成的,每个题目相互之间是独立的,我们也可以完全并行的、异步的处理每个题目。
具体怎么做?要避免重复工作肯定是使用缓存机制,对已处理过的题目进行缓存。我们看看怎么使用缓存机制进行优化。这个业务,毋庸置疑,map肯定是最适合的,因为我们要根据题目的id来找题目的详情,用哪个map?我们现在是在多线程下使用,考虑的是并发安全的concurrentHashMap。

当我们的服务接收到处理一个题目的请求,首先会在缓存中get一次,没有找到,可以认为这是个新题目,准备向数据库请求题目数据并进行题目的解析,图片的下载。

这里有一个并发安全的点需要注意,因为是多线程的应用,会发生多个线程在处理多个文档时有同时进行处理相同题目的情况,这种情况下不做控制,一是会造成数据冲突和混乱,比如同时读写同一个磁盘文件,二是会造成计算资源的浪费,同时为了防止文档的生成阻塞在当前题目上,因此每个新题目的处理过程会包装成一个Callable投入一个线程池中 而把处理结果作为一个Future返回,等到线程在实际生成文档时再从Future中get出结果进行处理。因此在每个新题目实际处理前,还会检查当前是否有这个题目的处理任务正在进行。

如果题目在缓存中被找到,并不是直接引用就可以了,因为题库中的题目因为种种关系存在被修改的可能,比如存在错误,比如可能内容被替换,这个时候缓存中数据其实是失效过期的,所以需要先行检查一次。如何检查?

我们前面说过题库中的题目平均长度在800个字节左右,直接equals来检查题目正文是否变动过,明显效率比较低,所以我们这里又做了一番处理,什么处理?对题目正文事先做了一次SHA的摘要并保存在数据库,并且要求题库开发小组在处理题目数据入库的时候进行SHA摘要。

在本机缓存中同样保存了这个摘要信息,在比较题目是否变动过时,首先检查摘要是否一致,摘要一致说明题目不需要更新,摘要不一致时,才需要更新题目文本,将这个题目视为新题目,进入新题目的处理流程,这样的话就减少了数据的传输量,也降低了数据库的压力。

题目处理的流程就变为:
ここに画像を挿入説明
所以我们有了第二次改进,
1)在题目实体类QuestionInDBVo中增加一个

// 题目sha摘要
private final String sha;

2)增加一个题目保存在缓存中的实体类QuestionInCacheVo

public class QuestionInCacheVo {
	private final String questionDetail;
    private final String questionSha;

3)增加一个并发处理时返回的题目结果实体类TaskResultVo

public class TaskResultVo {
	private final String questionDetail;
	private final Future<QuestionInCacheVo> questionFuture;

按照我们前面的描述,我们可以得知,题目要么已经处理完成,要么正在处理,所以在获取题目结果时,先从questionDetail获取一次,获取为null,则从questionFuture获取。那么这个类的构造方法需要单独处理一下。

public TaskResultVo(String questionDetail) {
		this.questionDetail = questionDetail;
		this.questionFuture = null;
	}

2.1、在处理文档的服务的类ProduceDocService中增加一个处理文档的新方法makeDocAsyn

	public static String makeDocAsyn(SrcDocVo pendingDocVo) throws InterruptedException, ExecutionException {
		System.out.println("开始处理文档:"+ pendingDocVo.getDocName());
		// 每个题目的处理结果
		Map<Integer, TaskResultVo> questionResultMap = new HashMap<>();
		for(Integer questionId : pendingDocVo.getQuestionList()) {
			questionResultMap.put(questionId, ParalleQstService.makeQuestion(questionId));
		}
		StringBuffer sb = new StringBuffer();
		for(Integer questionId : pendingDocVo.getQuestionList()) {
			TaskResultVo taskResultVo = questionResultMap.get(questionId);
			sb.append(taskResultVo.getQuestionDetail() == null ? 
					taskResultVo.getQuestionFuture().get().getQuestionDetail() : taskResultVo.getQuestionDetail());
		}
		return "complete_"+System.currentTimeMillis()+"_"+ pendingDocVo.getDocName()+".pdf";
	}

在这个方法中,会调用一个并发处理题目的方法。
2.2、增加一个优化题目处理的类ParallelQstService,其中提供了并发处理题目的方法,还包括了主程序:

public class ParalleQstService {
	// 题目在本地的缓存
	private static ConcurrentHashMap<Integer,QuestionInCacheVo> questionCache = new ConcurrentHashMap<>();
	// 正在处理的题目的缓存
	private static ConcurrentHashMap<Integer,Future<QuestionInCacheVo>> processingQestionCache = new ConcurrentHashMap<>();
	// 处理题目的线程池
	private static ExecutorService makeQuestionExector = Executors.newCachedThreadPool();

	public static TaskResultVo makeQuestion(Integer questionId) {
		QuestionInCacheVo questionInCacheVo = questionCache.get(questionId);
		if(null == questionInCacheVo) {
			 System.out.println("题目["+questionId+"]不存在,准备启动");
			 return new TaskResultVo(getQuestionFuture(questionId));
		}else {
			String questionSha = SL_QuestionBank.getQuestionSha(questionId);
			if(questionInCacheVo.getQuestionSha().equals(questionSha)) {
				System.out.println("题目["+questionId+"]在缓存已存在,可以使用");
				return new TaskResultVo(questionInCacheVo.getQuestionDetail());
			}else {
				System.out.println("题目["+questionId+"]在缓存已过期,准备更新");
                return new TaskResultVo(getQuestionFuture(questionId));
			}
			
		}
	}
	/**
	 * 获取题目任务
	 */
	private static Future<QuestionInCacheVo> getQuestionFuture(Integer questionId) {
		Future<QuestionInCacheVo> questionFuture = processingQestionCache.get(questionId);
		try {
			if(null == questionFuture) {
				QuestionInDBVo questionInDBVo = SL_QuestionBank.getQuetion(questionId);
				//
				QuestionTask questionTask = new QuestionTask(questionInDBVo,questionId);
				//不靠谱的做法,无法避免两个线程对同一个题目进行处理
//                questionFuture = makeQuestionExecutor.submit(questionTask);
//                processingQuestionCache.putIfAbsent(questionId,questionFuture);
                // 如果直接改成
//                processingQuestionCache.putIfAbsent(questionId,questionFuture);
//                questionFuture = makeQuestionExecutor.submit(questionTask);
                // 也不行,因为ConcurrentHashMap的value是不允许为null的,那么就需要另做处理
				FutureTask<QuestionInCacheVo> ftask = new FutureTask<>(questionTask);
				questionFuture = processingQestionCache.putIfAbsent(questionId, ftask);
				if(null == questionFuture) {
					//当前线程成功了占位了
					questionFuture = ftask;
					makeQuestionExector.execute(ftask);
					System.out.println("当前任务已启动,请等待完成后");
				}else {
					System.out.println("有其他线程开启了题目的计算任务,本任务无需开启");
				}
			}else {
				System.out.println("当前已经有了题目的计算任务,不必重复开启");
			}
			return questionFuture;
		}catch(Exception e) {
			processingQestionCache.remove(questionId);
			e.printStackTrace();
	        throw e;
		}
	}
	
	/**
	 * 解析题目的任务类,调用最基础的题目生成服务即可
	 */
	private static class QuestionTask implements Callable<QuestionInCacheVo> {
		QuestionInDBVo questionDBVo;
        Integer questionId;
        public QuestionTask(QuestionInDBVo questionDBVo, Integer questionId) {
            this.questionDBVo = questionDBVo;
            this.questionId = questionId;
        }
		@Override
		public QuestionInCacheVo call() throws Exception {
			try {
				String questionDetail = QstService.makeQuestion(questionId, questionDBVo.getDetail());
				String questionSha = questionDBVo.getSha();
				QuestionInCacheVo questionInCacheVo = new QuestionInCacheVo(questionDetail,questionSha);
				return questionInCacheVo;
			}finally {
				//无论正常还是异常,均要将生成题目的任务从缓存中移除
				processingQestionCache.remove(questionId);
			}
		}
	}
}

3、继续改进

3.1 数据结构的改进

但是我们仔细分析就会发现,作为一个长期运行的服务,如果我们使用concurrentHashMap,意味着随着时间的推进,缓存对内存的占用会不断的增长。最极端的情况,十万个题目全部被加载到内存,这种情况下会占据多少内存呢?我们做了统计,题库中题目的平均长度在800个字节左右,十万个题目大约会使用75M左右的空间。

看起来还好,但是有几点,第一,我们除了题目本身还会有其他的一些附属信息需要缓存,比如题目图片在本地磁盘的存储位置等等,那就说,实际缓存的数据内容会远远超过800个字节。第二,map类型的的内存使用效率是比较低的,以hashmap为例,内存利用率一般只有20%到40%左右,而concurrentHashMap只会更低,有时候只有hashmap的十分之一到4分之一,这也就是说十万个题目放在concurrentHashMap中会实际占据几百兆的内存空间,是很容易造成内存溢出的,也就是大家常见的OOM。

考虑到这种情况,我们需要一种数据结构有map的方便但同时可以限制内存的占用大小或者可以根据需要按照某种策略刷新缓存。最后,在实际的工作中,我们选择了ConcurrentLinkedHashMap,这是由Google开源一个线程安全的hashmap,它本身是对ConcurrentHashMap的封装,可以限定最大容量,并实现一个了基于LRU也就是最近最少使用算法策略的进行更新的缓存。很完美的契合了我们的要求,对于已经缓冲的题目,越少使用的就可以认为这个题目离当前考试考察的章节越远,被再次选中的概率就越小,在容量已满,需要腾出空间给新缓冲的题目时,越少使用就会优先被清除。

3.2 线程数的设置

原来我们设置的线程数按照我们通用的IO密集型任务,两个线程池设置的都是机器的CPU核心数2,但是这个就是最佳的吗?不一定,通过反复试验我们发现,处理文档的线程池线程数设置为CPU核心数4,继续提高线程数并不能带来性能上的提升。而因为我们改进后处理文档的时间和上传文档的时间基本在1:4到1:3的样子,所以处理文档的线程池线程数设置为CPU核心数43。

这时我们有了第三次改进:

public class RpcServiceWebV2 {
	// 处理文档生成的线程池 IO密集型 故而大小设置为CPU核心数*2
	private static ExecutorService docMakeService = Executors.newFixedThreadPool(Consts.THREAD_COUNT*4);
	// 处理文档上传的线程池
	private static ExecutorService docUploadService = Executors.newFixedThreadPool(Consts.THREAD_COUNT*4*3);
	private static CompletionService<String> docCompletingServcie = new ExecutorCompletionService(docMakeService);
	private static CompletionService<String> docUploadCompletingServcie = new ExecutorCompletionService<String>(docUploadService);

3.3 缓存的改进

我々はまた、ローカルファイルストレージを使用するローカルメモリキャッシュに加えて、二次キャッシュ機構が有効になっています。なぜローカルファイルストレージを使用できますか?サーバーのアップグレード、ダウンタイムが失われるメモリデータにキャッシュされていることを考える。これを回避するために、我々は、関連するデータはなかったでしょう永続的なローカルで動作し、ローカルディスクに保存されています。

4、改善の効果

1)WEBオリジナルの単一のシリアルプロセス、加工3つの文書は、
ここに画像を挿入説明
文書が51秒かかる意味します。
2)サービス、文書生成並列化として、60個の文書がかかった
ここに画像を挿入説明
文書の平均は3.5秒かかり、すでにシングルWEB実現のシリアルバージョンが桁違いに向上しているよりも高くなっています。
3)キャッシュに重複を避けるために、非同期並列処理の対象は、文書60がかかり
ここに画像を挿入説明
再び桁違いに改善して文書の平均は、0.65秒かかります。
4)スレッドの数を調整した後、60加工文書は
ここに画像を挿入説明
平均文書が再び3回の速度、および私たちの相対的な性能の最初のバージョン、処理された文書51 / 0.23の平均性能を高めるために0.23秒かかり=並行プログラミングの力を有効に利用することで221回、!

  • 改善されたユーザー体験:あなたはまた、先に私たちと、組み合わせたフロントの進行状況を表示し、ユーザーがより良い経験を与えると言う考えの実用的な枠組みの同時タスクを実行することができます。
  • 含意:最適化プロジェクトは、彼にインスピレーションを得たものを私たちにもたらしましたか?
    パフォーマンスの最適化は、このようなパフォーマンスの最適化で私たちのエントリポイントとして、事業の徹底的な分析に基づいている必要があり、キャッシュデータ構造の選択は、ビジネスの深い理解に基づいて構築されています。
    パフォーマンスの最適化は、我々はこれらの特性やメカニズムを使用するからこそ、ただ質的な飛躍があるアプリケーションのパフォーマンスで私たちに聞かせて、パフォーマンス、非同期タスクおよびその他のメカニズムを最適化するために、高い同時実行の言語の良い使用、より一層の活用のキャッシュを作るために、各種の導入一方、このようなキャッシュされたデータとして、不安やボトルネックを回避するために新たな注目をもたらすメカニズムは問題、同時スレッドの安全性の問題、我々は克服し、解決するために必要なすべてを満了します。

おすすめ

転載: blog.csdn.net/m0_37661458/article/details/90707291