본문 바로가기

Redis

[Redis] Transaction으로 갱신손실 문제 해결하기

갱신손실 문제 예시

Redis는 기본적으로 원자적으로 동작하지만, 사용에 따라 여러가지 문제가 발생할 수 있습니다.

(단, 이 글에서는 레디스에서 제공하는 원자적으로 동작하는 set ... get 등의 메소드를 활용할 수 없는 상황이라 가정하겠습니다.)

 

아래표는 조회하고 업데이트하는 예시입니다. 이 트랜잭션의 결과는 어떻게 될까요?

  redis client 1 redis client 2
1 GET 1 -> 1 이 조회됨.  
2   GET 1 -> 1 이 조회됨
3 도메인 조건 확인 도메인 조건 확인
4 SET 1 3  
5   SET 1 4

실제로 실행해보면, redis client 1 에서 업데이트한 3은 사라진것을 알 수 있습니다.

Redis의 각 명령이 원자적으로 동작한다는것이 클라이언트의 요청 sequence도 원자적으로 동작한다는것을 뜻하지는 않습니다. 따라서, 위 결과와 같은 상황이 발생하는것 입니다.


해결방법

다행히도, 위 와 같은 상황을 방지하기 위해 Redis는 트랜잭션을 지원합니다.

 

MULTI

커넥션에 대해서, 트랜잭션 시작을 표시합니다.

이 명령어 다음으로 입력된 명령어들은 모두 Queue에 적재됩니다.

 

https://redis.io/commands/multi/

 

WATCH key [key ...]

입력된 key에 대해서, 하나의 트랜잭션에서만 수정할 수 있도록 허용합니다.

WATCH가 활성된 경우 내부적으로, 낙관적 락을 통해서 구현 되게 됩니다. (이때, 내부에서 생성한 해쉬로 버저닝을 하게 됩니다. 이 버저닝은 낙관적 락 구현에 사용됩니다.)

 

UNWATCH 명령어나, EXEC, DISCARD가 호출되면 자동으로 WATCH 상태는 풀리게 됩니다.

 

https://redis.io/commands/watch/

 

EXEC

MULTI 명령어에 의해 Queue에 적재된 명령들을 묶어서 원자적으로 실행합니다.

 

주의할점은, 큐에 적재된 명령의 수행시간 만큼 시간이 걸리므로 이 명령이 끝나기 전 까지 다른 클라이언트의 요청은 실행될 수 없으며, Queue의 사이즈가 커지고, 각 명령의 시간복잡도가 높다면, 성능에 악영향을 미칠 수 있습니다.

또한, O(Queue.size) 만큼의 시간이 걸리는것이 아닌, 큐에 적재된 명령의 수행시간 만큼의 시간이 걸리는것을 반드시 주의해야합니다.

 

공식 문서에서도 @slow 표시가 되어있고, Queue 사이즈에 따라서 성능이 달라진다고 표시되어 있습니다.

 

https://redis.io/commands/exec/

 

DISCARD-O(N), UNWATCH-O(1) 도 있는데, 따로 다루지는 않겠습니다 :)

 


실제 적용

상황 1.

그렇다면, client-1과 client-2가 동시에 트랜잭션을 수행하는데, 한쪽에서만 WATCH를 하게되면 어떻게 될까요?

redis client 2만 WATCH 하고,
redis client 2 먼저 EXEC
redis client 1 redis client 2
1   GET 1
2 GET 1 도메인 제약사항 검증
3 도메인 제약사항 검증 WATCH 1
4 MULTI  
5   MULTI
6   SET 1 100
7 SET 1 101  
8   EXEC
9 EXEC  

 

여전히 갱신손실의 문제가 발생합니다.

위 사진을 보면 client2 가 변경한 100이 응답되고 client 1의 트랜잭션은 실패함을 원했는데, 101이 반환되며 갱신손실의 문제가 여전히 발생하고 있습니다.

 

이유는 2번 트랜잭션이 먼저 EXEC 되면서, WATCH가 UNWATCH 되었기 때문입니다. (공식 문서에 따르면, EXEC, DISCARD 되면, 자동으로 UNWATCH 된다고 나와있습니다!)

상황 2.

이번에는 client-1과 client-2모두 WATCH하고 트랜잭션을 하는 이상적인 상황입니다. 이번에는 테스트 해보지 않아도, 갱신손실이 발생하지 않을것 같지만, 혹시 모르니 테스트도 해보겠습니다.

redis client 1, 2 WATCH 하고,
redis client 2 먼저 EXEC
redis client 1 redis client 2
1   GET 1
2 GET 1 도메인 제약사항 검증
3 도메인 제약사항 검증 WATCH 1
4 WATCH 1  
5 MULTI  
6   MULTI
7   SET 1 100
8 SET 1 101  
9   EXEC
10 EXEC  

갱신손실 문제가 발생하지 않았습니다

이번에는 갱신손실 문제가 발생하지 않았는데, 2번 트랜잭션이 먼저 EXEC 되더라도, 1번 트랜잭션에 의해서 여전히 WATCH 된 상태이기 때문입니다.

 

상황 3.

이번에도 상황1과 마찬가지로, 한쪽에서만 WATCH를 하지만, EXEC 시점이 다른 경우입니다. WATCH와 같은 커넥션의 EXEC가 WATCH를 풀어버린다는 점에서 이번 상황에서는 갱신손실이 발생하지 않을것 같지만.. 테스트 해보겠습니다. 

redis client 2만 WATCH 하고,
redis client 1 먼저 EXEC
redis client 1 redis client 2
1   GET 1
2 GET 1 도메인 제약사항 검증
3 도메인 제약사항 검증 WATCH 1
4 MULTI  
5   MULTI
6   SET 1 100
7 SET 1 101  
8 EXEC  
9   EXEC

마찬가지로 갱신손실 문제가 발생하지 않았습니다.

상황 1번에서 EXEC 하는 순서만 바꿨는데 갱신손실 문제가 발생하지 않았습니다. 그 이유는 트랜잭션 2 가 EXEC 하는 시점에 여전히 1 번 KEY는 WATCH 되고 있고 이미 트랜잭션 1 에 의해서 변경되었기 때문입니다.

하지만, 서로 다른 스레드 끼리의 명령의 순서를 보장하기는 어렵기 때문에 이 방식또한 안전하다고 볼수는 없을것 같습니다.

 

 

'Redis' 카테고리의 다른 글

[Redis] Sentinel failover 테스트  (1) 2023.10.15
[Redis] Cluster 환경에서 Transaction 에러 해결하기  (0) 2023.10.11