SpringBootとRedisを使用した個人ブログの構築(3)

13466 ワード

前の2つの文章はRedis sentinel環境の構築とSpringBoot+redis環境の構築について話して、本章は業務コードの開発に入ります.
Redisデータ構造の設計
まず,このシステムで用いられるRedisのデータ構造と命令を紹介し,redisのデータ構造を熟知することが最も基本的な要求であると言える.見終わったら、みんながredisを使って文字列だけを使わないでほしい.それは森を放棄したのと同じだ.
  • HashはjavaのHashMapと同様に、リレーショナル・データベースの1行に類似した複数の属性を1つのkeyで格納できます.例えば、タイトル、内容、作者、発表時間などを含むブログです.ブログidをkey、valueをhashで属性を格納できます.Redisはまた、Hash内の単一属性に対する操作コマンドを提供し、例えば、ユーザが文章を開くときに、文章の読解数属性に1を加えることができる.
  • ListはjavaのArrayListに類似しており、データの順序は挿入の順序である.redisは,Listの左右両端からのデータアクセスをサポートし,取得時にListが空であればブロック待ちをサポートするので,分散メッセージキューとして利用できる.例えば、ユーザが発行したブログは、作成順にリストにIDを入れてブログリストを照会することができる.
  • Setは、JavaのHashSetと同様に、データが無秩序に格納され、ランダムに取得しやすい.redisはSetに対して多くの集合を求める交差差命令を提供した.
  • SortedSet、整列集合.コレクション内の各要素には値のほかに、すべてのコレクション内の要素がscoreで配列された要素のscoreも格納されます.redisは、シーケンスループ、逆シーケンスループ、下付きスケール範囲とスコア範囲に基づいてサブセットを取得するなどのコマンドを提供します.例えば、ユーザーのブログは、公開時間をscoreとし、秩序ある集合を入れることができます.
  • INCRコマンドは、数値タイプに対してvalue原子を1ずつ加算し、keyが存在しない場合はデフォルトでkeyを新規作成し、1に割り当てます.自己増加id生成を実現するのに非常に適している.これに対応して、DECRは自己減算動作に使用される.

  • 本システムで用いられるRedisのデータ構造は基本的に以上である.次に、最も基本的な操作(ブログを新規作成)からredisのデータストレージの設計を開始します.その前にredis keyのフォーマットを制約し、すべてのkeyは次のフォーマットを使用します: : :ID.このようなメリットは、検索が容易で、複数の人が協力している間にkeyの衝突を防ぐことです.
    新規記事
    生成ID
    新しいブログはまずIDが必要で、関係型データベース(MySQLなど)は一般的に自己成長idを提供し、redisはINCRコマンドを使用して同じ効果を達成することができます.例えば、前のブログidを記録したkeyがarticle:nextvalであれば、新しいIDを取得するコマンドはINCR article:nextval 1であり、このコマンドの効果は、keyが存在し、数字であれば1を加算して1を加算した値を返し、keyが存在しない場合は1を新規作成し、デフォルト値を0に設定して1を加算することである.このコマンドは原子操作なので、複数のスレッドが同じ値になる同時発生はありません.keyの定義は次のとおりです.
    key
    valueタイプ
    使用方法
    article:nextval
    string
    ブログid sequence、毎回1を追加
    JAvaで次のブログidを取得するコードは以下の通りです.ここではidを取得する方法をツールクラスにカプセル化し、他のDaoを直接呼び出せばいいです.
    @Repository
    public class RedisSequenceImpl implements SequenceSupport {
        @Autowired
        private RedisSupport redisSupport;
    
        @Override
        public Long nextValue(String sequenceName) {
            return redisSupport.incr(sequenceName, 1);
        }
    }
    
    @Repository
    public class ArticleDaoImpl implements ArticleDao {
        private static final String ARTICLE_SEQ = "article:nextval";
        @Autowired
        private SequenceSupport seqSupport;
        @Override
        public void create(ArticleDto article) {
            article.setId(seqSupport.nextValue(ARTICLE_SEQ));
            。。。
        }
    }
    

    記事の内容を保存
    前述したredisデータ構造では、リレーショナル・データベース内のテーブルの1行のデータをredisのhash構造で格納できます.ここでは、2つのkeyを使用して1つの文章を格納します.最初のkeyのvalueはhashで、文章の基本属性、作者、時間、読書回数、タイトル、本文の最初の64文字などの属性を格納します.2番目のkeyのvalueはstringで、文章の本文、つまり本文の完全なhtmlを格納します.
    どうしてそうするの?redisを使用する上で重要な原則はvalueのサイズを小さくすることです.文章の本文をhashに存在させると、valueが大きくなり、小さな属性(例えば、読解数に1を加える)を変更するたびにメモリの再割り当てが発生します.もう1つの理由は、ほとんどのブログを読むのは、リストを調べてから詳細を見ることです.本文は単独で保存され、必要に応じてクエリーできます.したがってredisのデータ構造は次のとおりです.
    key
    valueタイプ
    使用方法
    article:${id}
    Hash
    記事を格納し、hashでは属性名でkey、属性値でvalueを作成します.${id}は、上で取得した記事idを表します.
    article:content:${id}
    string
    記事本文の詳細、リッチテキストを格納
    記事に格納されている問題は解決され、クエリーの問題も解決されます.クエリに関連するいくつかの問題を解決する必要があります.
  • ユーザーはブログを見て、1つは検索エンジンを通じて流れてきたので、idに基づいて直接検索します.もう1つは、リストを調べてから単一の文章に進むので、SortedSetは、scoreとしてパブリッシュ時間を使用するパブリッシュされたブログのリストを格納する必要があります.トップ記事については、score値の設定がより大きい必要があるため、すべてのトップ記事10+のパブリッシュ時間をscoreとする.
  • 著者にとっては、公開されたブログに加えて、未公開の記事も表示されるため、作成時間をscore
  • として使用するすべてのブログを格納するには、個別のSortedSetが必要です.
  • 前章では,文章情報が変更された後,検索エンジンに通知する必要があると述べた.ここではredisのListを用いてメッセージキューの機能を実現する.変化するたびに文章idを左からListに入れ、検索エンジンのサービスを右から読み出す.

  • 以上の要件を満たすために、以下の構成を追加します.
    key
    valueタイプ
    使用方法
    article:ids
    SortedSet
    作成したブログのリストを保存し、時間ソートを作成します.
    article:pub:ids
    SortedSet
    パブリッシュされたブログのリストを保存し、パブリッシュ時にソート
    article:msg
    List
    ブログ変更リスト
    以上が文章に格納されているすべてのデータ構造です.次にコード部分に入り、controllerから始め、完全なブログを公開するにはいくつかのステップが必要です.
    新しいブログコードロジック
    まずcontrollerで、すべてのバックグラウンド管理クラスurlは/adminで始まり、後で権限制御を追加するのに便利です.Controllerは主にパラメータチェックを行い、サービスを呼び出して文章を保存します.
    @RestController
    @RequestMapping("/admin/article")
    public class ArticleAdminController {
        @PostMapping("/add")
        public Response add(@RequestBody @Validated({ValidGroups.AddGroup.class,Default.class}) ArticleDto article, BindingResult bindingResult){
            if(bindingResult.hasErrors())
                return new Response<>(ResultCode.INVALID_PARAM, bindingResult.getAllErrors().get(0).getDefaultMessage());
    
            Response response = articleService.create(article);
            if(response.getCode() > 0)
                return new Response<>(response.getCode(), response.getMessage());
            return new Response<>();
        }
    }
    

    サービスの実装を参照してください(コメントを参照):
    @Service
    public class ArticleServiceImpl implements ArticleService{
        @Override
        public Response create(ArticleDto article) {
            //           ,  css js,       html
            article.setContent(HtmlUtils.getSafeBody(article.getContent())); 
            //  Dao          Redis
            articleDao.create(article);
            //              ,            
            if(article.getStatus().intValue() == 1)
                articleDao.updatePubStatus(article);
            //   ID      ,      
            messageDao.push(article.getId());
            return new Response<>(article);
        }
    
    }
    

    サービスでは、Daoの3つのメソッドが呼び出され、コンテンツの保存->パブリッシュ->メッセージの送信、実装を参照してください.ここでは、Redisコマンドの使用方法に注目します.
    @Repository
    public class ArticleDaoImpl implements ArticleDao {
        @Override
        public void create(ArticleDto article) {
            //        ID
            article.setId(seqSupport.nextValue(ARTICLE_SEQ));
            //        
            if(article.getStatus() == null)
                article.setStatus(0);  //       
            java.util.Date now = new java.util.Date();
            article.setCreated(now);
            article.setModified(now);
            if(article.getAllowComment()==null)
                article.setAllowComment(true); //      
            if(article.getAllowShare()==null)
                article.setAllowShare(false);  //      
            //  Hash          64          
            String content = article.getContent();
            String header = StringUtils.left(HtmlUtils.getBodyText(content), 64);
            article.setContent(header);
           //  Pipline      Redis
            SessionCallback sessionCallback = new SessionCallback() {
                @Override
                public  Void execute(RedisOperations redisOperations) throws DataAccessException {
                    //        ,  Hash HMSET  ,BeanUtils.beanToMap      POJO  Map
                    redisOperations.opsForHash().putAll((K)("article:" + article.getId()), BeanUtils.beanToMap(article, "userLike"));
                   //      HTML      key,  SET  
                    redisOperations.opsForValue().set((K)("article:content:" + article.getId()), (V)content);
                    //   ID        ,        score,  SortedSet zAdd  
                    String score = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
                    redisOperations.opsForZSet().add((K)"article:ids", (V)article.getId(), Double.parseDouble(score));
                    return null;
                }
            };
            redisSupport.executePipelined(sessionCallback);
    
            log.debug("Save activle article:{} success", JSON.toJSONString(article));
        }
    
        /**
         *            /   
         */
        @Override
        public void updatePubStatus(ArticleDto article){
            Assert.notNull(article.getId(), "id must not be null");
            Assert.notNull(article.getStatus(), "status must not be null");
            //  Hash HMSET    status     (       )
            ArticleDto newDto = new ArticleDto();
            java.util.Date now = Calendar.getInstance().getTime();
            newDto.setStatus(article.getStatus());
            if(article.getStatus() == 1)
                newDto.setIssueTime(now);
            newDto.setModified(now);
            redisSupport.hmset("article:"+article.getId(), BeanUtils.beanToMap(newDto));
            //       ,        ,     , score  score       10。
            //     id        
            //       ,    ZREM   id  
            if(article.getStatus()==1) {
                String score = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
                if(BooleanUtils.isTrue(article.getIstop()))
                    score = "10"+score;
                redisSupport.zAdd("article:pub:ids", article.getId(), Double.parseDouble(score));
            }else if(article.getStatus()==0) {
                redisSupport.zRem("article:pub:ids", article.getId());
            }
        }
    }
    
    @Repository
    public class ArticleMessageDaoImpl implements ArticleMessageDao {
            //    ,   ID List    ,  LPUSH  
            @Override
        public void push(Long articleId) {
            redisSupport.lPush(article_msg_queue, articleId);
        }
    }
    

    ここまで、redisコマンドの使用に加えて、createメソッドではpipelineを使用してコマンドをコミットする新しいブログのリリースが完了しました.pipelineを使用すると、複数のコマンドを一度に実行する際にコマンド実行時間を大幅に短縮できます.pipelineを使用すると、クライアントは複数のコマンドをパッケージ化して一度にredisにコミットし、ネットワーク往復の時間を大幅に短縮するためです.新しいブログを作成する過程で、Redisとリレーショナル・データベースの最大の違いは2つあります.1つは、リストと属性を2つのkeyに分けて保存することです.また、クエリーの際にフィルタリングがサポートされていないため、条件クエリーが必要な場合は、上記のすべての文章とパブリッシュされた文章を別々に保存する必要があるなど、リストを事前に準備する必要があります.だからブログというデータ構造が簡単で、クエリーも複雑ではないビジネスでredisを使うのは全く問題ありません.複数の条件の組合せクエリーが必要な場合、リレーショナル・データの優位性はより顕著ですが、redisも実現できないわけではありません.複雑さを増すだけで、開発者はredisに詳しい必要があります.
    ブログ検索ロジック
    パブリケーションに比べて、クエリーはずっと簡単です.Daoのコードを直接見ればいいです.
    @Override
        public List listPub(int startIndex, int pageSize) {
            //          index
            int stopIndex = startIndex+pageSize-1;
            //  SortedSet ZREVRANGE    ID  ,       score             
            //               score   ,     score  
            Set ids = redisSupport.zRevRange("article:pub:ids", startIndex, stopIndex);
            //     ID,  Hash HMGET          ,   POJO
            List articleList =
                    ids.stream()
                        .map(e -> {
                            Map result = redisSupport.hmget("article:" + e);
                            return BeanUtils.mapToBean(result, ArticleDto.class);})
                        .filter(e->(e!=null) && e.getId()!=null && NumberUtils.zeroOnNull(e.getStatus())==1)
                        .sorted(ArticleDto::compareByIssueTime)
                        .collect(Collectors.toList());
            return articleList;
        }
    

    文章がほめられる.
    点賛機能は比較的簡単で、主に2つのデータを操作します.1つは、ブログのプロパティの「いいね」の数を変更し、「いいね」をクリックするときに1を追加し、「いいね」をキャンセルすると1を減らすことです.そして、各ブログには「いいね」の集合を記録する必要があります.Setを使用すると、自動的に「いいね」を繰り返すのを防ぐことができます.データ構造は次のとおりです.
    key
    valueタイプ
    使用方法
    article:like:{articleID}
    Set
    文章にいいねをつけるuser集合
    コードは次のとおりです.
    @RestController
    @RequestMapping("/article")
    public class ArticleController {
        @PostMapping("/{articleId}/like")
        public Response like(@PathVariable Long articleId,@SessionAttr("user") UserDto user){
            if(articleId < 0)
                return new Response<>(ResultCode.INVALID_PARAM,"     ");
    
            return articleService.addLike(articleId, user.getUserId());
        }
    }
    
    @Service
    public class ArticleServiceImpl implements ArticleService{
        @Override
        public Response addLike(Long articleId, Long userId) {
            ArticleDto article = articleDao.getSummary(articleId);      
            if(article != null) {
                Integer likeCount = article.getLikeCount();
                // userId    ,    true,      false
                boolean result = articleLikeDao.add(articleId, userId);
                if(result) //        +1
                    likeCount = articleDao.increaseLikeNum(articleId);
                return new Response<>(likeCount==null ? 0:likeCount);
            }
            return new Response<>(ResultCode.LOGICAL_ERROR, "           ");
        }
    }
    
    @Repository
    public class ArticleLikeDaoImpl implements ArticleLikeDao {
        @Override
        public boolean add(Long articleId, Long userId) {
            Assert.notNull(articleId, "article id must not be null");
            Assert.notNull(userId,"user id must not be null");
            // userId  SADD      ,num      
            long num = redisSupport.sAdd("article:like:"+articleId, userId);
            return (num > 0);
        }
    }
    
    @Repository
    public class ArticleDaoImpl implements ArticleDao {
        @Override
        public Integer increaseLikeNum(Long id) {
            Assert.notNull(id, "id must not be null");
           //       +1,      likeCount    
            return (int)redisSupport.hincr("article:"+id,"likeCount", 1);
        }
    }
    

    点賛の操作には2つの注目すべき点があります.
  • SADDを使用して集合に要素を追加すると、実際にいくつか追加されたことに戻ります.つまり、要素がすでに集合の中にある場合は、0
  • を返します.
  • Hashが原子を供給するHINCRコマンドは、属性値を+1にし、加算値
  • を返す.
    以上の2つの特性は素晴らしいですか?秒殺関係型データベースはありますか?
    ユーザーコメント
    コメントの機能は比較的簡単で、以下にデータ構造を列挙して、コードは貼らないで、興味のあるのはgitの上のコードを見ることができます
    key
    valueタイプ
    使用方法
    comment:nextval
    string
    コメントIdの自己増加シーケンス
    comment:{commentID}
    Hash
    メッセージを保存
    comment:ids:{articleID}
    List
    文章の伝言リスト
    comment:like:{userID}
    Set
    ユーザーが「いいね」をクリックしたコメントのリスト
    ここまでSpingboot+Redisで実現した個人ブログはほぼ終了しており、権限管理や個人情報などの機能は比較的簡単なので実現していません.次の記事では、実現過程で遭遇した問題やRedisの使い方についてお話ししますので、ご期待ください