1. Home
  2. Insights
  3. Redis
  4. Redis Distributed Lock ใน Node.js จำเป็นตอนไหน และระวังอะไร
Redis

Redis Distributed Lock ใน Node.js จำเป็นตอนไหน และระวังอะไร

อธิบายว่า Redis distributed lock ในระบบ Node.js ใช้แก้ปัญหาอะไร เหมาะกับกรณีไหน และมีความเสี่ยงอะไรบ้างถ้าใช้โดยไม่เข้าใจ semantics ของ lock ในระบบ distributed

Redis Distributed Lock ใน Node.js จำเป็นตอนไหน และระวังอะไร

พอระบบเริ่มมีหลาย instance, มี worker หลายตัว, มี cron jobs หลายจุด หรือมี request ที่อาจวิ่งเข้ามาพร้อมกัน คำถามเรื่อง “ใครควรเป็นคนทำงานนี้ก่อน” จะเริ่มสำคัญขึ้นทันที

ถ้ามี payment request เดิมเข้ามาพร้อมกันสองรอบจะเกิดอะไรขึ้น
ถ้ามี worker สองตัวหยิบงานที่ไม่ควรประมวลผลซ้ำไปพร้อมกันล่ะ
ถ้ามี cron เดียวกันรันบนหลาย instance แล้วแต่ละตัวคิดว่าตัวเองควรทำ job นี้
ถ้ามี action ที่ควรเกิดได้แค่หนึ่งครั้งในช่วงเวลาหนึ่ง จะกัน race condition ยังไง

หลายทีมพอเจอปัญหาแบบนี้ก็มักนึกถึง Redis distributed lock ทันที ซึ่งไม่ผิด แต่สิ่งที่อันตรายคือ lock ในระบบ distributed ไม่ได้ง่ายแบบ lock ใน memory ของ process เดียว

เพราะทันทีที่คุณพูดว่า “lock นี้ควบคุมหลาย process หรือหลาย instance” คุณกำลังเข้าไปแตะโลกของ timing, crash, timeout, retries, clock skew และ partial failure พร้อมกัน

บทความนี้อธิบายว่า Redis distributed lock ใน Node.js จำเป็นตอนไหน ใช้เพื่ออะไร และมีข้อควรระวังอะไรบ้างก่อนเอาไปพึ่งใน production

TL;DR

สรุปให้สั้นที่สุด

distributed lock มีไว้ลดโอกาสที่หลาย process จะทำงานชนกันในเวลาเดียวกัน
แต่ lock ไม่ใช่ยาวิเศษ และไม่ควรถูกใช้แทน idempotency, unique constraints, state validation หรือ business correctness ทั้งหมด

ถ้าต้องใช้ lock จริง คุณต้องตอบให้ได้อย่างน้อยว่า

  • ถ้า lock หลุดกลางคันจะเกิดอะไรขึ้น
  • ถ้า worker ตายแล้ว lock ค้างจะ recover ยังไง
  • ถ้าอีก process เข้ามาหลัง TTL หมด แล้วงานเดิมยังไม่เสร็จ จะยอมรับได้ไหม
  • ถ้างานถูกทำซ้ำ ผลเสียคืออะไร

Distributed Lock คืออะไร

distributed lock คือกลไกที่ช่วยให้หลาย process หรือหลาย instance พยายามตกลงกันว่า ณ เวลาหนึ่ง จะมีผู้ถือสิทธิ์ทำงานบางอย่างได้เพียงรายเดียว

ถ้าเป็น process เดียวในเครื่องเดียว ปัญหานี้อาจใช้ mutex หรือ in-memory lock ธรรมดาได้ แต่พอระบบกระจายหลาย instance แล้ว in-memory lock ของ process หนึ่งจะไม่ช่วยอะไรกับอีก process ที่อยู่คนละเครื่องหรือคนละ container

Redis จึงมักถูกใช้เป็น shared coordination point สำหรับกรณีแบบนี้ เพราะทุก instance มองเห็น Redis ชุดเดียวกันได้

ปัญหาแบบไหนที่ lock พยายามแก้

ปัญหาที่ distributed lock พยายามช่วยมักเป็นแบบนี้

  • งานหนึ่งควรถูก execute ครั้งละหนึ่ง worker
  • resource หนึ่งไม่ควรถูกอัปเดตพร้อมกันจากหลายฝั่ง
  • cron job บางตัวควรรันครั้งเดียว แม้จะมีหลาย instance
  • reconciliation หรือ settlement job ไม่ควรถูกเริ่มซ้ำ
  • การสร้าง side effect สำคัญบางอย่างไม่ควรถูกยิงพร้อมกันจนชนกัน

พูดง่าย ๆ คือมันช่วยเรื่อง coordination มากกว่าเรื่อง correctness ทั้งหมดของระบบ

ทำไม Node.js ยังต้องคิดเรื่อง lock

บางคนเห็นว่า Node.js เป็น single-threaded แล้วเลยเข้าใจว่า race condition คงไม่ใช่ปัญหา แต่จริง ๆ race condition ในระบบจริงไม่ได้เกิดแค่ใน thread เดียว มันเกิดระหว่าง

  • หลาย request ที่เข้ามาใกล้กัน
  • หลาย Node.js processes
  • หลาย containers
  • หลาย workers
  • หลาย cron instances
  • หลาย services ที่คุยกับ resource เดียวกัน

ดังนั้นแม้ภายใน process เดียว event loop จะเป็นเส้นเดียว แต่ระดับระบบยังมี concurrency เต็มตัว

เมื่อไร Redis Distributed Lock ถึง “จำเป็น”

คำว่า “จำเป็น” สำคัญมาก เพราะหลายระบบใช้ lock ก่อนที่จะใช้เครื่องมือที่เหมาะกว่าด้วยซ้ำ

กรณีที่ lock มักมีเหตุผล เช่น

  • มี shared resource ที่ไม่ควรถูกประมวลผลพร้อมกัน
  • งานนั้นไม่สามารถ rely แค่ unique constraint ได้
  • เป็น coordination ระหว่างหลาย workers
  • เป็น leader election แบบง่าย ๆ สำหรับงานบางช่วง
  • มี critical section ข้ามหลาย processes จริง

ตัวอย่างเช่น

  • job ประเภท settlement ต้องรันแค่ครั้งเดียวต่อรอบ
  • worker ต้องกันไม่ให้ process document เดียวกันพร้อมกัน
  • scheduled task ที่มีหลาย replicas ต้องให้มีตัวเดียวเป็นคนทำ
  • step บางอย่างใน workflow มี side effect ที่ expensive มาก ถ้าเกิดซ้ำจะเสียหาย

เมื่อไรไม่ควรรีบใช้ lock

นี่สำคัญพอ ๆ กัน

หลายกรณีที่คนไปหยิบ lock มาใช้ จริง ๆ ควรเริ่มจากอย่างอื่นก่อน เช่น

1) ใช้ unique constraint หรือ transaction ได้อยู่แล้ว

ถ้าปัญหาคือ “record นี้ไม่ควรถูกสร้างซ้ำ” database unique constraint มักน่าเชื่อถือกว่าการใช้ lock ลอย ๆ มาก

2) ใช้ idempotency ได้ตรงกว่า

ถ้าปัญหาคือ request เดิมอาจเข้ามาซ้ำ การใช้ idempotency key มักตรงปัญหากว่าการพยายาม lock ทั้ง route

3) ใช้ queue semantics ได้อยู่แล้ว

ถ้าปัญหาคืออยากให้ job ถูกหยิบทีละตัว queue หรือ worker framework อาจมี claiming / reservation model ที่เหมาะกว่า lock แยกต่างหาก

4) ใช้ state transition guard ได้อยู่แล้ว

บางปัญหาแก้ได้ด้วยการเช็ก state ภายใน business logic เช่น “resource นี้อยู่สถานะที่ process ได้หรือไม่” โดยไม่ต้อง lock ทั้ง resource

พูดอีกแบบคือ อย่าใช้ lock เป็นทางลัดแทนการออกแบบ invariants ของระบบ

ตัวอย่างสถานการณ์ที่ lock อาจเหมาะ

สมมติคุณมี worker หลายตัวที่คอย process invoice เดียวกันจาก queue หลายแหล่ง และการ generate settlement file สำหรับ invoice นี้ห้ามเกิดพร้อมกัน

กรณีนี้ lock อาจช่วยกันไม่ให้สอง worker เข้าส่วน critical section พร้อมกัน

อีกตัวอย่างหนึ่งคือ cron job รายวันรันใน environment ที่มีหลาย replicas ถ้าไม่มี coordination ทุก replica อาจเริ่มสร้าง report หรือส่ง email batch พร้อมกันหมด

lock อาจถูกใช้เพื่อบอกว่า “รอบนี้ instance ไหนได้สิทธิ์ทำงาน”

แต่ lock ไม่ได้แปลว่างานจะถูกต้องเสมอไป

นี่เป็นจุดที่ต้องย้ำมาก

ถึงคุณจะได้ lock สำเร็จ ก็ไม่ได้แปลว่า business logic ถูกต้องแล้ว เพราะยังมีคำถามอื่นอีก เช่น

  • resource นี้ควรถูก process ตอนนี้จริงไหม
  • มีคน process ไปก่อนหน้าแล้วหรือยัง
  • ถ้า process ค้างแล้ว TTL หมด จะเกิด concurrent execution รอบใหม่ไหม
  • ถ้าได้ lock แต่เขียน database ไม่สำเร็จ จะ rollback ยังไง
  • ถ้า logic ข้างในไม่ idempotent แล้ว rerun จะเกิดอะไรขึ้น

ดังนั้น lock เป็นเพียงชั้น coordination ไม่ใช่ guarantee ของ correctness แบบครบวงจร

หลักการพื้นฐานของ Redis lock

รูปแบบพื้นฐานที่นิยมคือ

  1. พยายาม SET key value NX PX ttl
  2. ถ้าตั้งได้ แปลว่าได้ lock
  3. ทำงานใน critical section
  4. ตอนปล่อย lock ต้องเช็กว่า value ตรงกับเจ้าของเดิมก่อนค่อยลบ

แนวคิดนี้สำคัญเพราะถ้าแค่ DEL key ตรง ๆ โดยไม่เช็ก owner อาจลบ lock ของคนอื่นที่เข้ามาถือแทนแล้วหลัง TTL หมด

ทำไมต้องมี owner token

สมมติ worker A ได้ lock แล้วถือค่า token abc
จากนั้น worker A ค้างนานเกิน TTL
lock หมดอายุ
worker B ได้ lock ใหม่ด้วย token xyz

ถ้า worker A ฟื้นขึ้นมาแล้วสั่ง DEL lock_key แบบไม่เช็ก token มันอาจไปลบ lock ของ B ทั้งที่ A ไม่ใช่ owner แล้ว

นี่คือเหตุผลว่าทำไม release lock ที่ปลอดภัยกว่ามักต้องเช็ก “ฉันยังเป็นเจ้าของ lock นี้อยู่หรือไม่” ก่อนปล่อย

TTL สำคัญยังไง

distributed lock ที่ไม่มี TTL เสี่ยงมาก เพราะถ้าผู้ถือ lock ตายไปก่อนปล่อย lock, resource อาจค้างถาวร

แต่การมี TTL ก็ไม่ได้จบปัญหา เพราะถ้า TTL สั้นเกินไป งานอาจยังไม่เสร็จแต่ lock หมดก่อน ทำให้ process อื่นเข้ามาซ้อน

ดังนั้น TTL ต้องสะท้อนธรรมชาติของ critical section จริง และบางงานอาจต้องมี lock renewal หรือ heartbeat ถ้างานกินเวลานาน

ปัญหาคลาสสิกที่ต้องระวัง

1) Lock หมดอายุก่อนงานเสร็จ

อันนี้คือปัญหายอดฮิต ถ้า critical section ใช้นานกว่าที่คิด หรือ system load สูงชั่วคราว lock อาจหมดก่อนงานจบ แล้วอีก process เข้ามาทำงานซ้ำ

2) Process ตายหลังทำ side effect ไปบางส่วน

ถึง lock จะช่วยกัน concurrent execution ได้ช่วงหนึ่ง แต่ถ้า process ตายหลังทำ side effect ไปครึ่งทาง ระบบยังต้องมี recovery strategy อยู่ดี

3) นาฬิกาและ timing ไม่ได้แม่นเป๊ะเสมอ

ในระบบ distributed เรื่องเวลาไม่ใช่เรื่องเล็ก เพราะ TTL, retry และ renewal ทั้งหมดผูกกับเวลา ถ้าคิดแบบ optimistic เกินไป lock จะดูปลอดภัยกว่าความจริง

4) ใช้ lock แทน data integrity

ถ้าระบบหวังว่า “มี lock แล้วเลยไม่ต้องมี unique constraints หรือ state guards” วันหนึ่งจะพังหนัก เพราะ lock ช่วยแค่ลดโอกาสชน ไม่ได้แทน invariant ใน storage layer

Lock ต่างจาก Idempotency ยังไง

สองอย่างนี้คนชอบเอาไปแทนกัน แต่หน้าที่ต่างกันชัด

Idempotency

ตอบคำถามว่า request เดิมเข้ามาซ้ำแล้วจะไม่สร้าง side effect ซ้ำได้อย่างไร

Distributed Lock

ตอบคำถามว่าในช่วงเวลาหนึ่งจะกันไม่ให้หลาย process เข้า section เดียวกันพร้อมกันได้อย่างไร

พูดอีกแบบคือ

  • idempotency เน้น duplicate execution semantics
  • lock เน้น concurrent coordination

หลายระบบที่มี side effect สำคัญอาจต้องมีทั้งคู่ แต่ไม่ควรเอาอย่างหนึ่งไปแทนอีกอย่างหนึ่ง

Lock ต่างจาก Queue ยังไง

ถ้าคุณใช้ queue worker framework ที่มี reservation/claim model อยู่แล้ว ปัญหาเรื่อง “ใครหยิบ job นี้” อาจถูกแก้ใน queue layer ไปแล้ว

แต่ถ้าภายใน job ยังมี critical section บางอย่าง เช่น resource เดียวกันถูกกระทบจากหลาย job คนละคิว lock อาจยังมีประโยชน์อยู่

ดังนั้นอย่าใส่ lock ซ้ำซ้อนโดยไม่ถามก่อนว่าปัญหาจริงถูกแก้ที่ queue semantics ไปแล้วหรือยัง

Rate Limiting แก้ปัญหาเดียวกันไหม

ไม่เหมือนกัน

rate limiting ช่วยลดความถี่ของ requests
แต่ไม่ได้รับประกันว่า request ที่เหลือจะไม่ชนกัน

คุณอาจ limit route เหลือ 5 requests/minute แต่ 2 requests นั้นก็ยังอาจวิ่งชน resource เดียวกันพร้อมกันได้อยู่

ดังนั้น rate limiting เป็นเรื่องการควบคุมปริมาณ ส่วน lock เป็นเรื่องการควบคุม critical section

Audit Trail เกี่ยวอะไรกับ lock

ในระบบที่ใช้ lock กับ action สำคัญ การมี audit trail หรืออย่างน้อย structured logs จะช่วยมากเวลา debug ปัญหา เช่น

  • ใครได้ lock
  • lock key อะไร
  • ได้เมื่อไร
  • ปล่อยเมื่อไร
  • ทำงานสำเร็จหรือ fail
  • มี timeout หรือ renewal เกิดขึ้นไหม
  • มี worker อื่นพยายามแย่ง lock ตอนไหน

ถ้าไม่มีข้อมูลพวกนี้ ปัญหาที่เกี่ยวกับ concurrency มักตรวจสอบย้อนหลังยากมาก เพราะอาการมักมาเป็นช่วง ๆ และจับซ้ำยาก

ตัวอย่าง Redis lock แบบเริ่มต้นใน Node.js

ตัวอย่างนี้สาธิตหลักการพื้นฐานของ lock แบบ owner token + safe release

const crypto = require("crypto");
const Redis = require("ioredis");

const redis = new Redis();

async function withRedisLock(lockKey, ttlMs, work) {
  const ownerToken = `lock_${crypto.randomUUID()}`;

  const acquired = await redis.set(lockKey, ownerToken, "PX", ttlMs, "NX");

  if (acquired !== "OK") {
    throw new Error("Lock not acquired");
  }

  try {
    return await work();
  } finally {
    await releaseLock(lockKey, ownerToken);
  }
}

async function releaseLock(lockKey, ownerToken) {
  const releaseScript = `
    if redis.call("GET", KEYS[1]) == ARGV[1] then
      return redis.call("DEL", KEYS[1])
    else
      return 0
    end
  `;

  await redis.eval(releaseScript, 1, lockKey, ownerToken);
}

async function processSettlement(settlementId) {
  const lockKey = `lock:settlement:${settlementId}`;

  return withRedisLock(lockKey, 15000, async () => {
    console.log(`Processing settlement ${settlementId}`);

    await doSettlementWork(settlementId);

    console.log(`Finished settlement ${settlementId}`);
  });
}

async function doSettlementWork(settlementId) {
  await new Promise((resolve) => setTimeout(resolve, 3000));
}

processSettlement("st_1001").catch((error) => {
  console.error("Settlement processing failed:", error.message);
});

โค้ดชุดนี้กำลังช่วยอะไร

จุดสำคัญของตัวอย่างนี้มีสองเรื่อง

อย่างแรกคือมันใช้ SET ... NX PX เพื่อพยายาม acquire lock แบบ atomic
อย่างที่สองคือมันปล่อย lock ด้วย owner token ไม่ใช่ DEL ตรง ๆ

แบบนี้ช่วยลดปัญหาพื้นฐานของการลบ lock ของคนอื่นโดยไม่ตั้งใจ

แต่ต้องย้ำว่าแค่นี้ยังไม่พอสำหรับทุกกรณี production เพราะยังไม่ได้แตะเรื่อง renewal, observability, crash recovery, retry strategy หรือ business idempotency

จุดที่ต้องระวังใน production

1) อย่าคิดว่า lock ทำให้ side effect ปลอดภัยแล้ว

ถ้างานด้านในไม่ idempotent หรือไม่มี storage-level protections พอ lock พลาดครั้งเดียวอาจเกิดผลเสียใหญ่ได้

2) อย่าตั้ง TTL แบบเดาสุ่ม

TTL ควรมาจากความเข้าใจว่ากงานใช้เวลานานเท่าไรใน worst-case ไม่ใช่ดูจาก happy path อย่างเดียว

3) อย่าปล่อย lock ตรง ๆ โดยไม่เช็ก owner

นี่คือจุดผิดที่เจอบ่อยและอันตรายมากในระบบหลาย instance

4) อย่าลืมคิดว่าถ้า lock acquire ไม่ได้จะทำยังไง

จะ retry ทันที
จะ backoff
จะ return 409
จะ enqueue ต่อ
จะ skip งานนี้ไปเลย

behavior พวกนี้ต้องชัด ไม่ใช่แค่โยน error แล้วหวังว่าทุกอย่างจะดีเอง

5) อย่าเอา lock ไปห่อทั้งระบบกว้างเกินไป

ถ้า lock key ใหญ่เกิน เช่น lock ทั้ง job type แทนที่จะ lock ตาม resource จริง throughput จะตกโดยไม่จำเป็น

แล้วควรใช้ library ไหม

ถ้าจะใช้ distributed lock จริงจังใน production ส่วนใหญ่มักคุ้มกว่าที่จะใช้ library ที่ดูแล semantics ให้ดีพอระดับหนึ่ง มากกว่าประกอบเองแบบคร่าว ๆ ทุกจุด

แต่ถึงใช้ library ก็ยังต้องเข้าใจ model อยู่ดี เพราะไม่มี library ไหนแก้ business invariants ให้คุณอัตโนมัติ

สิ่งที่ library ช่วยได้มักเป็นเรื่อง

  • acquire/release semantics
  • owner token handling
  • renewal
  • retry/backoff
  • integration กับ Redis patterns ที่ปลอดภัยขึ้น

สิ่งที่ library ไม่ได้ตอบแทนคือ

  • lock key ควร granular แค่ไหน
  • ถ้า lock fail ควรทำอะไร
  • งานด้านในต้อง idempotent แค่ไหน
  • ถ้า lock พลาด ผลเสียคืออะไร

ถ้าต้องตัดสินใจจริง ควรถามอะไรบ้าง

ก่อนใช้ distributed lock ถามให้ชัดก่อนว่า

  • ปัญหานี้แก้ด้วย unique constraint ได้ไหม
  • แก้ด้วย idempotency ได้ไหม
  • queue layer แก้ไปแล้วหรือยัง
  • งานนี้ถ้าทำซ้ำจะเสียหายแค่ไหน
  • ถ้า lock หลุดกลางคันจะเกิดอะไรขึ้น
  • critical section ใช้เวลานานสุดเท่าไร
  • key ที่ lock ควรผูกกับ resource ไหน
  • ถ้า acquire ไม่ได้ จะ retry หรือยอมปล่อยผ่าน
  • มี log และ audit พอจะตามย้อนหลังหรือยัง

คำถามพวกนี้สำคัญกว่าคำถามว่า “ใช้ Redis ทำ lock ได้ไหม” เพราะเชิงเทคนิคแน่นอนว่าทำได้ แต่คำถามจริงคือ “ควรทำไหม และรู้หรือยังว่ากำลังแบกอะไรเพิ่ม”

รีวิวแนวทางนี้แบบ production-minded

Correctness

lock ช่วยลดการชนกันของงานข้ามหลาย process ได้จริง แต่ไม่ควรถูกมองว่าเพียงพอสำหรับ data correctness ทั้งหมด ต้องมี storage guarantees หรือ business guards ร่วมด้วย

Security

มันไม่ใช่ security control แบบ auth หรือ signature verification แต่เกี่ยวกับความน่าเชื่อถือของ execution flow ถ้าใช้กับ action สำคัญ การมี audit trail และ observability จะช่วยมาก

Efficiency

lock ที่ granular ดีช่วยให้ coordination มีประสิทธิภาพ แต่ lock ที่กว้างเกินจะบีบ throughput ของระบบโดยไม่จำเป็น

Error handling

สิ่งสำคัญไม่ใช่แค่ “lock ได้ไหม” แต่คือ “ถ้า lock ไม่ได้, ถ้า lock หลุด, หรือถ้างาน fail กลางทาง จะทำยังไง”

Checklist สั้น ๆ ก่อนใช้ Redis Distributed Lock ใน production

  • ตอบได้ก่อนว่าทำไม unique constraint หรือ idempotency อย่างเดียวไม่พอ
  • รู้ว่ากำลัง lock resource อะไร
  • lock key granular พอ ไม่กว้างเกินไป
  • ใช้ owner token และ safe release
  • มี TTL ที่อิง worst-case ที่สมเหตุผล
  • ถ้างานยาว มี renewal หรือ strategy ที่ชัด
  • งานด้านใน critical section ยังมี business guards ที่จำเป็น
  • กำหนด behavior ชัดเมื่อ acquire lock ไม่ได้
  • มี structured logs หรือ audit trail เกี่ยวกับ lock lifecycle
  • ทดสอบกรณี crash, timeout และ retry จริง

บทความที่ควรอ่านต่อ

สรุป

Redis distributed lock มีประโยชน์จริงในระบบที่หลาย process ต้องประสานกันไม่ให้เข้าทำงานชนกัน แต่ปัญหาที่มันแก้คือ coordination ไม่ใช่ correctness ทั้งหมดของระบบ

ถ้าใช้โดยไม่เข้าใจ semantics ของ TTL, owner, crash และ retries มันอาจให้ความรู้สึกว่าปลอดภัย ทั้งที่จริงแล้วยังมีช่องให้เกิด duplicate execution หรือ inconsistent state ได้อยู่

สรุปสั้นที่สุดคือ

ใช้ distributed lock เมื่อมีปัญหาเรื่อง coordination จริง
แต่อย่าคาดหวังให้ lock แทน idempotency, storage invariants และ business rules ทั้งหมด

💬 Chat (ตอบเร็ว)