springboot+MongoDB+マルチスレッド読み取り統計

背景の概要

リーダーは私に要件を言いました。「フロントエンドの顧客ページで記事の閲覧数やその他のアクセス情報をカウントする必要がある」というものでした。このあたりは今まで触れたことがなかったので、アクセス情報の収集について調査を開始しました。モジュールの開発が完了した際にも、この機会にモジュールのレビューを行ってください。この記事内のすべてのコード情報は復号化されています。

テクノロジーの選択

バックエンド データベース: Mysql+MongoDB
フロントエンド バックエンド: springboot–Filter
時間指定タスク: @Scheduled
ここで、システムがフロントエンド (ユーザー アクセス エンド) とバックエンド (グループ管理およびメンテナンス エンド) に分かれていることに注意してください。

モンゴDB

MySQL については言うまでもありませんが、MongoDB を使用する理由についてお話します;
1. 初期段階では Mysql ストレージを使用することを選択しましたが、後でアクセス数が増加し、データ エントリの数が増加することに気づきました。 、単一データがカバーする情報が増加し、占有容量の増加により Mysql データへのアクセス速度が低下する;
2. アクセス ログ データはテキスト形式で記録される半構造化データであり、MongoDB に適しています

基本構造

主な構成要素: ドキュメント (ドキュメント) コレクション (コレクション) データベース (データベース)
ドキュメント: データベース内のレコードの行に相当 コレクション
:複数のドキュメントで構成され、データベース内のテーブルに相当
データベース: 複数のコレクションで構成され、論理的に組織をまとめるとデータベースになります。

フィルター フィルター

参考記事

使用の意味

WEB 開発者は、フィルター テクノロジを使用して、JSP、サーブレット、静的画像ファイル、静的 HTML ファイルなど、Web サーバーによって管理されるすべての Web リソースをインターセプトし、いくつかの特別な機能を実現します。たとえば、URL レベルの権限制御、機密ワードのフィルタリング、応答情報の圧縮などの高度な機能が実装されています。

原理 - チェーンの形態

フィルターを使用すると、フィルターはブラウザーのリクエストをフィルター処理します。フィルターは、1.
リリース前のコード、2. リリース、3. リリース後のコードの3 つの部分に動的に分割できます。

public interface Filter {
    
    
	// 执行过滤器的初始化工作
    default void init(FilterConfig filterConfig) throws ServletException {
    
    
    }
 	// 当请求和响应被过滤器拦截后,都会交给doFilter来处理:其中两个参数分别是被拦截request和response对象,可以使用chain的doFliter方法来放行。
    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
 	// 释放关闭Filter对象打开的资源,在web项目关闭时,由web容器自动调用该方法。
    default void destroy() {
    
    
    }
}

上記の 3 つの部分に対応して、フィルターのライフ サイクルは、初期化、傍受とフィルタリング、および破棄の 3 つの段階に分割できます。

初期化フェーズ: サーバーが起動すると、サーバー (Tomcat) は構成ファイルを読み取り、注釈をスキャンして、フィルターを作成します。
インターセプトおよびフィルタリング ステージ: リクエストされたリソースのパスがインターセプトされたパスと同じである限り、フィルターはリクエストをフィルター処理し、このステージはサーバーの実行中に循環し続けます。
破棄フェーズ: サーバー (Tomcat) がシャットダウンされると、サーバーによって作成されたフィルターも破棄されます。

スケジュールされたタスク

Spring 3.0以降はスケジュールタスクが付属し、スケジュールタスク機能を実現するための @EnableScheduling アノテーションと @Scheduled アノテーションが提供される。

非同期/スレッド プール (ThreadPoolTask​​Executor)

実装

フィルタを定義する

1. Filterを使用したい場合はFilterクラスを継承するメソッドを記述する必要がある
2. 観光客ユーザー(未登録)の扱い

@Component
@WebFilter(filterName = "MyFilter", urlPatterns = "/*")
public class MyFilter implements Filter {
    
    

	// 这里的ALLOWED_PATHS 字面上是放行路径,实际上是后文不需要进行访问统计分析的路径
    private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(
        Arrays.asList(
                "/js",
                "/elementui",
                "/images",
                "/doc",
                "/getImage",
                "/chart",
                "/video"
        )));

    private Logger log = LoggerFactory.getLogger(this.getClass());

        @Autowired
        @Lazy
        MongoBasicService mongoBasicService;

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    
            // do something 处理request 或response

            // doFilter()方法中的servletRequest参数的类型是ServletRequest,需要转换为HttpServletRequest类型方便调用某些方法

            try {
    
    
                HttpServletRequest request = (HttpServletRequest) servletRequest;
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");

                boolean allowedPath = false;

                for (String prefix : ALLOWED_PATHS) {
    
    
                    if (path.startsWith(prefix + "/") || path.startsWith(prefix)) {
    
    
                        allowedPath = true;
                        break;
                    }
                }
                
            	// 这里的allowedPath实际上是设置了一些不需要进行过滤器处理的资源
                if (!allowedPath) {
    
    
                    String ip = request.getRemoteAddr();
                    String url = request.getRequestURL().toString();
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

                    Date d = new Date();
                    String date = sdf.format(d);

                	// 定义JSON对象-后续存至MongoDB-这里是获取信息的关键
                    JSONObject j = new JSONObject();
                    j.put("ip", ip);
                    j.put("url", url);
                    j.put("time", date);
                    j.put("timeStamp", d.getTime());

                    HttpSession session = request.getSession();

                    if (session != null) {
    
    
                        //考虑游客情况
                        if (session.getAttribute("id") != null) {
    
    
                            j.put("userId", session.getAttribute("id"));
                        } else {
    
    
                            j.put("userId", 0);
                        }

                        if (session.getAttribute("name") != null) {
    
    
                            j.put("name", session.getAttribute("name"));
                        }
                        if (session.getAttribute("wechatName") != null) {
    
    
                            j.put("wechatName", session.getAttribute("wechatName"));
                        }
                    }

                        j.put("userAgent", UserAgentUtil.parse(request.getHeader("User-Agent")));

                    }

                    System.out.println(date + " -- " + j.toString());
  
                    log.info("#filter# -- " + j.toString());
                    // 存放至MongoDB
                    this.mongoBasicService.insert(j, Constant.COLLECTION_VISIT_LOG);
                    }

                        filterChain.doFilter(request, response);
                    } catch (Exception e) {
    
    
                        e.printStackTrace();
                        log.warn(e.toString());
                    }

                    }
                    }

ユーザーログインコントローラを定義する

上記のフィルターで取得されたパラメーターを見ると、セッション内のユーザー属性はどこから来たのか、少し混乱するかもしれません。これは実際にユーザーがプラットフォームにログインしたときに取得するものですが、ここでは属性の取得をuserControllerに入れて、取得した属性をセッションに格納します。
例:

//微信id
       session.setAttribute("wid", account.getwId());
//首次通过微信登录需要绑定手机号码
       session.setAttribute("account", account);
  • セッションとクッキーの違い

MongoDB に保存

mongoDB の使用については、次の章を参照してください。
ここでは、mongoTemplate を使用して MongoDB を挿入します。ここでは、データの保存に JSON 形式が使用されています。ここでは非同期が使用されていることに注意してください。

 @Async
    public void insert(JSONObject j, String collectionName){
    
    
        try{
    
    
            this.mongoTemplate.insert(j,collectionName);
        }catch(Exception e){
    
    
            e.printStackTrace();
            log.warn(e.toString());
        }
    }

これはMongoDBに保存されるデータスタイルです

  • JAVAの非同期操作について

ここまででトップページのユーザー情報取得機能の開発は完了しましたが、以下からバックグラウンド解析・処理プログラムの開発に着手します。

モンゴDB

これらのダウンロードとインストールは比較的簡単です。Baidu に直接アクセスすることをお勧めします。
ここでは、データベース管理ツールを使用して MongoDB に接続する方法を説明する必要があります。個人的には Navicat
Navicat のダウンロードが好きです

基本的な使い方

記事「MongoDB ユーザー パスワード ログイン」
を参照してください。 注: 初めて使用する前に環境変数を設定する必要があります。 1. mongod --port 66667 --dbpath D:/MonogoDB4/data/db
を使用してログインします。 2. 指定された新しいユーザーを作成します。

---> 执行命令
mongo --port 66667

use admindb.createUser(
... {
    
    
...  user: "user",
...  pwd: "user123456",
...  roles: [ {
    
     role: "userAdminAnyDatabase", db: "admin" } ]
... }
... )
--->  返回打印
Successfully added user: {
    
    
        "user" : "qiao",
        "roles" : [
                {
    
    
                        "role" : "userAdminAnyDatabase",
                        "db" : "admin"
                }
        ]
}

MongoDBの基本操作

ここに画像の説明を挿入
ここで注意してください: DB データベースにテーブルがない場合、show dbs では表示されません。
記事を参照してください。

MongoDB から生のアクセス レコードを取得する


上記では、フォアグラウンド システムでアクセス データを取得して MongoDB に保存しました。バックグラウンドで元のデータを分析します。ここで注意すべき点がいくつかあります: 1. MongoDB の条件付きクエリ 2. なぜ制限を行う

か一度に読みすぎるデータによる詰まりを防ぐページング処理;
3.全データの読み込み方法:do whileループで判定

	do{
			//查询mongo数据
            Query query = new Query();
            // 模糊查询
            //如果url使用模糊查询则 创建正则
            query.addCriteria(
                    new Criteria().and("url").regex("/msg/")
            );

            query.addCriteria(
                    new Criteria().andOperator(
                            Criteria.where("time").gt(lastDate)
                    )
            );
            query.limit(20000);
            visitLogs = mongoTemplate.find(query, VisitLog.class);
    } while (visitLogs.size() != 0);

統計ロジック

読み取りボリューム統計の主なロジック:
MongoDB の各生データを分析し、Mysql データ テーブルを挿入/更新します。
データテーブルにすでにアクセスレコードがある場合は元のレコードのカウント数が累積され、
データテーブルにレコードがない場合、つまり処理対象のレコード(たとえば、データテーブルに記事が記録されていない場合)にはカウントされます。 ) の場合、新しい対応するレコードを作成する必要があり、カウント count は 1 です。
特定のコードは実際には特定のビジネス シナリオと大きく関係しているため、ここには投稿しません。

非同期処理

これは処理速度を向上させるためのもので、この部分については後で時間をかけて検討し、整理する予定です。

非同期を実現する 2 つの方法

1. 非同期タスクを実行する @Async アノテーションを使用するには、非同期関数を手動で有効にする必要があります。有効にする方法は @EnableAsync を追加することです。 2. ThreadPoolTask​​Executor は手動で実装されます

Async アノテーションの詳細な説明

https://juejin.cn/post/6858854987280809997
原則 : AOP + アノテーション 1 に基づく
、通知の具体的な実装は次のとおりです。
● 最初のステップは、非同期タスクの実行に使用される非同期タスク スレッド プールAsyncTaskExecutor
を取得することです● Callable を使用して対象メソッドをラップする
● 非同期タスクを実行し、戻り値の型に応じて対応処理
2. BeanPostProcessorの後処理により、カットポイントを満たすBeanのプロキシを生成

スレッド プールの使用法と構成 (ここでは IO 集中型)

デフォルトのスレッド プールのキュー サイズとスレッドの最大数は両方とも Integer の最大値であることがわかり、これは
明らかにシステムに特定のリスクと隠れた危険を残します
500~1000 、0.1 秒の応答時間を想定
: システムで許容される最大応答時間 (1 秒を想定)

1. corePoolSize = 1 秒あたりに必要なスレッド数は何ですか?
I/O 集中型のコア スレッドの数 = CPU コアの数 / (1-ブロッキング係数)。ブロッキング係数の範囲は 0 ~ 1 です。通常は 0.8 ~ 0.9 の間ですが、ここでは 50 を採用します。
スレッド数 = タスク/(1/タスクコスト) = タスク*タスクアウト = (500 ~ 1000)*0.1 = 50 ~ 100 スレッドです。
corePoolSize 設定は 50 より大きくする必要があります。
2. maxPoolSize スレッドの最大数は、多くの場合、実稼働環境の corePoolSize と同じに設定されます。これにより、処理中にスレッドを作成するオーバーヘッドを軽減できます。
3. queueCapacity = (coreSizePool/taskcost)応答時間の
計算では、queueCapacity = 80/0.1 1 = 800を取得できます。これは、キュー内のスレッドが 1 秒間待機できることを意味し、それを超える場合は、実行するために新しいスレッドを開く必要があります。あまり大きく設定しないと、応答時間が急激に増加します。

executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略 -如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

コアスレッド数の計算方法

参考記事
最適なスレッド数 = ((スレッド待ち時間 + スレッド CPU 時間) / スレッド CPU 時間) * CPU 番号
a. プログラムの各スレッドの平均 CPU 実行時間が 0.5 秒で、スレッド待ち時間 ( CPU 以外の実行時間 (IO など) は 1.5 秒、CPU コアの数は 8 ですが、最適なスレッド数はどれくらいでしょうか?
上記の式によれば、最適なスレッド数は ((0.5+1.5)/0.5)*8=32 と推定されます。

b. リクエストで計算操作に 5 ミリ秒かかり、DB 操作に 100 ミリ秒かかる場合、8 個の CPU を備えたサーバーの場合、合計時間は 100+5=105 ミリ秒となり、計算操作に使用されるのは 5 ミリ秒のみとなり、CPU 使用率は次のようになります。 5/(100+5)。スレッド プールを使用する目的は、CPU 使用率を最大化し、CPU リソースの無駄を減らすことです。CPU 使用率が 100% であると仮定すると、CPU 使用率 100% を達成するには、いくつのスレッドを設定する必要がありますか?
((5+100)/5)*8=168 スレッド。

ThreadPoolTask​​Executor を使用して数十万のデータをバッチで挿入する

データをバッチで挿入する場合、シングルスレッド環境では非常に時間がかかります

Spring コンテナはスレッド プール Bean オブジェクトを注入します

参考記事 - Springbootがバッチでデータを挿入する

@Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
    {
    
    
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(maxPoolSize);
        executor.setCorePoolSize(corePoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 线程池对拒绝任务(无线程可用)的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
非同期操作インターフェースを作成する
public interface IUserDataService 
{
    
    

/**
* 异步批量插入用户注册信息
* @param userData 用户信息
* @return
*/
public void asyncinsertUserDataList(UserData userData, CountDownLatch countDownLatch);
}
非同期スレッドのビジネスクラス
public class UserDataServiceImpl implements IUserDataService 
{
    
    

/**
 * 异步插入用户注册信息
 * @param userData 用户信息
 * @param countDownLatch
 */
@Override
@Async("threadPoolTaskExecutor")
public void asyncinsertUserDataList(UserData userData, CountDownLatch countDownLatch)
{
    
    
    try  {
    
    
        log.info("start executeAsync");
        userDataMapper.insertUserData(userData);
        log.info("end executeAsync");
    } catch(Exception e){
    
    
        e.printStackTrace();
    } finally {
    
    
        // 无论上面程序是否异常必须执行 countDown,否则 await 无法释放
        countDownLatch.countDown();
    }
}

}

時限タスク

フロントホワイは定期的に処理する必要がある

処理対象のデータを「バッチ」に蓄積し、指定した時刻に一度に処理します。これをバッチ処理と呼びます。バッチの実行とも呼ばれます。
拡張:
1. バッチ ビジネスを実行する特徴: 処理量が大きい (バッチ)、特定のトリガー タイミング (指定された時点)、自動処理 (手動介入は不要) 2.
銀行の場合、すべてのデータがリアルタイムで操作されるわけではありません。これらの大規模なビジネス、給与支払いの一元化、カード開設の一元化など、このためにバッチの実行が生まれました。

一般的なタイミング タスクの実装方法

参考記事
1、Javaにはタイマーが付属している
2、ScheduledExecutorService
3、Javaタスク(この記事で使用、つまりアクセスデータを定期的に処理するために使用)

コード

スケジュールされたタスク クラスを作成して、スケジュールされたタスクを必要とするビジネスを管理する @Scheduled


import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
 
/**
 * 基于注解设定多线程定时任务
 * @author pan_junbiao
 */
@Component
@EnableScheduling   // 1.开启定时任务
@EnableAsync        // 2.开启多线程
public class MultithreadScheduleTask
{
    
    
    @Async
    @Scheduled(fixedDelay = 1000)  //间隔1秒
    public void first() throws InterruptedException {
    
    
    // TODO 这里可以写需要定时处理的任务,如日志读取、访问数据分析
        System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
        System.out.println();
        Thread.sleep(1000 * 5);
    }
 
    @Async
    @Scheduled(fixedDelay = 2000)
    public void second() {
    
    
        // TODO 这里可以写需要定时处理的任务,如日志读取、访问数据分析
        System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
        System.out.println();
    }
}

以上が springboot+MongoDB を使用して読み取り量をカウントするプロセスの全体ですが、全体のプロセスは少し荒いので、Xiaobai さん、アドバイスをお願いします。許可なく転載することは禁止されています。

おすすめ

転載: blog.csdn.net/weixin_54594861/article/details/130025276