[Cache]Redis와 Mysql의 동기화 문제에 대한 해결책들(feat. Redisson)
지난번 포스팅 에서 update (혹은 delete) 시
redis와 Mysql 간의 동기화 문제가 있는 것을 확인할 수 있었습니다!
오늘은 이에 대한 포스팅을 진행하도록 하겠습니다
Write Through 방식으로 해결하기
동기화를 해주는 방식 중 생각할 수 있는 것의 첫번째는
값을 update 할 때 redis에도 업데이트 한 값을 함께 저장해주는 것입니다
이를 Write-Through 방식이라고 합니다!
참고로 기존에 사용하던 Jedis 는 Write-through 방식이 적용되지 않으므로 저는 Redisson을 이용해서 진행을 해보도록 하겠습니다
Write Through 방식 코드
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 는 주석처리를 해주었습니다
- 만일 주석처리를 하지 않을 경우 NoSuchMethodError가 발생합니다
- redisson과 spring-session-data-redis dependency를 추가해줍니다
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);
}
}
- RedissonClient를 등록하기 위해 @SpringbootApplication annotation 부분에 bean을 등록합니다
- RedissonClient에 redis 서버의 정보를 등록합니다
- redis 서버의 주소는 "redis://host:port" 형식으로 되어있습니다
- Config(org.redisson.config.Config)를 이용해 주소 정보 이외에도 connection pool size 등 많은 부분에 대해 설정할 수 있습니다
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은 주석처리를 했습니다(Redisson 이용을 위해)
- RMapCache는 캐시의 TTL(time to live)를 설정할 수 있도록 해줍니다
- MapWriter<키값의 타입, 저장할 타입>을 override하여, 캐시에 값을 write 하거나 delete 할 때 할 행동을 지정해줍니다
- 이 때 저는 캐시에 저장하는 값이 DB에도 저장이 되도록 했습니다
- 또한 캐시에서 삭제요청을 하면 DB에서도 삭제가 되도록 했습니다
- writeMode를 MapOptions.WriteMode.WRITE_BEHIND로 하여 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);
}
}
- 값을 조작하는 StudentService를 구현했습니다
- 새로운 값을 insert/update 할 때 RMapCache.put(id, 저장할 값)과 같이 캐시에 대한 부분만 조작합니다(그럼 db에도 함께 들어가게 됩니다)
- 값을 insert 할 때 TTL도 함께 지정해줄 수 있습니다
그럼 이제 Test를 진행해보겠습니다
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에 동시 저장을 하기 때문에 첫번째 실행과 그 이후의 실행의 속도 차가 이전만큼 크지 않은 것을 확인할 수 있었습니다
update의 경우에는 동기화가 된 것을 볼 수 있었습니다
delete의 경우 cache의 값만 삭제해도 db의 값까지 null이 되는 것을 확인할 수 있었습니다
Write Through 방식의 문제점
위와 같은 방식으로 진행을 하면 동기화는 바로바로 된다고 볼 수 습니다
하지만 write/update 시 모든 정보를 redis 저장하게 되다 보니 저장소 두개에 모두 저장을 해야하고,
불필요하게 캐싱 처리 되는 경우가 있습니다
(사실상 20% 미만의 정보만 재사용된다는 사실)
이를 위해서는 데이터의 TTL(Time To Live)를 지정해줘 불필요한 데이터를 삭제해줘야합니다
또한 write 를 할 때마다 DB를 사용하기 때문에 속도도 느리다는 단점이 있습니다
Write Back 방식으로 해결하기
write back은 모든 write 작업을 redis에 한 후 일정 간격으로 batch를 해서
redis 서버에 저장되어 있는 데이터를 DB로 옮기는 작업을 하는 방식을 말합니다
insert 시 여러 건을 한번에 처리하기 때문에 속도가 빠르다는 장점이 있습니다
반면에 캐시 서버에만 데이터를 저장한채로 장애가 날 경우 데이터가 모두 날아갈 수 있다는 단점이 있습니다
보통 Log 데이터를 저장할 때 사용을 하며,
Write-Through 방식과 마찬가지로 TTL을 이용해 불필요한 데이터를 삭제해줘야 합니다
Write-Back 방식의 코드
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 부분은 Write-Through 구현 했을 때와 같습니다
- RMapCache 구현을 할 때 writeMode를 MapOptions.WriteMode.WRITE_BEHIND로 설정해줬습니다
- writeBehindDelay(1000)으로 설정해 1초 간격으로 배치가 이루어지게 했습니다(default가 1000)
그럼 이제 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)
@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)를 이용해 test 이후 결과가 DB에 남아있도록 했습니다
- Thread.sleep(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
Author And Source
이 문제에 관하여([Cache]Redis와 Mysql의 동기화 문제에 대한 해결책들(feat. Redisson)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@zzarbttoo/CacheRedis와-Mysql의-동기화-문제에-대한-해결책들feat.-Redisson저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)