[Cache]RedisとMysqlの同期問題に対する解決策(feat.Redison)


最終位置決めで更新(または削除)された場合
redisとMysqlの間に同期の問題があることを発見しました!
今日はこれを宣伝します.

書き込みスルーで解決


同期の方法で、最初に考えられることは、次のとおりです.
値を更新すると、redisも更新した値を同時に保存します.

これを書き込み-透過方式と呼びます!

ちなみに、既存のJedisは書き込み方式には適用されないので、Redisonを使って操作します.

透過コードの書き込み


pom.xml

<!--        <dependency>-->
<!--            <groupId>org.springframework.data</groupId>-->
<!--            <artifactId>spring-data-redis</artifactId>-->
<!--            <version>2.3.3.RELEASE</version>-->
<!--        </dependency>-->

<!--        <dependency>-->
<!--            <groupId>redis.clients</groupId>-->
<!--            <artifactId>jedis</artifactId>-->
<!--            <version>3.3.0</version>-->
<!--            <type>jar</type>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
  • 前回追加したspring-data-redis,jedisは注釈処理
  • を行った.
  • コメントを行わないとNoSuchMethodError2
  • が生成する.
  • Redissonとspring-sessession-data-Redis依存
  • を追加
    SpringbootApplicationセクション
    @SpringBootApplication
    public class RedisPracticeApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(RedisPracticeApplication.class, args);
        }
    
        @Bean
        public RedissonClient redissonClient(){
    
            Config config = new Config();
            config.useSingleServer().setAddress("redis://192.168.0.20:6379");
            return Redisson.create(config);
    
        }
    }
    
    @SpringbootApplicationAnnotationセクションにbeanを登録して
  • Redisconクライアントを登録
  • RedisconクライアントにRedissサーバ上の情報
  • を登録する.
  • redisサーバアドレスは「redis://host:port「フォーマットは
  • です.
  • 構成(org.redisson.config)を使用すると、アドレス情報のほか、接続プールの大きさなど、多くの部分があります。構成
  • とすることができる.
    RedisConfig.java
    
    @Configuration
    @EnableCaching
    public class RedisConfig{
    
        private StudentRepository studentRepository;
        private RedissonClient redissonClient;
    
        public RedisConfig(StudentRepository studentRepository, RedissonClient redissonClient) {
            this.studentRepository = studentRepository;
            this.redissonClient = redissonClient;
        }
    
        @Bean
        public RMapCache<String, Student> studentRMapCache(){
            final RMapCache<String, Student> studentRMapCache
                    = redissonClient.getMapCache("Student", MapOptions.<String, Student>defaults()
                    .writer(getStudentMapWriter())
                    .writeMode(MapOptions.WriteMode.WRITE_BEHIND));
                    
    
    
            return studentRMapCache;
        }
    
        private MapWriter<String, Student> getStudentMapWriter(){
            return new MapWriter<String, Student>(){
                @Override
                public void write(Map<String, Student> map) {
                    map.forEach((k, v) -> {
                        studentRepository.save(v);
                    });
                }
    
                @Override
                public void delete(Collection<String> keys) {
                    keys.stream().forEach(key -> {
                        studentRepository.deleteById(key);
                    });
                }
            };
        }
    }
    
  • より前に作成されたJedisConnectionFactoryとRedisTemplateにコメントがあります
  • RMAPCacheキャッシュのTTL(time to live)
  • を設定できます.
  • MapWriter<キー値タイプ、保存するタイプ>を上書きして、キャッシュに値を書き込みまたは削除するときに実行するアクション
  • を指定します.
  • の場合、キャッシュの値をデータベースに保存します.
  • は、キャッシュからデータベースを削除することも求めます.
    MapOptionsは
  • 書き込みモードを採用している.WriteMode.WRITE BEHINDは書き込み後のキャッシュを許可する
    - cf ) .writeMode(MapOptions.WriteMode.WRITE_THROUGH));
  • StudentService.java

    @Service
    public class StudentService {
    
        private StudentRepository studentRepository;
        private RMapCache<String, Student> studentRMapCache;
    
        @Autowired
        public StudentService(StudentRepository studentRepository, RMapCache<String, Student> studentRMapCache) {
            this.studentRepository = studentRepository;
            this.studentRMapCache = studentRMapCache;
        }
    
        public Student save(Student student){
            studentRMapCache.put(student.getId(), student);
            return student;
    
        }
    
        public Student findById(String id){
            return this.studentRMapCache.get(id);
        }
    
        public void update(String id, String name) throws Exception {
            Student student = studentRMapCache.get(id);
            student.setName(name);
            studentRMapCache.put(id, student);
        }
    
        public void delete(String id){
    
            studentRMapCache.remove(id);
    
        }
    
    }
    動作
  • 値のStudioサービスを実現しました.
    新しい値
  • の挿入/更新時のRMAPCache.put(id、保存する値)などのキャッシュのみを操作します.
  • を挿入する場合、TTLを指定することもできます.

    今からテストを行います


    Redisson.Test
    @SpringBootTest
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    public class RedissonTest {
    
        StudentService studentService;
        RedissonClient redissonClient;
        StudentRepository studentRepository;
    
        @Autowired
        public RedissonTest(StudentService studentService, RedissonClient redissonClient
                , StudentRepository studentRepository) {
            this.studentService = studentService;
            this.redissonClient = redissonClient;
            this.studentRepository = studentRepository;
        }
    
    
        @Test
        @Order(1)
        void saveTest(){
            Student student1 = new Student("1", "zzarbttoo1", Student.Gender.FEMALE, 1);
            Student student2 = new Student("2", "zzarbttoo2", Student.Gender.FEMALE, 2);
            Student student3 = new Student("3", "zzarbttoo3", Student.Gender.FEMALE, 3);
            Student student4 = new Student("4", "zzarbttoo4", Student.Gender.FEMALE, 4);
            Student student5 = new Student("5", "zzarbttoo5", Student.Gender.FEMALE, 5);
    
            studentService.save(student1);
            studentService.save(student2);
            studentService.save(student3);
            studentService.save(student4);
            studentService.save(student5);
    
        }
    
        @Test
        @Order(2)
        void selectTest(){
    
            long start1 = System.currentTimeMillis();
    
            System.out.println(studentService.findById("1").toString());
            System.out.println(studentService.findById("2").toString());
            System.out.println(studentService.findById("3").toString());
            System.out.println(studentService.findById("4").toString());
            System.out.println(studentService.findById("5").toString());
    
            long end1 = System.currentTimeMillis();
            System.out.println(end1 - start1);
    
            long start2 = System.currentTimeMillis();
    
            System.out.println(studentService.findById("1").toString());
            System.out.println(studentService.findById("2").toString());
            System.out.println(studentService.findById("3").toString());
            System.out.println(studentService.findById("4").toString());
            System.out.println(studentService.findById("5").toString());
    
            long end2 = System.currentTimeMillis();
    
            System.out.println(end2 - start2);
    
            long start3 = System.currentTimeMillis();
    
            System.out.println(studentService.findById("1").toString());
            System.out.println(studentService.findById("2").toString());
            System.out.println(studentService.findById("3").toString());
            System.out.println(studentService.findById("4").toString());
            System.out.println(studentService.findById("5").toString());
    
            long end3 = System.currentTimeMillis();
    
            System.out.println(end3 - start3);
    
    
        }
    
        @Test
        @Order(3)
        void updateTest() throws Exception {
    
            studentService.update("1", "updated Name");
    
            Student selectStudent = studentRepository.findById("1").get();
            System.out.println(selectStudent.toString());
    
            Student redisStudent = studentService.findById("1");
            System.out.println(redisStudent.toString());
    
        }
    
        @Test
        @Order(4)
        void deleteTest(){
    
            System.out.println(studentService.findById("4"));
            studentService.delete("4");
            Assertions.assertNull(studentService.findById("4"));
    
        }
    }
    
    テストコードは以下の通りです
    まずselectの結果を表示します.

    cacheとdbに同時に格納されるため、1回目の運転とその後の運転の速度差が以前ほど大きくないと判断できます.

    updateの場合、同期済みが表示されます
    deleteの場合、cacheの値を削除するだけでdbの値がnullであることを決定できます.

    書き込み方式に問題がある


    上記のようにすれば、同期は直ちに行われるものと考えられる.
    ただし、write/update時にすべての情報を再格納するため、すべての情報を2つのリポジトリに格納する必要があります.
    不要なキャッシュ処理がある場合
    (実際には20%未満の情報だけが再利用されます)
    そのためには、不要なデータを削除するために、アクティビティ(TTL)にデータの時間を指定する必要があります.
    また、書き込みのたびにDBが使用されるため、速度も遅い.

    書き込みバックアップで解決



    write backは、すべてのwrite操作をredisに配置し、バッチを一定時間おきに実行します.
    Redisサーバに格納されているデータをDBに移動する動作.
    Insertでは複数のイベントを一度に処理し、速度が速いという利点があります
    もう1つの欠点は、障害が発生し、キャッシュ・サーバにのみデータが格納されている場合、すべてのデータが失われる可能性があることです.
    通常、ログデータを格納するために使用されます.
    書き込み-転送と同様に、TTLを使用して不要なデータを削除する必要があります.

    コールバック


    write-throughコードとあまり変わらない
    RedisConfig.java
    @Configuration
    @EnableCaching
    public class RedisConfig{
    
        private StudentRepository studentRepository;
        private RedissonClient redissonClient;
    
        public RedisConfig(StudentRepository studentRepository, RedissonClient redissonClient) {
            this.studentRepository = studentRepository;
            this.redissonClient = redissonClient;
        }
    
        @Bean
        public RMapCache<String, Student> studentRMapCache(){
            final RMapCache<String, Student> studentRMapCache
                    = redissonClient.getMapCache("Student", MapOptions.<String, Student>defaults()
                    .writer(getStudentMapWriter())
                    .writeMode(MapOptions.WriteMode.WRITE_BEHIND)
                    .writeBehindBatchSize(5000)
                    .writeBehindDelay(1000)
            );
                    //.writeMode(MapOptions.WriteMode.WRITE_THROUGH));
    
            return studentRMapCache;
        }
    
        private MapWriter<String, Student> getStudentMapWriter(){
            return new MapWriter<String, Student>(){
                @Override
                public void write(Map<String, Student> map) {
                    map.forEach((k, v) -> {
                        studentRepository.save(v);
                    });
                }
    
                @Override
                public void delete(Collection<String> keys) {
                    keys.stream().forEach(key -> {
                        studentRepository.deleteById(key);
                    });
                }
            };
        }
  • MapWriter部分は書き込み時と同じ:
  • RMAPCacheを実施する場合、writeModeをMapOptionsに設定します.WriteMode.WRITE BEHIND
  • に設定
  • writeBehindDelay(1000)に設定し、毎秒間隔で展開(デフォルトは1000)
  • 次にテストを行います

    @SpringBootTest
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    public class RedissonTest {
    
        StudentService studentService;
        RedissonClient redissonClient;
        StudentRepository studentRepository;
    
        @Autowired
        public RedissonTest(StudentService studentService, RedissonClient redissonClient
                , StudentRepository studentRepository) {
            this.studentService = studentService;
            this.redissonClient = redissonClient;
            this.studentRepository = studentRepository;
        }
    
        @Test
        @Order(1)
        @Rollback(false)
        void saveTest(){
            Student student1 = new Student("1", "zzarbttoo1", Student.Gender.FEMALE, 1);
            Student student2 = new Student("2", "zzarbttoo2", Student.Gender.FEMALE, 2);
            Student student3 = new Student("3", "zzarbttoo3", Student.Gender.FEMALE, 3);
            Student student4 = new Student("4", "zzarbttoo4", Student.Gender.FEMALE, 4);
            Student student5 = new Student("5", "zzarbttoo5", Student.Gender.FEMALE, 5);
    
            studentService.save(student1);
            studentService.save(student2);
            studentService.save(student3);
            studentService.save(student4);
            studentService.save(student5);
    
        }
    
        @Test
        @Order(2)
        void selectTest(){
    
            long start1 = System.currentTimeMillis();
    
            System.out.println(studentService.findById("1").toString());
            System.out.println(studentService.findById("2").toString());
            System.out.println(studentService.findById("3").toString());
            System.out.println(studentService.findById("4").toString());
            System.out.println(studentService.findById("5").toString());
    
            long end1 = System.currentTimeMillis();
            System.out.println(end1 - start1);
    
            long start2 = System.currentTimeMillis();
    
            System.out.println(studentService.findById("1").toString());
            System.out.println(studentService.findById("2").toString());
            System.out.println(studentService.findById("3").toString());
            System.out.println(studentService.findById("4").toString());
            System.out.println(studentService.findById("5").toString());
    
            long end2 = System.currentTimeMillis();
    
            System.out.println(end2 - start2);
    
            long start3 = System.currentTimeMillis();
    
            System.out.println(studentService.findById("1").toString());
            System.out.println(studentService.findById("2").toString());
            System.out.println(studentService.findById("3").toString());
            System.out.println(studentService.findById("4").toString());
            System.out.println(studentService.findById("5").toString());
    
            long end3 = System.currentTimeMillis();
    
            System.out.println(end3 - start3);
    
    
        }
    
        @Test
        @Order(3)
        @Rollback(false)
        void updateTest() throws Exception {
    
            studentService.update("1", "updated Name");
            Thread.sleep(1000);
            
        }
    
        @Test
        @Order(4)
        @Rollback(false)
        void deleteTest() throws InterruptedException {
    
            System.out.println(studentService.findById("4"));
            studentService.delete("4");
            Assertions.assertNull(studentService.findById("4"));
    
            Thread.sleep(1000);
    
        }
    }
    
  • @Rollback(false)を使用して、テスト後の結果をDBに保持します.
  • Thread.展開(1000)が完了するまで展開を待ちましょう.
  • 私たちはこのすべてが順調に進むことを確保することができます.
    https://github.com/skshukla/SampleCacheWebApp
    https://www.baeldung.com/redis-redisson
    https://redisson.org/feature-comparison-redisson-vs-jedis.html
    https://www.programcreek.com/java-api-examples/?api=org.redisson.api.RMapCache
    https://www.javadoc.io/doc/org.redisson/redisson/3.7.2/org/redisson/api/RMapCache.html
    https://www.javadoc.io/doc/org.redisson/redisson/3.6.0/org/redisson/api/MapOptions.html
    https://www.javadoc.io/doc/org.redisson/redisson/3.4.4/org/redisson/api/map/MapWriter.html
    https://stackoverflow.com/questions/51992484/caused-by-java-lang-nosuchmethoderror-org-springframework-data-redis-connectio
    https://github.com/redisson/redisson/wiki/7.-distributed-collections#712-map-persistence
    https://www.youtube.com/watch?v=mPB2CZiAkKM
    https://waspro.tistory.com/697
    https://dzone.com/articles/database-caching-with-redis-and-java