ruby 69 lines · 2 tabs

Redis-Based Distributed Mutex (with TTL)

Shared by codesnips Jan 2026
2 tabs
require "securerandom"

class RedisMutex
  class LockError < StandardError; end

  UNLOCK_SCRIPT = <<~LUA.freeze
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end
  LUA

  def initialize(redis, key, ttl_ms: 10_000)
    @redis = redis
    @key = key
    @ttl_ms = ttl_ms
    @token = SecureRandom.hex(20)
  end

  def acquire
    @redis.set(@key, @token, nx: true, px: @ttl_ms) ? true : false
  end

  def release
    @redis.eval(UNLOCK_SCRIPT, keys: [@key], argv: [@token]) == 1
  end

  def with_lock
    return false unless acquire
    begin
      yield
      true
    ensure
      release
    end
  end
end
2 files · ruby Explain with highlit

A distributed mutex coordinates work across many processes or machines that share nothing but a Redis instance. The pattern is simple in spirit — one key represents the lock, and only the holder may release it — but the details determine whether it is actually safe. This snippet shows a minimal, correct implementation and a realistic caller.

In RedisMutex, acquisition uses SET key token NX PX ttl. The NX flag makes the write succeed only when the key is absent, giving atomic mutual exclusion, and PX attaches an expiry in milliseconds. The TTL is the crucial safety valve: if the holder crashes or its network partitions away, the lock does not leak forever — it expires and another worker can proceed. Each acquisition generates a unique token (a random hex string) so the process can later prove ownership.

The subtle bug this design avoids is releasing a lock the caller no longer owns. Without a check, a slow worker whose lock already expired could DEL a key that a different worker has since acquired, silently breaking exclusion. release therefore runs the Lua script in UNLOCK_SCRIPT, which compares the stored value to the caller's token and deletes only on a match. Running compare-and-delete as a server-side Lua script makes it atomic, closing the check-then-act race. The with_lock helper wraps acquire/release in an ensure block and yields only when the lock is held, so cleanup always runs even if the block raises.

The caller, InvoiceFinalizerJob, guards a non-idempotent side effect — finalizing an invoice exactly once. It namespaces the key per record (invoice:finalize:<id>) so unrelated invoices never contend. When with_lock returns false, another worker already holds the lock, so the job re-enqueues itself with a short delay rather than blocking a thread.

A few trade-offs are worth noting. This is a single-Redis lock, not Redlock; it assumes a reasonably reliable primary and is unsuitable when correctness must survive Redis failover. The TTL must exceed the worst-case critical-section time, or the lock can expire mid-work — for long tasks a watchdog that periodically extends the TTL is the usual remedy. It is a practical, low-ceremony choice for deduplicating jobs and serializing access to external systems.


Related snips

Share this code

Here's the card — post it anywhere.

Redis-Based Distributed Mutex (with TTL) — share card
Link copied