Active-Active 데이터베이스의 Sorted Sets

Active-Active 데이터베이스의 Sorted Sets (Sorted sets in Active-Active databases)

지리적으로 분산된 Active-Active 데이터베이스에서 Sorted Sets를 쓸 때, 일반 Redis와 똑같을 것 같지만 사실은 조금 달라요. 여러 인스턴스가 같은 Set에 동시에 쓰다 보면 "누가 이겼지?" 하는 충돌 문제가 생기거든요. 이 페이지에서는 그 충돌이 어떻게 해결되는지, 그리고 어떤 규칙으로 동작하는지 하나씩 짚어볼게요.

출처: Redis 공식 문서 — Sorted sets in Active-Active databases

마지막까지 읽기 전에 하나만 기억해 둘게요. Redis Geospatial(Geo)은 Sorted Sets 기반으로 만들어져 있어서, 아래 내용이 Geo 명령에도 그대로 적용돼요(GEOADD 참고).

Sorted Sets 기본 동작

Redis Sorted Sets는 Set처럼 문자열의 "비중복" 모음이에요. 차이는 Set의 각 멤버에 score(점수) 가 붙는다는 점이죠. 이 score를 기준으로 오름차순으로 정렬된 상태를 유지해요. 멤버 자체는 유일(unique)하지만, 같은 score를 가지는 건 허용돼요.

이런 특징 덕분에 Sorted Sets에서는 요소를 빠르게 추가·삭제·갱신할 수 있고, score 기준 또는 rank(순위, 위치) 기준으로 범위를 가져올 수도 있어요.

Active-Active에서의 충돌 해결

Active-Active 데이터베이스의 Sorted Sets는 기본 동작은 같지만, 동시에 발생하는 충돌 쓰기를 처리하기 위한 추가 메타데이터를 유지해요. 충돌 해결은 두 단계로 진행돼요.

  1. Set 레벨 충돌 해결 – "OR Set"(Observed-Remove Set) 방식을 사용해요. 각 Active-Active 인스턴스에서 일어난 쓰기는 보통 합집합(union)으로 합쳐지는데, 충돌이 있는 경우만 예외예요. 예를 들어 한 인스턴스가 요소를 삭제하면서 다른 인스턴스가 같은 요소를 추가/갱신하면, observed Remove 규칙이 적용돼요. 즉 해당 인스턴스가 "이미 본(seen)" 요소만 삭제되고, 나머지 경우에는 Add/Update가 이겨요.
  2. Score 레벨 충돌 해결 – 이 경우에는 score 자체를 counter(카운터) 로 취급해서, 일반 카운터와 같은 방식으로 충돌을 해결해요.

아래 예시들을 보면서 실제로 어떻게 동작하는지 감을 잡아볼게요.

충돌이 없는 단순한 Sorted Set

시점 CRDB Instance 1 CRDB Instance 2
t1 ZADD Z 1.1 x
t2 — Sync — — Sync —
t3 ZADD Z 1.2 y
t4 — Sync — — Sync —
t5 ZRANGE Z 0 -1 => x y ZRANGE Z 0 -1 => x y

설명: 서로 다른 두 인스턴스가 각자 다른 요소를 추가했어요. Instance 1은 Sorted Set Z에 score 1.1의 x를, Instance 2는 score 1.2의 y를 추가했죠. 두 작업이 동시가 아니라 순차적으로(각각 동기화가 끝난 뒤에) 일어났기 때문에, 최종적으로 양쪽 인스턴스 모두 xy가 들어 있는 결과를 보여요.

동시에 같은 요소를 추가하는 경우

시점 CRDB Instance 1 CRDB Instance 2
t1 ZADD Z 1.1 x
t2 ZADD Z 2.1 x
t3 ZSCORE Z x => 1.1 ZSCORE Z x => 2.1
t4 — Sync — — Sync —
t5 ZSCORE Z x => 2.1 ZSCORE Z x => 2.1

설명: 같은 요소 x를 서로 다른 두 인스턴스가 동시에 추가했어요 (Instance 1은 1.1, Instance 2는 2.1). 이때 Active-Active 데이터베이스는 Last Write Win(LWW, 마지막 쓰기 승리) 방식으로 x의 score를 결정해요. Instance 2가 t2(t1보다 이후)에 ZADD를 실행했으므로, 최종 score는 2.1이 되는 거예요.

정확히 같은 시각에 동시 추가

시점 CRDB Instance 1 CRDB Instance 2
t1 ZADD Z 1.1 x ZADD Z 2.1 x
t2 ZSCORE Z x => 1.1 ZSCORE Z x => 2.1
t3 — Sync — — Sync —
t4 ZSCORE Z x => 1.1 ZSCORE Z x => 1.1

설명: 드문 상황이에요. 두 인스턴스가 정확히 같은 시각에 같은 요소 x를 서로 다른 score(1.1 vs 2.1)로 추가했어요. 동기화 후 시스템은 두 작업이 같은 시각에 일어났음을 인지하고, 충돌을 임의적이지만 모든 인스턴스에서 일관되게 Instance 1을 우선하는 방식으로 해결했어요. 그래서 최종적으로 양쪽 모두 1.1을 보여줘요.

동시에 카운터 증가시키기

시점 CRDB Instance 1 CRDB Instance 2
t1 ZADD Z 1.1 x
t2 — Sync — — Sync —
t3 ZINCRBY Z 1.0 x ZINCRBY Z 1.0 x
t4 — Sync — — Sync —
t5 ZSCORE Z x => 3.1 ZSCORE Z x => 3.1

설명: score를 counter로 취급하는 경우를 보여줘요. 최종 결과는 두 인스턴스가 수행한 모든 ZINCRBY이 돼요. 1.1 + 1.0 + 1.0 = 3.1이 되는 거죠.

요소 삭제하기

시점 CRDB Instance 1 CRDB Instance 2
t1 ZADD Z 4.1 x
t2 — Sync — — Sync —
t3 ZSCORE Z x => 4.1 ZSCORE Z x => 4.1
t4 ZREM Z x ZINCRBY Z 2.0 x
t5 ZSCORE Z x => nil ZSCORE Z x => 6.1
t6 — Sync — — Sync —
t7 ZSCORE Z x => 2.0 ZSCORE Z x => 2.0

설명: t4~t5 구간에서 Instance 1은 ZREM, Instance 2는 ZINCRBY를 동시에 실행했어요. 아직 동기화되기 전이므로, ZREM은 Instance 1이 "본" 데이터만 삭제할 수 있어요. 그래서 Instance 1에서는 로컬 효과인 nil이, Instance 2에서는 6.1이 보이죠. t7에서 동기화가 끝나면, 시스템은 Instance 1의 값 4.1을 Instance 2의 값 6.1에서 빼서 충돌을 해결해요. 그래서 최종 score는 2.0이 돼요.

더 알아보기 (Learn more)