Skip to content
Published on

分散ロック(Distributed Lock)パターン比較:Redis Redlock vs ZooKeeper vs etcd — 整合性と可用性のトレードオフ

Authors
分散ロックパターン比較

はじめに

分散システムで複数のプロセスが同一リソースに同時にアクセスすると、データ整合性が壊れる。在庫引き当て、決済処理、ファイル書き込みなど**相互排他(mutual exclusion)が必要な作業は、必ず一つのプロセスだけが実行しなければならない。単一サーバーならミューテックスやセマフォで解決できるが、複数サーバーに分散されたプロセス間では分散ロック(Distributed Lock)**が必要だ。

分散ロックの用途は2つに区分される:

  • 効率性(Efficiency):同じ作業の重複実行を防ぐ目的。ロックがたまに失敗してもコストが無駄になるだけで、データは損傷しない。
  • 整合性(Correctness):同時アクセスによるデータ損傷を防ぐ目的。ロックが失敗するとデータが汚染されるため、はるかに厳格な保証が必要だ。

効率性目的なら Redis単一インスタンスロックで十分だ。しかし整合性目的ならFencing Tokenが必ず伴わなければならず、この違いがRedis Redlock論争の核心だ。

Redis単一インスタンスロック

最も簡単な分散ロックはRedisのSET NX PXコマンドを使用する方式だ。NX(Not eXists)オプションでキーがない時のみ設定し、PXでミリ秒単位の有効期限を指定する。

基本実装

import redis
import uuid
import time


class RedisSimpleLock:
    """Redis単一インスタンスベースの分散ロック"""

    def __init__(self, client: redis.Redis, resource: str, ttl_ms: int = 10000):
        self.client = client
        self.resource = resource
        self.ttl_ms = ttl_ms
        self.lock_value = str(uuid.uuid4())  # 所有権識別子

    def acquire(self, retry_count: int = 3, retry_delay_ms: int = 200) -> bool:
        """ロック獲得を試行。失敗時にリトライ。"""
        for attempt in range(retry_count):
            result = self.client.set(
                self.resource,
                self.lock_value,
                nx=True,
                px=self.ttl_ms
            )
            if result:
                return True
            time.sleep(retry_delay_ms / 1000.0)
        return False

    def release(self) -> bool:
        """所有権検証後にロック解放(Luaスクリプト使用)"""
        lua_script = """
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
        """
        result = self.client.eval(lua_script, 1, self.resource, self.lock_value)
        return result == 1


# 使用例
client = redis.Redis(host="localhost", port=6379)
lock = RedisSimpleLock(client, "order:12345:lock", ttl_ms=5000)

if lock.acquire():
    try:
        # クリティカルセクション作業実行
        print("ロック獲得成功、作業実行中...")
    finally:
        lock.release()
else:
    print("ロック獲得失敗")

Luaスクリプトベースの安全なロック解放

ロック解放時には必ず所有権を検証しなければならない。GETとDELを別々のコマンドで実行すると、その間に他のクライアントがロックを獲得する可能性がある。LuaスクリプトはRedisでアトミックに実行されるため、この問題を防止する。

-- safe_unlock.lua
-- 所有権検証後の安全なロック解放
-- KEYS[1]: ロックキー
-- ARGV[1]: 所有者識別値
-- 戻り値: 1 (成功), 0 (所有権不一致)

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

単一インスタンス方式の限界は明確だ。Redisマスターが障害を起こすとロック情報が消失する。レプリカへのフェイルオーバーが行われても、非同期レプリケーションの特性上、ロックキーがレプリケーションされる前にマスターがダウンすると、2つのクライアントが同時に同じロックを獲得する状況が発生する。

Redlockアルゴリズム

Redis創設者のSalvatore Sanfilippo(antirez)は単一インスタンスの限界を克服するためにRedlockアルゴリズムを提案した。核心のアイデアはN個(一般的に5個)の独立したRedisマスターノードで過半数合意を得ることだ。

アルゴリズム3段階

  1. 獲得段階:現在時刻を記録した後、すべてのN個ノードに順次SET NX PXコマンドを送信する。各ノードへのタイムアウトは全体TTLよりはるかに短く設定する。
  2. 有効性検証:過半数(N/2 + 1)以上のノードでロックを獲得し、全体所要時間がTTLより短ければロック獲得成功だ。有効残余時間はTTL - 所要時間だ。
  3. 解放段階:すべてのN個ノードに無条件で解放コマンドを送信する。獲得に失敗したノードにも解放を送信し、部分的に設定されたキーをクリーンアップする。

Redlock Python実装

import redis
import uuid
import time
from typing import List, Optional, Tuple


class Redlock:
    """Redlock分散ロックアルゴリズム実装"""

    CLOCK_DRIFT_FACTOR = 0.01  # クロックドリフト補正係数
    RETRY_DELAY_MS = 200
    RETRY_COUNT = 3

    def __init__(self, nodes: List[dict], ttl_ms: int = 10000):
        self.nodes = [
            redis.Redis(host=n["host"], port=n["port"], socket_timeout=0.1)
            for n in nodes
        ]
        self.quorum = len(self.nodes) // 2 + 1
        self.ttl_ms = ttl_ms

    def _acquire_single(self, client: redis.Redis, resource: str,
                        value: str) -> bool:
        try:
            return bool(client.set(resource, value, nx=True, px=self.ttl_ms))
        except redis.RedisError:
            return False

    def _release_single(self, client: redis.Redis, resource: str,
                        value: str) -> None:
        lua = """
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
        """
        try:
            client.eval(lua, 1, resource, value)
        except redis.RedisError:
            pass

    def acquire(self, resource: str) -> Optional[Tuple[str, float]]:
        """ロック獲得。成功時に(lock_value, validity_time)を返却"""
        for _ in range(self.RETRY_COUNT):
            lock_value = str(uuid.uuid4())
            start_time = time.monotonic()
            acquired_count = 0

            # Step 1: すべてのノードでロック獲得を試行
            for client in self.nodes:
                if self._acquire_single(client, resource, lock_value):
                    acquired_count += 1

            # Step 2: 有効性検証
            elapsed_ms = (time.monotonic() - start_time) * 1000
            drift = self.ttl_ms * self.CLOCK_DRIFT_FACTOR + 2
            validity_time = self.ttl_ms - elapsed_ms - drift

            if acquired_count >= self.quorum and validity_time > 0:
                return (lock_value, validity_time)

            # 失敗時はすべてのノードで解放
            for client in self.nodes:
                self._release_single(client, resource, lock_value)

            time.sleep(self.RETRY_DELAY_MS / 1000.0)

        return None

    def release(self, resource: str, lock_value: str) -> None:
        """すべてのノードでロック解放"""
        for client in self.nodes:
            self._release_single(client, resource, lock_value)


# 使用例
nodes = [
    {"host": "redis1.example.com", "port": 6379},
    {"host": "redis2.example.com", "port": 6379},
    {"host": "redis3.example.com", "port": 6379},
    {"host": "redis4.example.com", "port": 6379},
    {"host": "redis5.example.com", "port": 6379},
]

redlock = Redlock(nodes, ttl_ms=10000)
result = redlock.acquire("payment:order:99999")

if result:
    lock_value, validity_ms = result
    try:
        print(f"ロック獲得成功。有効時間: {validity_ms:.0f}ms")
        # クリティカルセクション作業実行
    finally:
        redlock.release("payment:order:99999", lock_value)

この実装でtime.monotonic()を使用することが重要だ。システム時刻(time.time())はNTP補正で戻る可能性があるが、単調時計は常に前進する。またCLOCK_DRIFT_FACTORでノード間のクロックドリフトを補正する。

Redlock批判:Kleppmann vs Antirez論争

2016年にMartin Kleppmannは「How to do distributed locking」という記事でRedlockアルゴリズムの根本的な問題を指摘した。この論争は分散システムコミュニティで最も有名な技術議論の一つとなった。

Kleppmannの核心的批判

1. タイミング前提の危険性

Redlockはプロセスが中断なく高速に完了するというタイミング前提に依存する。しかし現実では以下の状況が発生する:

  • クライアントAが5つのノードのうち3つでロック獲得成功
  • GC(ガベージコレクション)pauseが発生し、数十秒間プロセスが停止
  • その間にTTLが期限切れとなりロックが解放される
  • クライアントBが同じロックを獲得して作業実行
  • クライアントAがGCから復帰し(まだロックを保持していると思い)作業実行
  • 2つのクライアントが同時にクリティカルセクション作業を実行しデータ損傷が発生

2. Fencing Tokenの不在

Kleppmannは安全な分散ロックには必ずFencing Tokenが必要だと主張した。Fencing Tokenは単調増加する番号で、リソース(例:データベース)にアクセスする際に一緒に送信する。リソース側で以前より低いトークンのリクエストを拒否すれば、期限切れのロック所有者の遅延書き込みを安全にブロックできる。RedlockにはこのようなFencing Tokenを生成するメカニズムがない。

3. ネットワーク遅延とクロックジャンプ

NTP同期失敗やVM移行でシステムクロックが突然ジャンプすると、TTL計算が無効化される。Redlockはノード間のクロックがおおむね同期されていると仮定するが、これは非同期分散システムでは保証できない。

Antirezの反論

Salvatore Sanfilippoは「Is Redlock safe?」という記事で以下のように反論した:

  • GC pauseシナリオはRedlockに限った問題ではなく、すべての分散ロックシステムに適用される
  • 合理的な運用環境ではクロックドリフトは限定的であり、CLOCK_DRIFT_FACTORで十分に補正可能
  • Fencing Tokenはリソース側のサポートが必要であり、リソースがこれをサポートするなら、それ自体で同時実行制御が可能かもしれない

比較表:Redlock賛否論点の整理

論点Kleppmann(批判)Antirez(擁護)
タイミング前提非同期システムでのタイミング前提は危険合理的な運用環境で十分に有効
GC pauseロック有効期間中にプロセス中断の可能性すべての分散システムに共通の問題
Fencing TokenRedlockでは生成不可、必ず必要リソース側サポートが前提ならロック自体が不要な可能性
クロック同期NTP障害時のクロックジャンプリスクドリフト補正係数で対応可能
推奨用途効率性目的のみ、整合性には不適切大半の実践シナリオで十分に安全

筆者の見解では、整合性が重要な場合のRedlock単独使用は推奨しない。Fencing Tokenをサポートするストレージと併用するか、ZooKeeperやetcdのように合意ベースのシステムを選択する方が安全だ。

ZooKeeper分散ロック

Apache ZooKeeperは分散システムのコーディネーションのために設計された専用サービスだ。Zab(ZooKeeper Atomic Broadcast)プロトコルにより**線形化可能性(Linearizability)**を保証し、分散ロック実装に必要なプリミティブを標準提供する。

一時順序ノードパターン

ZooKeeperの分散ロックは**一時順序ノード(Ephemeral Sequential Znode)**を活用する:

  1. クライアントが/locks/resource-name/lock-パスに一時順序ノードを作成する。
  2. ZooKeeperが自動的に順序番号を付与する(例:lock-0000000001)。
  3. 作成したノードが最小番号であればロックを獲得する。
  4. そうでなければ直前の番号のノードにWatchを設定し待機する。
  5. クライアントセッションが終了すると一時ノードが自動削除されロックが解放される。

直前のノードにのみWatchを設定することがポイントだ。もしすべての待機者が最小ノードをWatchすると、ロック解放時にすべての待機者に通知が送信される**Herd Effect(群れ効果)**が発生する。

ZooKeeper分散ロックPython実装

from kazoo.client import KazooClient
from kazoo.recipe.lock import Lock
import logging

logging.basicConfig(level=logging.INFO)


class ZooKeeperDistributedLock:
    """ZooKeeperベースの分散ロック(Kazooライブラリ活用)"""

    def __init__(self, hosts: str, lock_path: str):
        self.zk = KazooClient(hosts=hosts)
        self.zk.start()
        self.lock = Lock(self.zk, lock_path)
        self.lock_path = lock_path

    def acquire(self, timeout: float = 30.0) -> bool:
        """ロック獲得。timeout秒以内に獲得できなければFalseを返却"""
        try:
            return self.lock.acquire(timeout=timeout)
        except Exception as e:
            logging.error(f"ロック獲得失敗: {e}")
            return False

    def release(self) -> None:
        """ロック解放"""
        try:
            self.lock.release()
        except Exception as e:
            logging.error(f"ロック解放失敗: {e}")

    def get_fencing_token(self) -> int:
        """zxidをFencing Tokenとして活用"""
        data, stat = self.zk.get(self.lock_path)
        return stat.czxid  # 作成時トランザクションID(単調増加)

    def close(self) -> None:
        self.zk.stop()
        self.zk.close()


# 使用例
zk_lock = ZooKeeperDistributedLock(
    hosts="zk1:2181,zk2:2181,zk3:2181",
    lock_path="/locks/payment/order-99999"
)

if zk_lock.acquire(timeout=10.0):
    try:
        fencing_token = zk_lock.get_fencing_token()
        logging.info(f"ロック獲得、fencing token: {fencing_token}")
        # fencing_tokenとともにストレージに書き込み
        # storage.write(data, fencing_token=fencing_token)
    finally:
        zk_lock.release()

zk_lock.close()

ZooKeeperの強みは**zxid(ZooKeeper Transaction ID)**をFencing Tokenとして活用できる点だ。zxidはグローバルに単調増加するため、ストレージ側で以前のzxidの書き込みを拒否すれば、期限切れのロック所有者の遅延書き込みを安全にブロックできる。

Read-Write Lockレシピ

ZooKeeperは排他ロックだけでなくRead-Write Lockもサポートする。読み取りロックノードはread-プレフィックス、書き込みロックノードはwrite-プレフィックスを使用する。読み取りロックは前に書き込みノードがなければ獲得可能で、書き込みロックは自身が最小番号の時のみ獲得可能だ。これにより読み取り並行性を高めつつ書き込みの相互排他を保証する。

etcd分散ロック

etcdはKubernetesの状態ストアとして広く知られた分散キーバリューストアだ。Raft合意アルゴリズムを基盤として**強い一貫性(Strong Consistency)**を保証し、分散ロック実装に適したLeaseとRevisionメカニズムを提供する。

LeaseベースのTTL管理

etcdのLeaseはTTLが設定された一時トークンだ。キーバリューペアをLeaseに接続すると、Leaseが期限切れになった時にそのキーも一緒に削除される。クライアントは定期的にKeepAliveを呼び出してLeaseを更新し、クライアント障害時に更新が停止され自動的にロックが解放される。

Revision番号によるFencing Tokenの自動生成

etcdのすべてのキー変更にはグローバルに単調増加するRevision番号が付与される。このRevisionをFencing Tokenとして直接活用できることがetcd分散ロックの大きなメリットだ。別途のトークン生成メカニズムが必要ない。

etcd分散ロックPython実装

import etcd3
import threading
import logging
from typing import Optional, Tuple

logging.basicConfig(level=logging.INFO)


class EtcdDistributedLock:
    """etcd Leaseベースの分散ロック"""

    def __init__(self, host: str = "localhost", port: int = 2379,
                 ttl: int = 10):
        self.client = etcd3.client(host=host, port=port)
        self.ttl = ttl
        self.lease: Optional[etcd3.Lease] = None
        self._keepalive_thread: Optional[threading.Thread] = None
        self._stop_keepalive = threading.Event()

    def acquire(self, lock_key: str,
                timeout: float = 30.0) -> Optional[Tuple[int, int]]:
        """
        ロック獲得。成功時に(revision, lease_id)を返却。
        revisionをFencing Tokenとして使用可能。
        """
        self.lease = self.client.lease(self.ttl)
        self.lock_key = lock_key

        # トランザクションによるアトミックなロック獲得
        # キーが存在しない場合のみ作成(Compare-And-Swap)
        success, responses = self.client.transaction(
            compare=[
                self.client.transactions.create(lock_key) == 0
            ],
            success=[
                self.client.transactions.put(
                    lock_key, "locked", lease=self.lease
                )
            ],
            failure=[]
        )

        if success:
            # RevisionをFencing Tokenとして使用
            revision = responses[0].header.revision
            self._start_keepalive()
            logging.info(
                f"ロック獲得成功。revision(fencing token): {revision}"
            )
            return (revision, self.lease.id)

        logging.warning("ロック獲得失敗: すでに他のクライアントが保持中")
        self.lease.revoke()
        return None

    def _start_keepalive(self) -> None:
        """Lease自動更新スレッド開始"""
        self._stop_keepalive.clear()

        def keepalive_loop():
            while not self._stop_keepalive.is_set():
                try:
                    self.lease.refresh()
                except Exception as e:
                    logging.error(f"Lease更新失敗: {e}")
                    break
                self._stop_keepalive.wait(timeout=self.ttl / 3.0)

        self._keepalive_thread = threading.Thread(
            target=keepalive_loop, daemon=True
        )
        self._keepalive_thread.start()

    def release(self) -> None:
        """ロック解放"""
        self._stop_keepalive.set()
        if self.lease:
            try:
                self.lease.revoke()
                logging.info("ロック解放完了")
            except Exception as e:
                logging.error(f"ロック解放失敗: {e}")

    def close(self) -> None:
        self.release()
        self.client.close()


# 使用例
lock = EtcdDistributedLock(host="etcd1.example.com", ttl=15)
result = lock.acquire("locks/payment/order-99999")

if result:
    fencing_token, lease_id = result
    try:
        # fencing_token(revision)とともにストレージに書き込み
        logging.info(f"作業実行中、fencing token: {fencing_token}")
    finally:
        lock.release()

Jepsenテスト結果と注意事項

Jepsenのetcd 3.4.3テストでは、etcdのロックが特定のネットワークパーティションシナリオで安全でない可能性があることが確認された。特にリーダー変更中にLease更新が遅延すると、クライアントがまだロックを保持していると思っているが、実際にはLeaseが期限切れとなり他のクライアントがロックを獲得している可能性がある。したがって、etcdロックもFencing Tokenとともに使用することが必須だ。

3種比較分析

核心比較表

項目Redis RedlockZooKeeperetcd
合意アルゴリズムなし(独立ノード過半数)Zab(Atomic Broadcast)Raft
一貫性モデル結果整合性ベースの近似線形化可能(Linearizable)線形化可能(Linearizable)
Fencing Token未サポートzxid活用可能Revision活用可能
障害許容N/2ノード障害までN/2ノード障害までN/2ノード障害まで
ロック解放メカニズムTTL期限切れセッション期限切れ + 一時ノード削除Lease期限切れ
パフォーマンス(獲得遅延)非常に高速(1-5ms)普通(5-20ms)普通(5-15ms)
スループット高い(10K+ ops/s)普通(1-5K ops/s)普通(2-8K ops/s)
運用複雑度低い高い(専用アンサンブル運用)普通(K8s環境なら既に存在)
Watch/通知Pub/Sub(非保証)Watch(順序保証)Watch(Revisionベース)
クライアントエコシステム非常に豊富豊富豊富(特にGoエコシステム)

ユースケース別選択ガイド

Redis Redlockを選択する場合:

  • 効率性目的の重複作業防止(キャッシュウォーミング、バッチジョブなど)
  • ミリ秒単位の高速ロック獲得が必要な場合
  • Redisをすでに使用しており追加インフラを導入したくない場合
  • 間欠的な二重実行が許容されるシナリオ

ZooKeeperを選択する場合:

  • 整合性が最重要でFencing Tokenが必須な場合
  • リーダー選出、設定管理など多様なコーディネーションが必要な場合
  • Hadoop、KafkaなどZooKeeper依存システムをすでに運用中の場合
  • Read-Write Lockなど複雑なロックパターンが必要な場合

etcdを選択する場合:

  • Kubernetes環境ですでにetcdを運用中の場合
  • Fencing Token(Revision)を簡便に活用したい場合
  • gRPCベースAPIとGoエコシステムを好む場合
  • 比較的少ない運用負荷で強い一貫性を求める場合

コスト-複雑度マトリクス

基準Redis RedlockZooKeeperetcd
インフラコスト低い(Redis再活用)高い(専用クラスタ)普通(K8sに含む可能)
学習曲線低い高い普通
整合性保証弱い強い強い
デバッグ容易性高い普通普通
コミュニティサポート非常に活発成熟成長中

障害事例と復旧手順

事例1:GC Pause中のTTL期限切れによる二重ロック獲得

シナリオ:

時間 ------>

クライアントA: [ロック獲得] --- [GC pause開始] -------------------- [GC復帰、書き込み試行]
                                              TTL期限切れ
クライアントB:                          [ロック獲得] --- [書き込み完了] --- [書き込み完了]

結果: AとB両方が書き込み実行 -> データ損傷

このシナリオはRedis RedlockだけでなくすべてのTTLベースロックで発生し得る。防御パターンはFencing Tokenの活用だ:

class FencingAwareStorage:
    """Fencing Tokenを検証するストレージラッパー"""

    def __init__(self):
        self.last_token = 0
        self._lock = threading.Lock()

    def write(self, data: dict, fencing_token: int) -> bool:
        """fencing_tokenが以前の値より大きい場合のみ書き込みを許可"""
        with self._lock:
            if fencing_token <= self.last_token:
                logging.warning(
                    f"拒否: token {fencing_token} <= "
                    f"last {self.last_token}"
                )
                return False
            self.last_token = fencing_token
            # 実際の書き込みを実行
            self._do_write(data)
            logging.info(
                f"書き込み成功: token {fencing_token}"
            )
            return True

    def _do_write(self, data: dict) -> None:
        # 実際のストレージ書き込みロジック
        pass

事例2:ZooKeeperセッション期限切れによるロック喪失

シナリオ:

ネットワークパーティションによりZooKeeperクライアントとアンサンブル間の接続が切断されると、セッションタイムアウト後に一時ノードが削除されロックが解放される。この時クライアントはまだ作業を実行中の可能性がある。

防御パターン:

from kazoo.client import KazooState


def connection_listener(state):
    """ZooKeeper接続状態モニタリング"""
    if state == KazooState.SUSPENDED:
        # 接続一時中断:進行中の作業を一時停止
        logging.warning("ZK接続中断 - 作業を一時停止")
        pause_current_operations()
    elif state == KazooState.LOST:
        # セッション期限切れ:ロック喪失確定、作業即座に中断
        logging.error("ZKセッション期限切れ - ロック喪失、作業中断")
        abort_current_operations()
    elif state == KazooState.CONNECTED:
        # 再接続成功:ロック再獲得を試行
        logging.info("ZK再接続 - ロック再獲得を試行")
        reacquire_lock()


zk = KazooClient(hosts="zk1:2181,zk2:2181,zk3:2181")
zk.add_listener(connection_listener)
zk.start()

復旧手順チェックリスト

  1. 即時対応:ロック喪失検知時に現在の作業を即座に中断
  2. 状態検証:部分完了した作業のデータ整合性を確認
  3. 冪等性設計:リトライ時に同一結果を保証するよう作業を冪等に設計
  4. 補償トランザクション:部分完了状態を元に戻す補償ロジックを実行
  5. アラート送信:運用チームに二重実行の可能性をアラート

運用時の注意事項

ロックの粒度とスコープ設計

ロックのスコープはできるだけ狭く設計すべきだ。広いスコープのロックは競合を増加させスループットを低下させる。

悪い例:  /locks/orders           (すべての注文に対する単一ロック)
普通:    /locks/orders/user-123  (ユーザー単位ロック)
良い例:  /locks/orders/99999     (個別注文単位ロック)

デッドロック検知とタイムアウト戦略

分散環境では2つのプロセスが互いに相手のロックを待つデッドロックが発生し得る。防止戦略は以下の通り:

  • 固定順序獲得:複数リソースのロックが必要な場合、常に同一順序で獲得
  • タイムアウト設定:すべてのロック獲得にタイムアウトを設定し無限待機を防止
  • ロック階層化:上位リソースから下位リソースの順でロック獲得

モニタリングメトリクス

分散ロック運用時に必ず収集すべきメトリクスは以下の通り:

メトリクス説明危険閾値
lock_acquisition_time_p99ロック獲得遅延P99TTLの50%超過時
lock_contention_rateロック競合率(失敗/全体)30%超過時
lock_hold_duration_p99ロック保持時間P99TTLの80%超過時
lock_timeout_rateタイムアウト発生率5%超過時
fencing_token_reject_rateFencing Token拒否率0より大きい場合即座に調査

fencing_token_reject_rateが0より大きい場合は二重実行が発生したことを意味するため、このメトリクスは最も緊急に対応すべきだ。

結論

分散ロックは単純なAPI呼び出しではなく、整合性と可用性の間のトレードオフを理解し設計するアーキテクチャ判断だ。

  • 効率性目的ならRedis単一インスタンスロックで十分だ。シンプルで高速。
  • 効率性目的だがRedis単一障害点が心配ならRedlockを検討する。ただし整合性保証は限定的だ。
  • 整合性が重要ならZooKeeperまたはetcdを選択し、必ずFencing Tokenとともに使用する。
  • どの実装を選択しても Fencing Token + 冪等性設計 + モニタリングが完全な分散ロックの3大要素だ。

完璧な分散ロックは存在しない。重要なのは自分のシステムが要求する保証レベルを明確にし、それに合った実装を選択し、失敗シナリオに対する防御戦略を備えることだ。

参考資料