春のパラレルデータのベストプラクティスを凝集 - DIは時に会ったパラレル

分析淘宝網PDP

私たちは、地図、淘宝網のPDP(製品の詳細ページ)のページを見てみましょう。

クロームネットワーク]パネルを開き、ページのデータをロード淘宝どのように見てみましょう。経験によると、それは一般的に非同期でロードされ、いずれかXHR、またはあるJS(JSONP)、あなたはすぐに見つけることができるはずです

あなたはまだ、このインターフェイスのパフォーマンスを見ることができます

驚くべきことに、淘宝網は、実際には単一の要求で全体のPDPの完全なデータページをプルダウン、およびサーバ・プロセスが少ない125msのよりも時間がかかります

まず第一に、何が良いですか?

  • フロントおよびリア側の開発シンプルなドッキング
  • コネクション確立要求ヘッダ廃棄物の流れの数を減らすことが可能な限り単一のネットワーク接続のデータの送信(データサイズユーザーエクスペリエンスに影響を与えてはならない、一般的には300キロバイト以下)。

その後、これはそれを行う方法ですか?

あなたがキャッシュを言うかもしれないが、あなたが知っている、そのような電気の供給が非常に重要なページで、絶対のようなチームの多くが含まれます:

  • 製品チーム
  • 売り手チーム
  • 評価チーム
  • チームオーダー
  • チームのメンバー
  • グループのための特別料金
  • Q&Aチーム
  • 推奨チーム
  • 物流システム
  • などの/ etc

各チームは、すべてのキャッシュされたデータである、あなたが1を得るでしょうにもかかわらず、簡単に125msの内に終了を取得していない。そして、お金関連のページと同じように、いくつかのデータが有効リアルタイム性を保証しなければならない、ではない多くの場所をキャッシュすることができます。実行する方法を、あなたの場合、あなたがどうなるか?オフラインマーキング?データ予熱?など。

この時点で、それはパラレル・コールの良い方法です。

あなたがいることがわかります、このページを分析し取得するために並列に完全に依存し、各モジュールのデータの間で、実際には、(同じ上院に)加えて、各モジュールが同じ商品に所属し、そしてません。

パラレル問題はありませんか?

データへの並列アクセス、インタフェースは、次のような問題の数を私たちのパフォーマンスを向上させるだけでなく、導入することができます:

  • アイテムは、コードを簡単に作成し、クリアする方法を、多くを依存してもよいですか?
  • 依存関係は、各ノードを並列に実行することができます理解する必要がない場合は、有向グラフである可能性が高いですか?
  • 非同期処理の後、どのように残業に対処する?ビジネスコードはどのように対処するスロー?
  • 無限ループ依存性がある場合はどのように行うには?
  • 誘導後、に対処する方法でのThreadLocalの内容は?どのように仕事のThreadLocalベースのコンテキストの一部を行うことを達成していませんか?
  • スレッド分離トランザクションはどのように行うことですか?
  • すべての非同期実行、各ノードのパフォーマンスを監視するには?

非同期の問題を解決する方法を、以下では、簡単な\使用\効率的な並列データの取得方法について説明します。

一般的なパラレル

あなたは今、3つのコピーモディは、どのような方法あなたが並列にそれを得ることができ、データのユーザー\ブログリスト\ファンリストに関する基本的な情報が必要な場合は?

パラレルJavaのスレッドプール

最も初歩的な方法は、Javaを直接使用することは、スレッドプールと今後のメカニズムを提供します。

public User getUserDataByParallel(Long userId) throws InterruptedException, ExecutionException {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    CountDownLatch countDownLatch = new CountDownLatch(3);
    Future<User> userFuture = executorService.submit(() -> {
        try{
            return userService.get(userId);
        }finally {
            countDownLatch.countDown();
        }
    });
    Future<List<Post>> postsFuture = executorService.submit(() -> {
        try{
            return postService.getPosts(userId);
        }finally {
            countDownLatch.countDown();
        }
    });
    Future<List<User>> followersFuture = executorService.submit(() -> {
        try{
            return followService.getFollowers(userId);
        }finally {
            countDownLatch.countDown();
        }
    });
    countDownLatch.await();
    User user = userFuture.get();
    user.setFollowers(followersFuture.get());
    user.setPosts(postsFuture.get());
    return user;
}
复制代码

Springの非同期並列

私たちが知っている、春のサポート@Asyncノートは、あなたが簡単に非同期実装することができ、支持体は、参照の戻り値:.キャプチャWww.baeldung.com/spring-asynを...

@Async豆の原則の実際の実現はインターセプトメソッド呼び出し、プロキシクラスのプロセスであり、実際には彼らの書かれたJavaのThreadPoolに原則的にのTaskExecutorビーンに非常に異なる呼び出し可能なタスクを提出します。

春の非同期は、次の3つの方法の戻り値を変更する。上記の機能を実現するためにまず必要性を使用して、戻り値の型を変更し、メソッドに注釈を追加@Async

class UserServiceImpl implements UserService {
    @Async
    public Future<User> get(Long userId) {
        // ... something
    }
}
class PostServiceImpl implements PostService {
    @Async
    public Future<List<Post> getPosts(Long userId) {
        // ... something
    }
}
class FollowServiceImpl implements FollowService {
    @Async
    public Future<List<User> getFollowers(Long userId) {
        // ... something
    }
}
复制代码

次のようにユーザデータの3部パラレル取得は、次いで、重合されます。

public User getUserDataByParallel(Long userId) throws InterruptedException, ExecutionException {
    Future<User> userFuture = userService.get(userId);
    Future<List<Post>> postsFuture = postService.getPosts(userId);
    Future<List<User>> followersFuture = followService.getFollowers(userId);
    
    User user = whileGet(userFuture);
    user.setFollowers(whileGet(followersFuture));
    user.setPosts(whileGet(postsFuture));
    return user;
}

private <T> T whileGet(Future<T> future) throws ExecutionException, InterruptedException {
    while(true) {
        if (future.isDone()) {
            break;
        }
    }
    return future.get();
}
复制代码

ここではスピンを使用して非同期データを取得することができます。もちろん、あなたも前のように、ロックアウトを渡して(たCountDownLatch)サービスは、呼び出し元のスレッドが上記のロックアウトで待機するようにしましょう、行くことができます。

並列結合DI(DI)

上記の二つの方法が機能して達成することができましたが、最初、彼らは直感的ではなく、タイムアウトが\異常\にThreadLocalを発生すると、前述した非同期の問題に対処していない、コードは仕事、あなたが期待していないないかもしれないと。何があることそれを行うには、より簡単で便利で信頼性の高い方法はありますか?

必要なデータは、あなたがあなたに渡され、上院に自動並列化の方法を得ることができれば、それはこのように非常に便利ではない、というような方法を想像してみ?:

@Component
public class UserAggregate {
    @DataProvider("userWithPosts")
    public User userWithPosts(
            @DataConsumer("user") User user,
            @DataConsumer("posts") List<Post> posts,
            @DataConsumer("followers") List<User> followers) {
        user.setPosts(posts);
        user.setFollowers(followers);
        return user;
    }
}
复制代码

ここです@DataConsumer。あなたは、非同期データIDを取得したい文の@DataProviderこのメソッドは、データを提供して宣言し、そしてIDがuserWithPostsです。

それともあなたは、このような集計クラスを記述する必要はありません、あなたは再利用する必要はありません、あなたは直接「匿名プロバイダ」を作成したい。そうすれば、直接どこでも結果を得るために、このように呼び出すことができます

User user = dataBeanAggregateQueryFacade.get(
     Collections.singletonMap("userId", 1L), 
     new Function3<User, List<Post>,List<User>, User>() {
            @Override
            public User apply(@DataConsumer("user") User user, 
                              @DataConsumer("posts") List<Post> posts,
                              @DataConsumer("followers") List<User> followers) {
                user.setPosts(posts);
                user.setFollowers(followers);
                return user;
            }
     });
Assert.notNull(user,"user not null");
Assert.notNull(user.getPosts(),"user posts not null");
复制代码

あなたの場合はここで関数3は、4つの一般的なパラメータを受信、ユーザーが最初の3つのパラメータが連続法の3つのタイプのパラメータを適用するために対応し、最後の戻り値の型を表す。定義済みのプロジェクト機能2-機能5、せいぜい5つのパラメータをサポートしていませんより多くのパラメータについては、インターフェイス(FunctionInterface)、インタフェースが継承することができますMultipleArgumentsFunctionを書くことができます。

明らかに

  • それぞれが@DataConsumer一つだけに対応します@DataProvider
  • Aは@DataProvider以上であってもよい@DataConsumer消費。
  • A @DataProvider複数の@DataConsumerに依存する複数の@DataProvider

さて、そのようなプロジェクトは、上記の機能を実現するために、そこにある。ただし、いくつかのコメントを追加するには、あなたのように。あなたはすぐに並列にあなたのコールツリーを作ることができます。

プロジェクト住所:github.com/lvyahui8/sp ...

あなたはどのように基本的な実装を気にしないでください。あなたが唯一のいくつかの機能を拡張するために、いくつかの設定パラメータの世話をする必要性をカスタマイズした場合にのみ。

原則

  1. 開始時のスプリング、スキャンアプリ@DataProvider@DataConsumer注釈。依存先を記録した(非有向接続グラフ)、および良好な記録@DataProviderスプリング豆との間のマッピング関係。
  2. ポスト噴射も依存関係の依存関係ツリーを記録し、再帰的に非同期Beanメソッドに対応する子ノードを呼び出して、スレッドプール(CountLatchDown)がロックされているから参照クエリは、現在のノード(約幅の結果を得るとき優先順位が、平行の、シーケンシャルアクセスノード)が不明であるためです。
  3. クエリパラメータを格納するために使用されるマップ、に渡し、再帰呼び出しを開始する前に、この方法がされていない@DataConsumer上院に注釈を付け、彼はこのマップから値を取得します。
  4. @DataProviderそして@DataConsumer注釈は冪等キャッシュするかどうかというように、\タイムアウト\例外処理モードを制御するために使用されるパラメータのいくつかをサポートすることができます。

パラレル/非同期の導入後、新たな問題を解決する方法

タイムアウトは、方法を制御するには?

@DataProviderアノテーションのサポートtimeoutパラメータはタイムアウトを制御するために使用する。原則は法を待って、タイムアウトブロックすることによってです。

java.util.concurrent.CountDownLatch#await(long, java.util.concurrent.TimeUnit)
复制代码

異常どのように対処するには?

飲み込ままたは上位層に投げ:異常は、2つの可能な方法を提供しています。

@DataConsumerアノテーションのサポートexceptionProcessingMethodパラメータは、消費者がプロバイダー投げたいに対処する方法を示すために使用されます。

もちろん、また、グローバルな次元に配置されたサポートされています。グローバル優先順位は、(<)消費者の優先順位の設定よりも低いです。

依存無限ループがどのように行うには?

あなたは、円形の依存関係の問題を解決する、いわゆる「への早期の参照」を使用できるように、二段階を作成するための豆と豆プロパティの割り当てポイントため、春豆の初期化、。

しかし、あなたの場合、サイクル依存豆、コンストラクタのパラメータの定義への依存は、その後、循環依存の問題を解決する方法はありません。

同様に、我々は、メソッドのパラメータを介して入力し、データが非同期に変化なしの場合の参照メソッドに、依存関係を注入し、それは死のサイクルの終わりではなく、したがって、円形の依存関係を禁止しなければなりません。

問題はその後、循環における2つの方法で非連結グラフに依存して検出する方法を、どのように依存のサイクルまたは禁止次のようになります。

  • DFSは、ステンドトラバースで:スタックアクセスノードの前に、最初のノードの状態はその後、再帰が完了すると、子ノードを降り、「訪問」とマークされた「完全なアクセス」と表示されたノード** DFS再帰処理する場合。もう一度「訪問」ノードへのアクセス、リングを示す。**
  • トポロジカルソート:グラフのノードに並ぶが、配列を有し、低いインデックス番号にノードnodeポインティングの高いインデックス番号が存在しない、それが0度を削除するには、図トポロジカルソート方法が実施されるトポロジカルソートを表します。程度のノードにノード、および将軍- 1、すべてのノードが削除されるまでリング内の有向グラフがある場合、明らかに、次に度ノードでリングは、その後、ノードは、従って完了削除しないことができない0。限り、ノードは、次数0のノードに&&完全に存在しない削除されないように、次いで環が存在しなければなりません。

ここでは、チェックリングを達成するために、検索を染色するリードテーブル+ DFSを取ります

private void checkCycle(Map<String,Set<String>> graphAdjMap) {
    Map<String,Integer> visitStatusMap = new HashMap<>(graphAdjMap.size() * 2);
    for (Map.Entry<String, Set<String>> item : graphAdjMap.entrySet()) {
        if (visitStatusMap.containsKey(item.getKey())) {
            continue;
        }
        dfs(graphAdjMap,visitStatusMap,item.getKey());
    }
}

private void dfs(Map<String,Set<String>> graphAdjMap,Map<String,Integer> visitStatusMap, String node) {
    if (visitStatusMap.containsKey(node)) {
        if(visitStatusMap.get(node) == 1) {
            List<String> relatedNodes = new ArrayList<>();
            for (Map.Entry<String,Integer> item : visitStatusMap.entrySet()) {
                if (item.getValue() == 1) {
                    relatedNodes.add(item.getKey());
                }
            }
            throw new IllegalStateException("There are loops in the dependency graph. Related nodes:" + StringUtils.join(relatedNodes));
        }
        return ;
    }
    visitStatusMap.put(node,1);
    log.info("visited:{}", node);
    for (String relateNode : graphAdjMap.get(node)) {
        dfs(graphAdjMap,visitStatusMap,relateNode);
    }
    visitStatusMap.put(node,2);
}
复制代码

ThreadLocalのどのように対処するには?

フレームワークの多くは、単一のリクエストでいくつかの共有データを保存するためにコンテキストを実装するためにThreadLocalを使用し、春も例外ではありません。

我々はすべて知っているように、ThreadLocalのは、地図をスレッド特別なエントリへのアクセスは、実際にある。ThreadLocalを現在のスレッドが唯一のアクセスデータ(コピー)は、交差スレッドならば、他のThreadLocalMapのデータを取得することはできません。

ソリューション

フィギュア

  1. 現在のスレッドの非同期タスクを提出する前に、実行にThreadLocalの現在のスレッドのデータは、タスクインスタンスに「縛られ」
  2. タスクが開始されたときにThreadLocalは、データが非同期スレッドのには、タスクインスタンスから現在のリターンを抽出しました
  3. タスクの終了後、ThreadLocalの現在の非同期スレッドを清掃します。

ここでは、まずこれらの3つのアクションを記述するためのインタフェースを定義します

public interface AsyncQueryTaskWrapper {
    /**
     * 任务提交之前执行. 此方法在提交任务的那个线程中执行
     */
    void beforeSubmit();

    /**
     * 任务开始执行前执行. 此方法在异步线程中执行
     * @param taskFrom 提交任务的那个线程
     */
    void beforeExecute(Thread taskFrom);

    /**
     * 任务执行结束后执行. 此方法在异步线程中执行
     * 注意, 不管用户的方法抛出何种异常, 此方法都会执行.
     * @param taskFrom 提交任务的那个线程
     */
    void afterExecute(Thread taskFrom);
}
复制代码

私たちがアクションを定義する3つの作品を作るために。我々はjava.util.concurrent.Callable#呼び出し方法を書き換える必要があります。

public abstract class AsyncQueryTask<T> implements Callable<T> {
    Thread      taskFromThread;
    AsyncQueryTaskWrapper asyncQueryTaskWrapper;

    public AsyncQueryTask(Thread taskFromThread, AsyncQueryTaskWrapper asyncQueryTaskWrapper) {
        this.taskFromThread = taskFromThread;
        this.asyncQueryTaskWrapper = asyncQueryTaskWrapper;
    }

    @Override
    public T call() throws Exception {
        try {
            if(asyncQueryTaskWrapper != null) {
                asyncQueryTaskWrapper.beforeExecute(taskFromThread);
            }
            return execute();
        } finally {
            if (asyncQueryTaskWrapper != null) {
                asyncQueryTaskWrapper.afterExecute(taskFromThread);
            }
        }
    }

    /**
     * 提交任务时, 业务方实现这个替代方法
     *
     * @return
     * @throws Exception
     */
    public abstract T  execute() throws Exception;
}
复制代码

あなたがスレッドプールにタスクを送信すると、その後、もはや直接呼び出し可能匿名クラスのインスタンスを提出していないが、AsyncQueryTaskインスタンスを提出した。そして、提出する前にトリガ taskWrapper.beforeSubmit();

AsyncQueryTaskWrapper taskWrapper = new CustomAsyncQueryTaskWrapper();
// 任务提交前执行动作.
taskWrapper.beforeSubmit();
Future<?> future = executorService.submit(new AsyncQueryTask<Object>(Thread.currentThread(),taskWrapper) {
    @Override
    public Object execute() throws Exception {
        try {
            // something to do
        } finally {
            stopDownLatch.countDown();
        }
    }
});
复制代码
何をやっていますか?

あなただけのクラスを実装し、このインタフェースを定義する必要があり、クラスコンフィギュレーションファイルを追加します。

@Slf4j
public class CustomAsyncQueryTaskWrapper implements AsyncQueryTaskWrapper {
    /**
     * "捆绑" 在任务实例中的数据
     */
    private Long tenantId;
    private User user;

    @Override
    public void beforeSubmit() {
        /* 提交任务前, 先从当前线程拷贝出ThreadLocal中的数据到任务中 */
        log.info("asyncTask beforeSubmit. threadName: {}",Thread.currentThread().getName());
        this.tenantId = RequestContext.getTenantId();
        this.user = ExampleAppContext.getUser();
    }

    @Override
    public void beforeExecute(Thread taskFrom) {
        /* 任务提交后, 执行前, 在异步线程中用数据恢复ThreadLocal(Context) */
        log.info("asyncTask beforeExecute. threadName: {}, taskFrom: {}",Thread.currentThread().getName(),taskFrom.getName());
        RequestContext.setTenantId(tenantId);
        ExampleAppContext.setLoggedUser(user);
    }

    @Override
    public void afterExecute(Thread taskFrom) {
        /* 任务执行完成后, 清理异步线程中的ThreadLocal(Context) */
        log.info("asyncTask afterExecute. threadName: {}, taskFrom: {}",Thread.currentThread().getName(),taskFrom.getName());
        RequestContext.removeTenantId();
        ExampleAppContext.remove();
    }
}
复制代码

追加の構成はTaskWapperテイク効果を可能にします。

io.github.lvyahui8.spring.task-wrapper-class=io.github.lvyahui8.spring.example.wrapper.CustomAsyncQueryTaskWrapper
复制代码

どのように非同期呼び出しごとに時間を監視するには?

ソリューション

我々は、次のライフサイクルにクエリを置きます

  • クエリタスクの初期提出(querySubmitted)
  • (queryBefore)の前にあるノードは、プロバイダを開始しました
  • プロバイダ1回のノードの実行が完了すると(queryAfter)
  • クエリが完了(queryFinished)
  • 異常なクエリ(exceptionHandle)

次のインターフェイスに変換

public interface AggregateQueryInterceptor {
    /**
     * 查询正常提交, Context已经创建
     *
     * @param aggregationContext 查询上下文
     * @return 返回为true才继续执行
     */
    boolean querySubmitted(AggregationContext aggregationContext) ;

    /**
     * 每个Provider方法执行前, 将调用此方法. 存在并发调用
     *
     * @param aggregationContext 查询上下文
     * @param provideDefinition 将被执行的Provider
     */
    void queryBefore(AggregationContext aggregationContext, DataProvideDefinition provideDefinition);

    /**
     * 每个Provider方法执行成功之后, 调用此方法. 存在并发调用
     *
     * @param aggregationContext 查询上下文
     * @param provideDefinition 被执行的Provider
     * @param result 查询结果
     * @return 返回结果, 如不修改不, 请直接返回参数中的result
     */
    Object queryAfter(AggregationContext aggregationContext, DataProvideDefinition provideDefinition, Object result);

    /**
     * 每个Provider执行时, 如果抛出异常, 将调用此方法. 存在并发调用
     *
     * @param aggregationContext  查询上下文
     * @param provideDefinition 被执行的Provider
     * @param e Provider抛出的异常
     */
    void exceptionHandle(AggregationContext aggregationContext, DataProvideDefinition provideDefinition, Exception e);

    /**
     * 一次查询全部完成.
     *
     * @param aggregationContext 查询上下文
     */
    void queryFinished(AggregationContext aggregationContext);
}
复制代码

開始するSpringアプリケーションの開始時に、全て実装するBean AggregateQueryInterceptorインターフェイスを取得し、注文注釈の並べ替えに基づいて、インターセプタチェーンとして。

インターセプタは、あなたが再帰クエリタスクを送信します。非常にシンプルに、実行方法については、いくつかの挿入フック(フック)関数を実行するコード関与の。Aロットすることができ、ここに掲載しませ、興味はgithubのクローンにコードビューを行くことができます。

何をやっていますか?

次のようにあなたは、インターセプタ、インターセプタのログ出力、実行状態の監視ノード(加工、アクセスパラメータ)を実装することができます:

@Component
@Order(2)
@Slf4j
public class SampleAggregateQueryInterceptor implements AggregateQueryInterceptor {
    @Override
    public boolean querySubmitted(AggregationContext aggregationContext) {
        log.info("begin query. root:{}",aggregationContext.getRootProvideDefinition().getMethod().getName());
        return true;
    }

    @Override
    public void queryBefore(AggregationContext aggregationContext, DataProvideDefinition provideDefinition) {
        log.info("query before. provider:{}",provideDefinition.getMethod().getName());
    }

    @Override
    public Object queryAfter(AggregationContext aggregationContext, DataProvideDefinition provideDefinition, Object result) {
        log.info("query after. provider:{},result:{}",provideDefinition.getMethod().getName(),result.toString());
        return result;
    }

    @Override
    public void exceptionHandle(AggregationContext aggregationContext, DataProvideDefinition provideDefinition, Exception e) {
        log.error(e.getMessage());
    }

    @Override
    public void queryFinished(AggregationContext aggregationContext) {
        log.info("query finish. root: {}",aggregationContext.getRootProvideDefinition().getMethod().getName());
    }
}
复制代码

プロジェクトアドレス

最後に、再びについて投稿:プロジェクトの住所... github.com/lvyahui8/sp

Paizhuan歓迎、歓迎スター、ようこそ

おすすめ

転載: juejin.im/post/5e131d276fb9a047ec702d66