プロジェクト: エコーフォーラムシステムプロジェクト

1. ログインおよび登録モジュール

1. 登録機能

1.1. 登録フローチャート

 

1.2. 登録コード
/**
     * 用户注册
     * @param user
     * @return Map<String, Object> 返回错误提示消息,如果返回的 map 为空,则说明注册成功
     */
    public Map<String, Object> register(User user) {
        Map<String, Object> map = new HashMap<>();

        if (user == null) {
            throw new IllegalArgumentException("参数不能为空");
        }
        if (StringUtils.isBlank(user.getUsername())) {
            map.put("usernameMsg", "账号不能为空");
            return map;
        }

        if (StringUtils.isBlank(user.getPassword())) {
            map.put("passwordMsg", "密码不能为空");
            return map;
        }

        if (StringUtils.isBlank(user.getEmail())) {
            map.put("emailMsg", "邮箱不能为空");
            return map;
        }

        // 验证账号是否已存在
        User u = userMapper.selectByName(user.getUsername());
        if (u != null) {
            map.put("usernameMsg", "该账号已存在");
            return map;
        }

        // 验证邮箱是否已存在
        u = userMapper.selectByEmail(user.getEmail());
        if (u != null) {
            map.put("emailMsg", "该邮箱已被注册");
            return map;
        }

        // 注册用户
        user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); // salt
        user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt())); // 加盐加密
        user.setType(0); // 默认普通用户
        user.setStatus(0); // 默认未激活
        user.setActivationCode(CommunityUtil.generateUUID()); // 激活码
        // 随机头像(用户登录后可以自行修改)
        user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
        user.setCreateTime(new Date()); // 注册时间
        userMapper.insertUser(user);

        // 给注册用户发送激活邮件
        Context context = new Context();
        context.setVariable("email", user.getEmail());
        // http://localhost:8080/echo/activation/用户id/激活码
        String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
        context.setVariable("url", url);
        String content = templateEngine.process("/mail/activation", context);
        mailClient.sendMail(user.getEmail(),"激活 Echo 账号", content);

        return map;
    }

1.3. パフォーマンスの最適化

スレッド プールを使用して電子メールを非同期に送信します。電子メールを送信する必要がある場合、スレッド プール内のスレッドが電子メールを非同期に送信できるようにします。メソッドに @Async アノテーションを追加し、スタートアップに @EnableAsync アノテーションを追加するだけです。クラス。

 

2. ログインモジュール

2.1. ログインページ

2.2. ログイン認証コードの問題

        まず、ログイン時に認証コードがランダムに生成されます。この認証コードと現在のユーザーを照合して認証コードを確認するにはどうすればよいでしょうか?

        明らかに、この時点ではユーザーはログインしていないため、ユーザーの ID を介して確認コードに一意に対応する方法はありません。したがって、現時点では、このユーザーを一時的に置き換えるランダム ID を生成し、その ID と対応する確認コードをRedis (60s) に一時的に保存することを検討します。そして、このユーザーに対して生成されたランダムな ID をCookie に一時的に保存します(60 秒)。

        検証コードの生成と検証は、それぞれ 2 つの URL リクエスト アドレスです。

        このようにして、ユーザーがログイン ボタンをクリックすると、ランダムな ID と検証コードが Redis から取得され、対応する検証コードが Cookie でクエリされ、ユーザーが入力した検証コードが一致しているかどうかが判断されます。

2.3. ログイン認証とユーザーステータスの問題

        ユーザーがユーザー名とパスワードを入力し、検証コードを検証すると、ログインが成功します。では、1 回のリクエストでユーザーのステータスを保存するにはどうすればよいでしょうか? ユーザーの情報をエコーするにはどうすればよいですか?

アプローチとしては、以下に示すようにクラスを設計することができます。

        説明すると、各ユーザーが正常にログインした後、ランダムで一意のログイン資格情報エンティティ クラス オブジェクト LoginTicket(ユーザー ID、ログイン資格情報文字列チケット、有効かどうか、および有効期限を含む) を生成します。このログイン資格情報エンティティ クラスをオブジェクトはRedis に永続的に保存され (キーはログイン認証情報文字列チケット、値はLoginTicketクラス)、認証情報クラスの認証情報チケットは Cookie に保存されます。いわゆる無効なログイン認証情報とは、ユーザーがアクセスを要求せず、認証情報クラスの有効期限が更新されないことを意味します。再度アクセスが要求されると、ローカル時間と認証情報クラスの時間が比較されて、アクセスが要求されているかどうかが判断されます。期限切れ。

        保存 LoginTicket 後、それに基づいてユーザーのステータスを取得できます。各リクエストの前に Cookie からチケットを取得し、そのチケットに基づいて Redis に移動して、  ユーザーのログイン資格情報が期限切れで有効であるかどうかを確認するインターセプター を定義しました。リクエストはLoginTicketInterceptorログイン資格情報が有効で有効な場合にのみ実行されます。有効期限が切れていない場合は、ログイン インターフェイスにジャンプします。LoginTicket

        ユーザーのログイン資格情報が有効で、有効期限が切れていない場合、このリクエストでユーザーの情報を保持できます。どうやって持ちますか?ThreadLocal ここでは、ユーザー情報を保存し、各スレッドでユーザー情報のコピーを作成することを考えます ThreadLocal 。つまり、各スレッドは独自の内部ユーザー情報コピー変数にアクセスできるため、スレッドの分離が実現されます。次のクラスを見てみましょう HostHolder 。

        したがって、ログイン成功後に保存される情報は次のとおりです。

        生成されたバウチャーを Redis に保存し、有効期限を設定し、状態を 1 に設定します。キーはバウチャー、値はLoginTicket クラスです。

       次に、各リクエストはまずインターセプターを通過し、Cookie を通じてチケット資格情報を取得します。チケット資格情報を使用して Redis からLoginTicket クラス情報を取得します。存在する場合、LoginTicket ユーザー情報はクラスのユーザー ID を介して照会され、ThreadLocal に保存されます。存在しない場合はインターセプトされます。

 

2.4. 終了機能

        認証情報に基づいて Redis から情報を削除しLoginTicket 、ThreadLocal の Remove() メソッドを実行してユーザー情報をクリアします。

2.5. パフォーマンスの最適化

        各リクエストはインターセプターの Cookie を介して認証情報を取得し、次に Redis に移動してLoginTicket クラスを取得する必要があるためです。検証に合格すると、毎回データベース内のユーザー情報が照会されるため、アクセス要求ごとにデータベースに大きなアクセス圧力がかかることになります。

        この状況を回避するために、インターセプターはまず Redis にアクセスしてユーザー情報をクエリし、存在する場合は直接 ThreadLocal に保存します。存在しない場合はデータベースにアクセスしてユーザー情報をクエリし、それから Redis に保存します。 。

2.6. プロセス
  • ユーザーログイン -> ログイン認証情報を生成し、キーとして Redis に保存します。値は認証情報です。チケット認証情報は Cookie に保存されます。
  • 各リクエストが実行される前に、インターセプターは Cookie を通じて Redis にクエリを実行し、ユーザーのログイン資格情報の有効期限が切れていて有効であるかどうかを確認します。「ログイン資格情報の有効期限を延長するには、「ログイン情報を記憶する」をクリックします。ユーザーがログアウトすると、ログイン資格情報は無効になります。
  • ログイン資格情報に対応するユーザー ID に従って、データベース内のユーザー情報を照会します。
  • ThreadLocal を使用して、このリクエスト全体にわたってこのユーザー情報を保持します。
  • 最適化ポイント: 各リクエストの前に、データベースにアクセスしてユーザー情報をクエリする必要があります。アクセス頻度が比較的高いため、ログインに成功したユーザー情報を Redis にしばらく保存することを検討します。インターセプタは、リクエストの前にまず Redis にクエリを実行します。クエリ。
 2.7. フローチャート

2.ポストモジュール

1.投稿を公開する

ここでは、フロントエンドのリッチ テキスト コンパイラ機能を使用して、ユーザーがテキスト関数だけでなく画像やビデオをアップロードできるようにします。

写真のアップロード、写真の削除、および写真のダウンロードには、Alibaba Cloud OSS の機能を使用します。

ビデオのアップロード、削除、およびビデオの再生には、Alibaba Cloud ビデオ オン デマンド機能を使用します。

上の図のように、投稿には分類モジュールがあり、投稿を公開するときに分類モジュールを選択できます。

図1に示すように、 

2. エンティティクラス

        ここには、ユーザーのID、記事内容、画像URL、動画再生URL、テキストタイプ、投稿いいね統計、コメント統計、ステータス、作成時刻、修正時刻などが保存されます。

3.エフェクト表示

        MybatisPlus ページング + Alibaba Cloud OSS 画像機能 + Alibaba Cloud ビデオオンデマンド機能を利用して、最終的なコンテンツを直感的に表示します。

4.人気投稿ランキング機能

フォーラムのトップページに入力されたコンテンツは人気順にランク付けされ、人気の高い投稿の上位 10 件をページ内に表示する機能が必要な場合。

1 つ目は次のようなエンティティ クラスです。

ポストテーブル

似た形

この投稿に対する「いいね!」の数の統計的考え方:

スケジュールされたタスクを公開し、投稿の ID を介して Like テーブルを定期的にクエリして、データベース内の投稿テーブルに Like の数を保存します。

すべての投稿をいいね数順に逆順に並べ替えてページに出力します。

このようにして、人気のある投稿の情報を取得することができます。

5. パフォーマンスの最適化

Redis+Canal+MySQL binlog でキャッシュの一貫性を実現

上の図に示すように、投稿へのアクセス数が比較的多いため、人気のある投稿をキャッシュするために redis が使用されますが、投稿にはデータベースとキャッシュの不整合の問題が発生するため、ここではキャッシュの整合性を実現するために redis+MySQL binlog が使用されます。

  • まずmysqlでbinlogを有効にします。
  • 次に、Linux 上に canal ミドルウェアをデプロイし、設定ファイルで IP アドレス、ポート番号、mysql のユーザー名とパスワードを設定し、canal サービスを開始します。
  • 次に、Springboot に canal を統合し、canal-spring-boot-starter を使用して統合します。
  • リスナーを作成し、EntryHandler インターフェイスを実装し、内部の追加、削除、および変更メソッドを書き換えます。データベースが変更を送信したら、MySQL バイナリ ログを監視してキャッシュを変更できます。

このプロジェクトでは、データベースのバイナリを canal 経由で監視しており、変更のための投稿が送信されるたびに、mysql のバイナリが canal 経由で監視され、redis 内のデータが更新されます。

グアバ ブルーム フィルター メカニズムを使用してキャッシュ侵入問題を解決する

guava を Java プロジェクトに導入し、構成クラスを設定し、誤検知率 (通常は 0.05) を設定します。次に、ブルーム フィルターにデータを入力するためのリクエスト インターフェイスを特別に設定します。入力されたデータには、現在の投稿のすべてのデータが入力されます。

以降のリクエストアクセスでは、ブルームフィルタでデータが見つかった場合は直接データを返しますが、見つからない場合はredisからデータを検索します。

3. コメントモジュール

1. コメントを投稿する

投稿の下にあるコメントを直接クリックすると、そのコメントが投稿コメントであれば、データベース内で投稿コメントとしてマークされ、一意のコメント識別子が生成され、投稿の ID に関連付けられます。

2. ユーザーがコメントに返信する

投稿の返信機能をクリックすると、そのコメントはユーザーのコメントであり、データベース内でユーザーのコメントとしてマークされ、一意のコメント識別子が生成されてユーザーの ID に関連付けられます。

コメントに対してユーザーが返信する機能もある。この関数は上記と同じで、フロントエンドで現在のコメント ID と保存されたユーザー ID に基づいてソートし、ページング関数を使用して時間でソートするだけです。

3. エンティティクラス

4. プライベートメッセージモジュール

1.エフェクト表示

2. 詳細な手順

2.1. プライベートメッセージリスト

        現在チャット中のユーザー数を表示したり、他のユーザーに関する情報や既読・未読などの情報を一覧表示する機能です。

        ユーザーがどのユーザーと直接チャットしたかを確認するには、conversation_id フィールドで直接照合します。このフィールドを保存すると、2 つのユーザー ID の文字列が区切り文字「_」で区切られるため、文字列インターセプトを使用して 2 つのユーザー ID を取得します。

        統計の繰り返しを避けるために、ここでは Set を使用して重複を削除し、conversation_id の命名規則は辞書順になっており、左側が小さいもの、右側が大きいものになっています。たとえば、102 と 101 の間の会話は次のように保存されます。 「101_102」の形式。このようにして、あいまい一致に基づいてデータベースからすべてのデータを取得し、Set コレクションに保存します。その後、内部のデータを取得し、文字列インターセプトを実行して 2 つのユーザー ID を取得できます。会話ユーザーのIDとIDを取得できます。

各セッションは、送信者 ID、受信者 ID、セッション ステータス、セッション表現、およびセッション時間を保存します。

2.2. 詳細ダイアログ

        エンティティクラスには form_id と to_id があり、この 2 つの id がそれぞれ送信側の form_id と受信側の to_id に対応します。conversation_idを取得すると、このconversation_idを元に双方の会話情報を取得し、時間順に表示することができます。

        たとえば、2 つのユーザー ID は 101 と 102 です。現在のユーザー 101 がメッセージを送信する場合、送信者は自分自身、受信者は 102 となり、その逆も同様です。このようにして、現在のユーザー ID に応じて時間順にソートされた会話メッセージを取得し、ページング機能で表示することができます。

2.3. メッセージの送信

        メッセージ送信時は、現在のログインユーザーIDと相手のセッションユーザーIDを辞書ソートに従って会話テーブルのconversation_idに保存し、送信者IDと受信者IDを保存し、ステータスステータスはデフォルトで0未読に設定されます。送信したメッセージの内容と送信時刻。        

2.4. 未読メッセージ数の統計

        まず、ログイン ユーザー ID に従って、conversation_id を使用して、あいまい一致を通じてユーザーのセッションに関連するデータをクエリします。取得したデータの中から、to_idがログインユーザーID、status=0のデータを条件クエリすることで、該当するデータを数量統計用に取得できます。

3. エンティティクラス

  • id: プライベートメッセージ/システム通知の一意の識別子
  • from_id: 送信者ID
  • to_id: 受信者ID
  • 会話 ID: 2 人のユーザー間の会話を識別します。たとえば、ユーザー ID 112 が 113 にメッセージを送信する、または 113 が 112 にメッセージを送信する、どちらも conservation_id 会話 です112_113このようにして、このフィールドを通じて 112 と 113 の間のプライベート メッセージを見つけることができます。もちろん、このフィールドは冗長であり、from_id と to_id を通じて推測できますが、このフィールドがあると、後続のクエリやその他の操作が容易になります。
  • content: プライベートメッセージ/システム通知の内容
  • status: 受信側通知のステータス
    • 0 - 未読 (デフォルト)
    • 1 - 読み取り
  • create_time: プライベートメッセージ/システム通知の送信時間

5. いいねモジュール

1. Redis のキーと値のペアの設計

  • key: (文字列) ターゲット ID と同様のユーザー ID を結合し、区切り文字は _
  • 値: (HashMap) ステータス (0: いいね 1: キャンセル) と更新時刻を格納するタイムスタンプ

key=ターゲットID_類似ユーザーID

値={時間: 長い}

ユーザーと投稿の両方がこのように設計されています。

2.「いいね!」の数を数える

この利点の 1 つは、投稿の「いいね!」の数やユーザーのコメントの「いいね!」の数をカウントする場合、redis を使用してターゲット ID をあいまい一致させるだけで「いいね!」の数を把握できることです。

Set Keys = redisTemplate.keys("目标id_" + "*");
int サイズ = key.size();

3. 気に入っているかどうかを判断する 

投稿またはコメントがユーザーに「いいね!」されたかどうかを確認したい場合は、キー <ターゲット ID、ユーザー ID> をクエリしてその値を取得し、「いいね!」されたかどうかを知るためのコンテンツがあるかどうかを判断するだけで済みます。

Set Keys = redisTemplate.keys(RedisUtils.setKey(target id, user id));
if(keys.size()==0){ System.out.println("Not Liked"); }else{ System.out . println("いいね"); }



4.いいね

key<ターゲット ID、ユーザー ID>、値<タイムスタンプ> に従って REID に保存します。

redisTemplate.opsForSet().add(RedisUtils.setKey(ターゲット ID, ユーザー ID),RedisUtils.setValue());

スケジュールされたタスクを設計し、一定期間後に mysql に保存し、統計テーブルで「いいね!」の合計数をカウントします。

5.「いいね」をキャンセルする

キー <ターゲット ID、ユーザー ID> を直接削除すると、いいねがキャンセルされ、いいねされたターゲットの総数が 1 つ減少します。

Boolean delete = redisTemplate.delete(RedisUtils.setKey("ターゲット ID", "ユーザー ID"));
if (delete){ System.out.println("キャンセル成功"); }else{ System.out.println("キャンセルに失敗しました"); }



6.いいねした人が獲得したいいねの総数

「いいね!」の合計数は、投稿に対する「いいね!」の合計数 + コメントまたはコメントの合計数で構成されます。

mysqlエンティティクラスのいいねの総数を確認する

これは RedisUtils エンティティ クラスです

public class RedisUtils {
    /**
     * 点赞设置key
     * @param Id1 目标id
     * @param Id2 点赞用户id
     * @return
     */
    public static String setKey(String Id1,String Id2){
        return Id1+"_"+Id2;
    }

    /**
     * 点赞设置value
     * 设置点赞时间戳
     * @return
     */
    public static Map<String,Long> setValue(){
        Map<String,Long> map=new HashMap<>();
        Instant instant = LocalDateTime.now().toInstant(ZoneOffset.ofHours(8));
        long millisecond = instant.toEpochMilli();
        map.put("time",millisecond);
        return map;
    }
}

7. エンティティクラス

7.1. 類似統計表

7.2. 類似情報テーブル

6. システム通知モジュール

1。概要

システム通知は非常に一般的な必要な要件であり、「いいね!」、フォロー、またはコメント操作が発生すると、システムは対応するユーザーに通知を送信します。

トラフィックが膨大なソーシャル ネットワーキング サイトでは、システム通知の需要が非常に大きく、プライベート メッセージングや投稿機能などの非同期機能を実行するために Ajax を使用するだけでは明らかに十分ではありません。したがって、システムのパフォーマンスを確保するには、メッセージ キュー (メッセージ キューの 3 つの主要な機能: デカップリング、非同期、ピーク除去) を使用することが非常に必要であり、Echo では Kafka が使用されます。

全体として、要件はシステム通知の送信とシステム通知の表示の 2 つだけです。

1.1. システム通知を送信します。
  • A は B に「いいね!」を与え、B にいいねタイプのシステム通知を送信します ( TOPIC_LIKE)
  • A が B を好きになり、B にフォロー型システム通知を送信します ( TOPIC_FOLLOW)
  • A が B を気に入って、B にコメント型システム通知を送信します ( TOPIC_COMMNET)

全体的なロジックは、同様の操作が発生すると、メッセージ キューの同様のイベントがトリガーされ、コンシューマーがこのイベントを消費するというものです。具体的な消費ロジックは、システム通知テーブルにデータを挿入することです (システム通知も同様)messageただし、システム通知はコード内で 1 としてハードコードされており 、 from_id システムによって送信されたことを示しています。そのため、次の場合にユーザー テーブルに id = 1 のユーザーを格納することに誰もが注意を払う必要があるのはこのためです。デプロイ中) 。

1.2. システム通知を表示します。
  • システム通知一覧(いいね、コメント、フォローの3種類の通知を表示)
  • システム通知の詳細(ページ内の特定の種類に含まれるシステム通知を表示)
  • 未読メッセージの数を表示する

2. イベントオブジェクトをカプセル化する

コンシューマがこのメッセージを消費することによってデータベース テーブル メッセージにレコードを挿入したい場合、このメッセージまたはイベントにはメッセージ テーブル内のすべてのフィールドが含まれている必要があります。そうでない場合、これらのフィールドはメッセージから派生できます。

さらに、MQ は 1 対多のパブリッシュ/サブスクライブ モデルであり、メッセージはトピックごとに分類され、プロデューサはメッセージをトピックにパブリッシュし、コンシューマはトピックをサブスクライブできます。like イベントを例として、以下の図を参照してください。

送信者が「いいね!」、フォロー、またはコメント操作の場合、受信者は応答するユーザーです。

効果はステーション b と同様です

いいね、コメント、フォローするとデータが送信され、受信したデータは分類、識別されます。

コード:

/**
     * 消费评论、点赞、关注事件
     * @param record
     */
    @KafkaListener(topics = {TOPIC_COMMNET, TOPIC_LIKE, TOPIC_FOLLOW})
    public void handleMessage(ConsumerRecord record) {
        if (record == null || record.value() == null) {
            logger.error("消息的内容为空");
            return ;
        }
        Event event = JSONObject.parseObject(record.value().toString(), Event.class);
        if (event == null) {
            logger.error("消息格式错误");
            return ;
        }

        // 发送系统通知
        Message message = new Message();
        message.setFromId(SYSTEM_USER_ID);
        message.setToId(event.getEntityUserId());
        message.setConversationId(event.getTopic());
        message.setCreateTime(new Date());

        Map<String, Object> content = new HashMap<>();
        content.put("userId", event.getUserId());
        content.put("entityType", event.getEntityType());
        content.put("entityId", event.getEntityId());
        if (!event.getData().isEmpty()) { // 存储 Event 中的 Data
            for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
                content.put(entry.getKey(), entry.getValue());
            }
        }
        message.setContent(JSONObject.toJSONString(content));

        messageService.addMessage(message);

    }

7. プロジェクトの困難

1. ブルームフィルターを使用して Redis のキャッシュ侵入問題を解決する

ここで使用されているのは、グアバを使用してブルーム フィルターを実装することです。

まず、guavaからデータが存在するかどうかを判断し、データが返ってきた場合はフィルタにデータが存在することを意味し、データが返されない場合はブルームフィルタにデータが存在しないことを意味し、その後、guavaからデータを取得します。レディス。

おすすめ

転載: blog.csdn.net/weixin_55127182/article/details/131998298