Published using Google Docs
Redis를 사용한 분산 잠금 (SETNX, Redlock)
Updated automatically every 5 minutes

출처: https://christina04.hatenablog.com/entry/redis-distributed-locking 

개요

분산 시스템에서 동일한 리소스에 접근 할 때 잠금( 배타 제어 )하는 메커니즘을 분산 잠금이라고한다.

락을 이용하는 배경으로서는 주로 2개 있다

Redis를 분산 잠금에 사용하는 경우 주로 전자의 경우에 권장된다.

환경

레디스 6.2.0

Redis의 분산 잠금

Redis에서 분산 잠금을 실현하는 방법은 주로 두 가지가 있다.

각 사례를 설명한다.

SETNX를 사용한 분산 잠금

단일 인스턴스의 경우 SETNX를 사용한다.

func (c *Client) updateCache(ctx context.Context, contentID string ) error {
       
// Lock
       nx, err := c.cli.SetNX(ctx,
"lock:" +contentID, true , lockTTL).Result()
       
if err != nil {
               
return err
       }
       
if !nx {
               
return errors.New( "잠금 실패" )
       }
       
// 잠금 해제
       지연  
func () {
               c.cli.Del(ctx,
"잠금:" +contentID)
       }()

       
// 무거운 콘텐츠 다운로드
       
// ...

       
// 콘텐츠를 캐시로 저장
       _, err = c.cli.Set(ctx, contentID, 콘텐츠, 캐시TTL).Result()
       
if err != nil {
                return err
       }
       retu
rn nil
}

이때 반드시 EXPIRE를 붙인다. 락을 취득한 클라이언트가 죽어 버렸을 경우, 언제까지나 락이 계속 남아 버리기 때문이다.

Redlock 알고리즘을 이용한 분산 잠금

SETNX의 과제인 내결함성 분산 잠금을 실현하기 위해 Redlock 알고리즘이 탄생했다.

N개의 마스터가 있을 때 클라이언트는 모든 인스턴스에 대해 락을 취득해 가고, 과반수의 락을 취한 클라이언트는 락을 취득할 수 있다는 것이다.

구체적인 구현

자체적으로 Redlock 로직을 구현하는 것은 어렵기 때문에 기본적으로 라이브러리를 사용한다.

Redis를 사용한 분산 잠금 | 레디스

go의 경우는 go-redsync/redsync 라는 것이 있다.

func NewUniversal() *UClient {
       cli := redis.NewUniversalClient(&redis.UniversalOptions{
               Addrs: []
string{"localhost:6379"},
       })
       
return &UClient{
               cli: cli,
       }
}

func (c *UClient) updateCache(ctx context.Context, contentID string) error {
       pool := goredis.NewPool(c.cli)
       rs := redsync.New(pool)
       mu := rs.NewMutex(
"lock:"+contentID, redsync.WithExpiry(lockTTL), redsync.WithTries(1))
       err := mu.Lock()
       
if err != nil {
               
return err
       }
       
defer mu.Unlock()

       
// Download heavy content
       
// ...

       
// Save content as cache
       _, err = c.cli.Set(ctx, contentID, content, cacheTTL).Result()
       
if err != nil {
               
return err
       }
       
return nil
}

과제

Redlock은 가용성이 높은 Redis Cluster로 실현되기 때문에 SETNX 보다 정확성이 높은 잠금이라고 할 수 있지만, 정확성을 완전하게 보증할 수 있을까라고 하면 아래의 경우에 문제가 있다.

How to do distributed locking — Martin Kleppmann’s blog

이처럼 Client 1이 GC 등에 의해 장시간 포즈하는 동안 잠금이 락이 풀려(해방) 버리는 경우가 있다. 이것은 Redis의 Redlock 뿐만 아니라 잠금을 자동 해제하는 시스템 전반에서 발생할 수 있다.

대책으로서 단조 증가하는 fencing token(펜싱 토큰)을 이용하여 스토리지에 쓸 때 순서대로 쓰여졌는지 확인하는(낙관 잠금) 방법이 있다.

그러나 Redis는 펜싱 토큰을 생성하는 메커니즘이 없으며, 스스로 준비하면 펜싱 토큰을 생성하기 위한 합의 알고리즘이 필요하다 (스플릿 브레인을 일으키지 않기 때문에).

그래서 정확성을 요구하는 경우는 Zookeeper와 같이 적절한 리더 선출의 알고리즘(Paxos)을 가진 서비스를 이용하는 것이 좋다.  Zookeeper의 트랜잭션 ID인 zxid는 단조 증가이므로 펜싱 토큰 으로 사용할 수 있다.

etcd라면 revision,  Consul이라면 LockIndex 가 있으므로 이들을 사용하면 어떤 일을 실현할 수 있다고 생각한다.

혹은 Go와 같이 context를 전파하는 기구가 있으면, 리스 기한에 근거하는(혹은 그것보다 짧은) deadline을 설정하는 것으로 context timeout을 일으켜 Client A의 처리를 중단시키는 것이 가능하다.

다만 이것은 클라이언트측의 내부 시계에 의존하기 때문에, 내부 시계의 어긋남이 크면 막을 수 없다