点讃モジュール設計-Redisキャッシュ+タイミングライトデータベース高性能点讃機能を実現

38559 ワード

ソースアドレス:github.com/cachecats/c…
いいねはシステム全体の小さなモジュールとして、コードはuser-serviceユーザーサービスの下にあります.
本稿ではSpringCloudに基づいて,ユーザが「いいね」を発し,「いいね」を取り消した後,まずRedisに格納し,2時間おきにRedisからいいねデータを読み出してデータベースに書き込み,永続化して格納する.
いいね機能は多くのシステムにありますが、機能が小さいのを見ないで、よく考えなければならないことが多いです.
いいね、いいねを取り消すのは高頻度の操作で、毎回データベースを読み書きすると、大量の操作がデータベースの性能に影響するので、キャッシュが必要です.
どのくらいRedisから一度データを取ってデータベースに保存するかは、プロジェクトの実際の状況によって決まりますが、私は一時的に2時間設定しました.
プロジェクトのニーズは、誰が「いいね」をクリックしたのかを確認する必要があるので、「いいね」をクリックした人や「いいね」をクリックした人を保存するには、簡単にカウントすることはできません.
文章は4つの部分に分けて紹介します.
  • Redisキャッシュ設計及び実現
  • データベース設計
  • データベース操作
  • オープンタイミングタスク永続化データベース
  • へ格納
    一、Redisキャッシュの設計と実現
    1.1 Redis設置及び運転
    Redisのインストールは、関連するチュートリアルを参照してください.
    DockerをインストールしてRedisを実行すると言います
    docker run -d -p 6379:6379 redis:4.0.8
    

    Redisがインストールされている場合は、コマンドラインを開き、Redisを起動するコマンドを入力します.
    redis-server
    

    1.2 RedisとSpringBootプロジェクトの統合
  • pom.xmlに依存
  • を導入する.
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
  • 起動クラスに注釈@EnableCaching
  • を追加する.
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableSwagger2
    @EnableFeignClients(basePackages = "com.solo.coderiver.project.client")
    @EnableCaching
    public class UserApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(UserApplication.class, args);
        }
    }
    
  • Redis構成クラスRedisConfig
  • を記述する.
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    
    import java.net.UnknownHostException;
    
    
    @Configuration
    public class RedisConfig {
    
        @Bean
        @ConditionalOnMissingBean(name = "redisTemplate")
        public RedisTemplate redisTemplate(
                RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
    
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
    
            RedisTemplate template = new RedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            template.setKeySerializer(jackson2JsonRedisSerializer);
            template.setValueSerializer(jackson2JsonRedisSerializer);
            template.setHashKeySerializer(jackson2JsonRedisSerializer);
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
            return template;
        }
    
    
        @Bean
        @ConditionalOnMissingBean(StringRedisTemplate.class)
        public StringRedisTemplate stringRedisTemplate(
                RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
    }
    

    これでRedisのSpringBootプロジェクトでの構成が完了し、快適に使用できます.
    1.3 Redisのデータ構造タイプ
    Redisは、String(文字列)、List(リスト)、Set(集合)、Hash(ハッシュ)、Zset(整列集合)の5つの異なるデータ構造タイプとの間のキーのマッピングを格納することができる.
    この5つのデータ構造タイプについて簡単に説明します.
    構造タイプ
    構造ストレージの値
    構造の読み書き能力
    String
    文字列、整数または浮動小数点数
    文字列または文字列の一部全体に対して操作を実行します.オブジェクトと浮動小数点数の自己増加(increment)または自己減少(decrement)
    List
    チェーンテーブルの各ノードに文字列が含まれているチェーンテーブル
    チェーンテーブルの両端から要素をプッシュまたはポップアップします.オフセット量に基づいてチェーンテーブルをトリミングする(trim);単一または複数の要素を読み込みます.値に基づいて要素を検索または削除
    Set
    文字列を含む無秩序コレクタ(unorderedcollection)であり、含まれる各文字列はユニークで異なる
    単一の要素を追加、取得、削除します.エレメントがコレクションに存在するかどうかを確認します.交差、並列、差分セットを計算します.コレクションからランダムに要素を取得する
    Hash
    キー値ペアを含む無秩序分散リスト
    単一のキー値ペアを追加、取得、削除します.すべてのキー値ペアを取得
    Zset
    文字列メンバー(member)と浮動小数点数スコア(score)の間の秩序マッピング、要素の配列順序はスコアのサイズによって決定されます
    単一の要素を追加、取得、削除します.要素は、スコア範囲(range)またはメンバーに基づいて取得されます.
    1.4点賛データのRedisにおける格納フォーマット
    Redisでは2種類のデータが格納されています.1つは、「いいね」をクリックした人、「いいねをクリックした人」、「いいねをクリックした人」、「いいねをクリックした人」、「いいねをクリックした人」の状態を記録したデータで、もう1つはユーザーごとに何回「いいね」をクリックしたかを簡単
    「いいね」と「いいね」を記録する必要があり、「いいね」の状態(いいね、いいねの取り消し)もあるため、Redisのすべての「いいね」データを一定時間間隔で取り出し、次の「Redisデータフォーマット」のHashが最適であることを分析した.Hashのデータはすべて1つのキーに存在するため、このキーを通じてすべての点賛データを簡単に取り出すことができます.このキーの中のデータはキー値のペアの形式に保存することもでき、いいね人、いいね人、いいね状態を保存するのに便利です.
    点赞人のidはlikedPostId,点赞人のidはlikedUserId,点赞时状态は1,キャンセル点赞状态は0とする.点賛人idと被点賛人idをキーとし、2つのidの間を::で区切り、点賛状態を値とする.
    したがって、ユーザが「いいね」をクリックすると、格納されたキーはlikedUserId::likedPostIdであり、対応する値は1である.
    いいねをキャンセルします.保存されているキーはlikedUserId::likedPostIdで、対応する値は0です.
    データを取るときにキーを::で切ると2つのidが得られ、便利です.
    ビジュアル化ツールRDMで見たのは、このようなものです.
    1.5操作Redis
    Redisの各種データフォーマットの操作方法はこの文章を見ることができて、書くのがとても良いです.
    具体的な操作方法をRedisServiceインタフェースにカプセル化
    RedisService.java
    import com.solo.coderiver.user.dataobject.UserLike;
    import com.solo.coderiver.user.dto.LikedCountDTO;
    
    import java.util.List;
    
    public interface RedisService {
    
        /**
         *   。   1
         * @param likedUserId
         * @param likedPostId
         */
        void saveLiked2Redis(String likedUserId, String likedPostId);
    
        /**
         *     。      0
         * @param likedUserId
         * @param likedPostId
         */
        void unlikeFromRedis(String likedUserId, String likedPostId);
    
        /**
         *  Redis         
         * @param likedUserId
         * @param likedPostId
         */
        void deleteLikedFromRedis(String likedUserId, String likedPostId);
    
        /**
         *         1
         * @param likedUserId
         */
        void incrementLikedCount(String likedUserId);
    
        /**
         *         1
         * @param likedUserId
         */
        void decrementLikedCount(String likedUserId);
    
        /**
         *   Redis          
         * @return
         */
        List getLikedDataFromRedis();
    
        /**
         *   Redis          
         * @return
         */
        List getLikedCountFromRedis();
    
    }
    

    実装クラスRedisServiceImpl.java
    import com.solo.coderiver.user.dataobject.UserLike;
    import com.solo.coderiver.user.dto.LikedCountDTO;
    import com.solo.coderiver.user.enums.LikedStatusEnum;
    import com.solo.coderiver.user.service.LikedService;
    import com.solo.coderiver.user.service.RedisService;
    import com.solo.coderiver.user.utils.RedisKeyUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.Cursor;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ScanOptions;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    
    @Service
    @Slf4j
    public class RedisServiceImpl implements RedisService {
    
        @Autowired
        RedisTemplate redisTemplate;
    
        @Autowired
        LikedService likedService;
    
        @Override
        public void saveLiked2Redis(String likedUserId, String likedPostId) {
            String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
            redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());
        }
    
        @Override
        public void unlikeFromRedis(String likedUserId, String likedPostId) {
            String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
            redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());
        }
    
        @Override
        public void deleteLikedFromRedis(String likedUserId, String likedPostId) {
            String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
            redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
        }
    
        @Override
        public void incrementLikedCount(String likedUserId) {
            redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1);
        }
    
        @Override
        public void decrementLikedCount(String likedUserId) {
            redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1);
        }
    
        @Override
        public List getLikedDataFromRedis() {
            Cursor> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
            List list = new ArrayList<>();
            while (cursor.hasNext()){
                Map.Entry entry = cursor.next();
                String key = (String) entry.getKey();
                //    likedUserId,likedPostId
                String[] split = key.split("::");
                String likedUserId = split[0];
                String likedPostId = split[1];
                Integer value = (Integer) entry.getValue();
    
                //    UserLike   
                UserLike userLike = new UserLike(likedUserId, likedPostId, value);
                list.add(userLike);
    
                //   list    Redis    
                redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
            }
    
            return list;
        }
    
        @Override
        public List getLikedCountFromRedis() {
            Cursor> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
            List list = new ArrayList<>();
            while (cursor.hasNext()){
                Map.Entry map = cursor.next();
                //         LikedCountDT
                String key = (String)map.getKey();
                LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());
                list.add(dto);
                // Redis       
                redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
            }
            return list;
        }
    }
    

    使用するツールクラスと列挙クラス
    RedisKeyUtilsは、一定のルールに基づいてkeyを生成するために使用されます.
    public class RedisKeyUtils {
    
        //         key
        public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";
        //          key
        public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";
    
        /**
         *         id      id  key。   222222::333333
         * @param likedUserId      id
         * @param likedPostId      id
         * @return
         */
        public static String getLikedKey(String likedUserId, String likedPostId){
            StringBuilder builder = new StringBuilder();
            builder.append(likedUserId);
            builder.append("::");
            builder.append(likedPostId);
            return builder.toString();
        }
    }
    

    LikedStatusEnumユーザーの称賛状態の列挙クラス
    package com.solo.coderiver.user.enums;
    
    import lombok.Getter;
    
    /**
     *        
     */
    @Getter
    public enum LikedStatusEnum {
        LIKE(1, "  "),
        UNLIKE(0, "    /   "),
        ;
    
        private Integer code;
    
        private String msg;
    
        LikedStatusEnum(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    }
    

    二、データベース設計
    データベース・テーブルには、少なくとも3つのフィールドが含まれます.プライマリキーidを加えて、作成時間、修正時間でいいです.
    テーブル文
    create table `user_like`(
    	`id` int not null auto_increment,
    	`liked_user_id` varchar(32) not null comment '      id',
    	`liked_post_id` varchar(32) not null comment '     id',
    	`status` tinyint(1) default '1' comment '    ,0  ,1  ',
    	`create_time` timestamp not null default current_timestamp comment '    ',
      `update_time` timestamp not null default current_timestamp on update current_timestamp comment '    ',
    	primary key(`id`),
    	INDEX `liked_user_id`(`liked_user_id`),
    	INDEX `liked_post_id`(`liked_post_id`)
    ) comment '     ';
    

    対応するオブジェクトUserLike
    import com.solo.coderiver.user.enums.LikedStatusEnum;
    import lombok.Data;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    /**
     *      
     */
    @Entity
    @Data
    public class UserLike {
    
        //  id
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
    
        //       id
        private String likedUserId;
    
        //      id
        private String likedPostId;
    
        //     .     
        private Integer status = LikedStatusEnum.UNLIKE.getCode();
    
        public UserLike() {
        }
    
        public UserLike(String likedUserId, String likedPostId, Integer status) {
            this.likedUserId = likedUserId;
            this.likedPostId = likedPostId;
            this.status = status;
        }
    }
    

    三、データベース操作
    操作データベースもインタフェースにカプセル化
    LikedService
    import com.solo.coderiver.user.dataobject.UserLike;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    
    import java.util.List;
    
    public interface LikedService {
    
        /**
         *       
         * @param userLike
         * @return
         */
        UserLike save(UserLike userLike);
    
        /**
         *        
         * @param list
         */
        List saveAll(List list);
    
    
        /**
         *        id      (            )
         * @param likedUserId      id
         * @param pageable
         * @return
         */
        Page getLikedListByLikedUserId(String likedUserId, Pageable pageable);
    
        /**
         *       id      (            )
         * @param likedPostId
         * @param pageable
         * @return
         */
        Page getLikedListByLikedPostId(String likedPostId, Pageable pageable);
    
        /**
         *           id          
         * @param likedUserId
         * @param likedPostId
         * @return
         */
        UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId);
    
        /**
         *  Redis            
         */
        void transLikedFromRedis2DB();
    
        /**
         *  Redis             
         */
        void transLikedCountFromRedis2DB();
    
    }
    

    LikedServiceImpl実装クラス
    import com.solo.coderiver.user.dataobject.UserInfo;
    import com.solo.coderiver.user.dataobject.UserLike;
    import com.solo.coderiver.user.dto.LikedCountDTO;
    import com.solo.coderiver.user.enums.LikedStatusEnum;
    import com.solo.coderiver.user.repository.UserLikeRepository;
    import com.solo.coderiver.user.service.LikedService;
    import com.solo.coderiver.user.service.RedisService;
    import com.solo.coderiver.user.service.UserService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    @Service
    @Slf4j
    public class LikedServiceImpl implements LikedService {
    
        @Autowired
        UserLikeRepository likeRepository;
    
        @Autowired
        RedisService redisService;
    
        @Autowired
        UserService userService;
    
        @Override
        @Transactional
        public UserLike save(UserLike userLike) {
            return likeRepository.save(userLike);
        }
    
        @Override
        @Transactional
        public List saveAll(List list) {
            return likeRepository.saveAll(list);
        }
    
        @Override
        public Page getLikedListByLikedUserId(String likedUserId, Pageable pageable) {
            return likeRepository.findByLikedUserIdAndStatus(likedUserId, LikedStatusEnum.LIKE.getCode(), pageable);
        }
    
        @Override
        public Page getLikedListByLikedPostId(String likedPostId, Pageable pageable) {
            return likeRepository.findByLikedPostIdAndStatus(likedPostId, LikedStatusEnum.LIKE.getCode(), pageable);
        }
    
        @Override
        public UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) {
            return likeRepository.findByLikedUserIdAndLikedPostId(likedUserId, likedPostId);
        }
    
        @Override
        @Transactional
        public void transLikedFromRedis2DB() {
            List list = redisService.getLikedDataFromRedis();
            for (UserLike like : list) {
                UserLike ul = getByLikedUserIdAndLikedPostId(like.getLikedUserId(), like.getLikedPostId());
                if (ul == null){
                    //    ,    
                    save(like);
                }else{
                    //   ,    
                    ul.setStatus(like.getStatus());
                    save(ul);
                }
            }
        }
    
        @Override
        @Transactional
        public void transLikedCountFromRedis2DB() {
            List list = redisService.getLikedCountFromRedis();
            for (LikedCountDTO dto : list) {
                UserInfo user = userService.findById(dto.getId());
                //             ,       
                if (user != null){
                    Integer likeNum = user.getLikeNum() + dto.getCount();
                    user.setLikeNum(likeNum);
                    //      
                    userService.updateInfo(user);
                }
            }
        }
    }
    

    データベースの操作はこれだけで、主に削除して調べます.
    四、オープンタイミングタスクの永続化データベースへの保存
    タイミングタスクQuartzは強力で、それを使用します.Quartz使用手順:
  • 依存
  • を追加
    
        org.springframework.boot
        spring-boot-starter-quartz
    
    
  • プロファイル作成
  • package com.solo.coderiver.user.config;
    
    import com.solo.coderiver.user.task.LikeTask;
    import org.quartz.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class QuartzConfig {
    
        private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";
    
        @Bean
        public JobDetail quartzDetail(){
            return JobBuilder.newJob(LikeTask.class).withIdentity(LIKE_TASK_IDENTITY).storeDurably().build();
        }
    
        @Bean
        public Trigger quartzTrigger(){
            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
    //                .withIntervalInSeconds(10)  //         
                    .withIntervalInHours(2)  //        
                    .repeatForever();
            return TriggerBuilder.newTrigger().forJob(quartzDetail())
                    .withIdentity(LIKE_TASK_IDENTITY)
                    .withSchedule(scheduleBuilder)
                    .build();
        }
    }
    
  • 実行タスクを記述するクラスは、QuartzJobBean
  • から継承される.
    package com.solo.coderiver.user.task;
    
    import com.solo.coderiver.user.service.LikedService;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang.time.DateUtils;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.quartz.QuartzJobBean;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     *        
     */
    @Slf4j
    public class LikeTask extends QuartzJobBean {
    
        @Autowired
        LikedService likedService;
    
        private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        @Override
        protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    
            log.info("LikeTask-------- {}", sdf.format(new Date()));
    
            //  Redis              
            likedService.transLikedFromRedis2DB();
            likedService.transLikedCountFromRedis2DB();
        }
    }
    

    タイミングタスクでLikedServiceカプセル化メソッドを直接呼び出してデータ同期を完了する.
    以上が点賛機能の設計と実現であり、不足点は皆さんによろしくお願いします.
    より良い実装案があれば、コメントエリアでの交流を歓迎します.
    コードはオープンソースプロジェクトCodeRiverから出て、全プラットフォーム型全スタック精品オープンソースプロジェクトの構築に力を入れている.
    coderiver中国語名河コードは、プログラマーとデザイナーにプロジェクト協力を提供するプラットフォームです.フロントエンド、バックエンド、モバイル開発者、デザイナー、プロダクトマネージャにかかわらず、プラットフォーム上でプロジェクトを公開し、同じパートナーと協力してプロジェクトを完了することができます.
    coderiver河コードはプログラマー宿屋に似ているが、主な目的は各細分化分野の人材間の技術交流を便利にし、共同で成長し、多くの人が協力してプロジェクトを完成することである.金銭取引には触れない.
    PC端末(Vue、React)、モバイルH 5(Vue、React)、ReactNativeのハイブリッド開発、Androidオリジナル、微信ウィジェット、javaバックエンドを含むフルプラットフォーム型フルスタックプロジェクトを計画しており、注目を集めたい.
    プロジェクトアドレス:github.com/cachecats/c…
    あなたの励ましは私の前進の最大の動力で、いいねを歓迎して、小さい星を送ることを歓迎します✨ ~