Spring Session Redis Master-Replica

Redis Developer Course Redis Technical Support Redis Enterprise Server

Java Program Project Configuration

구성 소프트웨어 버전

Spring Boot: 3.1.6
Spring: 6.0.14
Srping-session-data-redis:3.1.3
Lettuce: 6.2.7
Redis: 7.0.10
Java: 17

Project 생성: start.spring.io

build.gradle

dependencies에 spring-session-data-redis 가 있는지 확인한다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.session:spring-session-data-redis'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

application.properties

Standalone 설정에서 추가된 항목에서 중요한 것은 replicas.host, port와 readFrom이 추가되었다.
readFrom은 조회 명령을 복제(replica) 서버에서 실행하게 해서 부하분산을 하는 기능입니다.
Lettuce 설정에 있습니다. 여기서는 replicaPreferred으로 설정했습니다.
자세한 내용은 여기(readFrom)를 보세요.

참고

설정에서 spring.session.store-type:redis 이나 @EnableRedisHttpSession이 필요하다는 글도 있으나 현재 테스트 버전에서는 필요하지 않았다.


Java Program

Main Class: SpringSessionRedisMasterReplicaApplication

Class RedisMasterReplicaInfo

application.properties 설정 값을 읽어오는 클래스

Class RedisMasterReplicaConfig

master.host, port, replicas.host, port, readFrom 등의 정보로 redisConnectionFactory를 만든다.
application.properties에 readFrom이 없을 경우 ANY로 설정하게 했다.

Class SessionController

  • hello1(): session.setAttribute("hello1","Charlie");
  • hello2(): session.setAttribute(); 2회 실행
  • hello3(): ① 속성명 지정해서 값 가져오기. ② 모든 속성명, 값 가져오기. ③ Session ID, 생성일시, 마지막 액세스(접근) 일시 가져오기
  • hello4(): Redis HGETALL 명령으로 세션 데이터 가져오기. 메서드 명은 redisHash.entries()


Redis Master-Replica 구성도

    Master Replica config


Redis Server에서 실행되는 명령과 클라이언트 로그

Redis Server에서 실행되는 명령 확인은 redis-cli의 monitor 기능을 사용했습니다.
방법: redis-cli -h ip -p port -a password monitor

http://localhost:8080/hello1

hello1() source

레디스 서버에서 실행되는 명령

처음 실행: 새 브라우저에서 처음 실행할 때

Master (마스터)

  • 총 10 개 명령 실행: 레디스 서버 접속 관련 명령 6개 실행, Spring Session 명령 4개 실행.
  • HMSET 명령은 4개 필드 lastAccessedTime, maxInactiveInterval, creationTime, sessionAttr를 저장한다.
  • 가능한 한 줄에 보이게 하려고 'spring:session:sessions:a1ad6ded-b803-423f-a14e-52b4af3e64fe' 에서 'spring:session:sessions:' 부분을 삭제했다.
1701456142.528905 [Client:12660] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.575944 [Client:12660] "ROLE"
1701456142.611607 [Client:12665] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.621820 [Client:12665] "CLIENT" "SETNAME" "lettuce#MasterReplicaTopologyRefresh"
1701456142.629173 [Client:12665] "PING" "NODES"
1701456142.679598 [Client:12667] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.689638 [Client:12667] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "maxInactiveInterval" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer"
    "creationTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello1" "\xac\xed\x00\x05t\x00\aCharlie"
1701456142.713470 [Client:12667] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701457942547"
1701456142.835641 [Client:12667] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456142.842052 [Client:12667] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701457943459"

Replica1 (복제1)

  • 총 11 개 명령 실행: 레디스 서버 접속 관련 명령 6개 실행, 마스터에서 전파된 Spring Session 명령 5개 실행.
  • "SELECT" 명령은 마스터에서 명령이 처음 전파될때 DB를 선택하는 명령이 추가된다.
1701456142.529277 [Client:12661] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.576375 [Client:12661] "ROLE"
1701456142.609771 [Client:12664] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.621824 [Client:12664] "CLIENT" "SETNAME" "lettuce#MasterReplicaTopologyRefresh"
1701456142.630673 [Client:12664] "PING" "NODES"
1701456142.689914 [Master:18510] "SELECT" "0"
1701456142.689943 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "maxInactiveInterval" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer"
    "creationTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello1" "\xac\xed\x00\x05t\x00\aCharlie"
1701456142.713615 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701457942547"
1701456142.753148 [Client:12669] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.835792 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456142.842148 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701457943459"

Replica2 (복제2)

  • 총 20 개 명령 실행: 레디스 서버 접속 관련 명령 6개 실행, 마스터에서 전파된 Spring Session 명령 5개 실행, Client에서 실행된 명령 9개.
  • Client에서 실행된 명령 9개는 조회 명령은 복제서버에서 실행되도록 readFrom을 설정했기 때문이다.
  • "SELECT" 명령은 마스터에서 명령이 처음 전파될때 DB를 선택하는 명령이 추가된다.
1701456142.529148 [Client:12662] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.576256 [Client:12662] "ROLE"
1701456142.612773 [Client:12666] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.621951 [Client:12666] "CLIENT" "SETNAME" "lettuce#MasterReplicaTopologyRefresh"
1701456142.630825 [Client:12666] "PING" "NODES"
1701456142.689891 [Master:18510] "SELECT" "0"
1701456142.689919 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "maxInactiveInterval" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer"
    "creationTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello1" "\xac\xed\x00\x05t\x00\aCharlie"
1701456142.713600 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701457942547"
1701456142.748495 [Client:12668] "HELLO" "3" "AUTH" "(redacted)" "(redacted)" "SETNAME" "redisgate"
1701456142.757261 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.808719 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.829065 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.835802 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456142.842159 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701457943459"
1701456142.853307 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.860690 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.931101 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.938580 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.945206 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456142.956658 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"

두 번째 실행

Master (마스터)

  • 두 번째 실행할 때는 마스터에서는 2개 명령이 실행된다. 마스터 서버의 부담이 줄었다.
  • HGETALL 명령이 실행되면 HMSET 명령으로 lastAccessedTime을 수정한다. 그리고 PEXPIREAT 명령으로 만료(삭제)일시를 수정한다.   ->   HGETALL 명령은 복제2 에서 실행되었다.
  • "HMSET"과 "PEXPIREAT" 명령은 복제서버로 전파된다.
1701456374.985745 [Client:12667] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello1" "\xac\xed\x00\x05t\x00\aCharlie"
1701456374.994586 [Client:12667] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458175569"

Replica1 (복제1)

  • 마스터에서 전파된 2개 명령이 실행되었다.
1701456374.985898 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello1" "\xac\xed\x00\x05t\x00\aCharlie"
1701456374.994692 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458175569"

Replica2 (복제2)

  • 조회 명령 4개와 마스터에서 전파된 명령 2개가 순서대로 실행되었다.
  • 부하분산을 위한 readFrom 설정 효과로 조회 명령이 복제2에서 실행되었다.
1701456374.927330 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456374.937656 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456374.985901 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello1" "\xac\xed\x00\x05t\x00\aCharlie"
1701456374.994708 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458175569"
1701456375.002342 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456375.008803 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"

명령이 마스터와 복제에서 분산 실행되므로 문제가 발생할까?

마스터와 복제2에서 분산되어서 실행되는 명령을 시간 순으로 살펴보자.

  1. 927ms: 복제2 HGETALL 명령 실행
  2. 937ms: 복제2 EXISTS 명령 실행, 10ms 차이
  3. 985ms: 마스터 HMSET 명령 실행, 48ms 차이
  4. 985ms: 복제2 HMSET 명령 실행, 0ms 차이
  5. 994ms: 마스터 PEXPIREAT 명령 실행, 9ms 차이
  6. 994ms: 복제2 PEXPIREAT 명령 실행, 0ms 차이
  7. 002ms: 복제2 HGETALL 명령 실행, 8ms 차이
  8. 008ms: 복제2 EXISTS 명령 실행, 6ms 차이

명령 사이의 시간 간격은 약 10ms이고, 마스터에서 복제서버로 전파되는 시간은 1ms 이내이다.
그러므로 일련의 명령이 복제서버와 마스터에서 나누어서(분산) 실행되어도 순서가 바뀌어서 문제가 될 확률은 거의 없다.
(이 측정치는 부하(load) 정도, 서버나 네트워크 상황에 따라서 다를 수 있습니다)


http://localhost:8080/hello2

hello2() source

레디스 서버에서 실행되는 명령

Master (마스터)

  • 마스터에서는 2개 명령이 실행된다. 마스터 서버의 부담이 줄었다.
  • setAttribute()로 인해서 HMSET 명령에 sessionAttr:hello2-2, sessionAttr:hello2-1 필드와 값이 추가되었다.
1701456526.667372 [Client:12667] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello2-2" "\xac\xed\x00\x05t\x00\x06Kwon-2"
    "sessionAttr:hello2-1" "\xac\xed\x00\x05t\x00\x06Kwon-1"
1701456526.677153 [Client:12667] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458327289"

Replica1 (복제1)

  • 마스터에서 전파된 2개 명령이 실행되었다.
1701456526.667545 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello2-2" "\xac\xed\x00\x05t\x00\x06Kwon-2"
    "sessionAttr:hello2-1" "\xac\xed\x00\x05t\x00\x06Kwon-1"
1701456526.677274 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458327289"

Replica2 (복제2)

  • 부하분산을 위한 readFrom 설정 효과로 조회 명령이 복제2에서 실행되었다.
  • 조회 명령 4개와 마스터에서 전파된 명령 2개가 순서대로 실행되었다.
1701456526.647968 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456526.660285 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456526.667545 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
    "sessionAttr:hello2-2" "\xac\xed\x00\x05t\x00\x06Kwon-2"
    "sessionAttr:hello2-1" "\xac\xed\x00\x05t\x00\x06Kwon-1"
1701456526.677272 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458327289"
1701456526.688007 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456526.699060 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"

http://localhost:8080/hello3

hello3() source

레디스 서버에서 실행되는 명령

Master (마스터)

  • 마스터에서는 2개 명령이 실행된다. 마스터 서버의 부담이 줄었다.
1701456679.939033 [Client:12667] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456679.946950 [Client:12667] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458480552"

Replica1 (복제1)

  • 마스터에서 전파된 2개 명령이 실행되었다.
1701456679.939176 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456679.947071 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458480552"

Replica2 (복제2)

  • 부하분산을 위한 readFrom 설정 효과로 조회 명령이 복제2에서 실행되었다.
  • 조회 명령 4개와 마스터에서 전파된 명령 2개가 순서대로 실행되었다.
1701456679.915606 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456679.930006 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456679.939170 [Master:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456679.947046 [Master:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458480552"
1701456679.968754 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456679.977612 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"

클라이언트 로그 - 해당 java source를 같이 표시했다.

    // 속성명 지정해서 값 가져오기
    String hello1Value = (String) session.getAttribute("hello1");
    log.info("hello1 Value: {}",hello1Value);
T03:51:20.552 c.r.s.SessionController : hello1 Value: Charlie

    // 모든 속성명, 값 가져오기
    Enumeration<?> attrName = session.getAttributeNames();
    while (attrName.hasMoreElements()) {
        String attr = (String) attrName.nextElement();
        log.info("{}:{}",attr,session.getAttribute(attr));
    }
T03:51:20.552 c.r.s.SessionController : hello2-1:Kwon-1
T03:51:20.552 c.r.s.SessionController : hello1:Charlie
T03:51:20.552 c.r.s.SessionController : hello2-2:Kwon-2


    // Session ID, 생성일시, 마지막 액세스(접근) 일시 가져오기
    String sessionId = session.getId();
    log.info("sessionId: {}",sessionId);
T03:51:20.552 c.r.s.SessionController : sessionId: a1ad6ded-b803-423f-a14e-52b4af3e64fe
    Date ctime = new Date(session.getCreationTime());
    log.info("CreationTime: {}",ctime);
T03:51:20.552 c.r.s.SessionController : CreationTime: Sat Dec 02 03:42:22 KST 2023
    Date atime = new Date(session.getLastAccessedTime());
    log.info("LastAccessedTime: {}",atime);
T03:51:20.553 c.r.s.SessionController : LastAccessedTime: Sat Dec 02 03:51:20 KST 2023

http://localhost:8080/hello4

hello4() source

레디스 서버에서 실행되는 명령

Master (마스터)

  • 마스터에서는 2개 명령이 실행된다. 마스터 서버의 부담이 줄었다.

1701456867.987247 [Client:12667] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456867.998069 [Client:12667] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458668568"

Replica1 (복제1)

  • 마스터에서 전파된 2개 명령이 실행되었다.
1701456867.987388 [Master.18:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456867.998156 [Master.18:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458668568"

Replica2 (복제2)

  • 부하분산을 위한 readFrom 설정 효과로 조회 명령이 복제2에서 실행되었다.
  • 조회 명령 5개와 마스터에서 전파된 명령 2개가 순서대로 실행되었다.
  • 소스 "Map entries = redisHash.entries(springSessionId);" 의 결과로 두 번째 HGETALL이 실행되었다.
1701456867.937252 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456867.957537 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456867.967415 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456867.987389 [Master.18:18510] "HMSET" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
    "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;"
1701456867.998170 [Master.18:18510] "PEXPIREAT" "a1ad6ded-b803-423f-a14e-52b4af3e64fe" "1701458668568"
1701456868.005919 [Client:12668] "HGETALL" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"
1701456868.014797 [Client:12668] "EXISTS" "a1ad6ded-b803-423f-a14e-52b4af3e64fe"

클라이언트 로그

소스에서는 세 가지 방법으로 출력(로그)했으나 결과는 모두 같으므로 여기서는 "방법1: Lambda 사용"만 표시했다.

T03:54:28.569 c.r.s.SessionController : springSessionId: spring:session:sessions:a1ad6ded-b803-423f-a14e-52b4af3e64fe
T03:54:28.586 c.r.s.SessionController : 방법1: Lambda 사용
T03:54:28.586 c.r.s.SessionController : key:sessionAttr:hello1, value:Charlie
T03:54:28.586 c.r.s.SessionController : key:creationTime, value:java.lang.Long;
T03:54:28.586 c.r.s.SessionController : key:lastAccessedTime, value:java.lang.Long;
T03:54:28.586 c.r.s.SessionController : key:maxInactiveInterval, value:java.lang.Integer
T03:54:28.586 c.r.s.SessionController : key:sessionAttr:hello2-1, value:Kwon-1
T03:54:28.587 c.r.s.SessionController : key:sessionAttr:hello2-2, value:Kwon-2

<< Session Standalone Session Master-Replica Session Sentinel >>

Email 답글이 올라오면 이메일로 알려드리겠습니다.