ข้ามไปยังเนื้อหา

Lists เป็น Queue และ Stack

Queue จะประมวลผลข้อมูลตามลำดับ first-in, first-out คือสิ่งที่เข้ามาก่อนจะออกไปก่อน ด้วย Redis List: ฝั่ง producer ใช้ LPUSH (เพิ่มที่หัวลิสต์) และฝั่ง consumer ใช้ RPOP (ดึงจากท้ายลิสต์) เนื่องจาก LPUSH เพิ่มข้อมูลที่หัวเสมอ รายการที่เก่าที่สุดจึงอยู่ที่ท้ายเสมอ ซึ่งตรงกับตำแหน่งที่ RPOP อ่านออกมาพอดี

127.0.0.1:6379> LPUSH jobs "job:1"
(integer) 1
127.0.0.1:6379> LPUSH jobs "job:2"
(integer) 2
127.0.0.1:6379> LPUSH jobs "job:3"
(integer) 3
127.0.0.1:6379> RPOP jobs
"job:1"
127.0.0.1:6379> RPOP jobs
"job:2"

job:1 ถูก push เข้ามาก่อน จึงออกไปก่อน — นี่คือ FIFO

LPUSH jobs "job:1"
LPUSH jobs "job:2"
LPUSH jobs "job:3"
RPOP jobs
RPOP jobs

Stack จะประมวลผลข้อมูลตามลำดับ last-in, first-out คือสิ่งที่เข้ามาหลังสุดจะออกไปก่อน ทั้ง push และ pop เกิดขึ้นที่ปลายเดียวกัน (หัวของลิสต์) ดังนั้นรายการที่ push ล่าสุดจึงเป็นรายการถัดไปที่จะถูกดึงออกเสมอ

127.0.0.1:6379> DEL stack
(integer) 0
127.0.0.1:6379> LPUSH stack "frame:1"
(integer) 1
127.0.0.1:6379> LPUSH stack "frame:2"
(integer) 2
127.0.0.1:6379> LPUSH stack "frame:3"
(integer) 3
127.0.0.1:6379> LPOP stack
"frame:3"
127.0.0.1:6379> LPOP stack
"frame:2"
DEL stack
LPUSH stack "frame:1"
LPUSH stack "frame:2"
LPUSH stack "frame:3"
LPOP stack
LPOP stack

RPOPLPUSH source destination จะดึงข้อมูลจากท้ายของ source และเพิ่มไปที่หัวของ destination ในการทำงาน atomic เพียงครั้งเดียว นี่คือ pattern คลาสสิกสำหรับ reliable queue: งานจะถูกย้ายจากลิสต์ pending ไปยัง processing ในขั้นตอนเดียว จึงไม่มีทางเกิดปัญหาหากเกิด crash ขึ้นระหว่างกลาง

127.0.0.1:6379> DEL pending processing
(integer) 0
127.0.0.1:6379> RPUSH pending "task:A" "task:B" "task:C"
(integer) 3
127.0.0.1:6379> RPOPLPUSH pending processing
"task:A"
127.0.0.1:6379> LRANGE pending 0 -1
1) "task:B"
2) "task:C"
127.0.0.1:6379> LRANGE processing 0 -1
1) "task:A"

task:A อยู่ที่ท้ายของ pending (ถูก push เข้ามาก่อนด้วย RPUSH) หลังจาก RPOPLPUSH จะไปอยู่ที่หัวของ processing ส่วน pending เหลือแค่รายการที่ยังไม่ถูกหยิบ

DEL pending processing
RPUSH pending "task:A" "task:B" "task:C"
RPOPLPUSH pending processing
LRANGE pending 0 -1
LRANGE processing 0 -1

BLPOP key [key ...] timeout จะบล็อก connection ไว้จนกว่าจะมีข้อมูล แล้วจึงดึงจากลิสต์แรกที่ไม่ว่างเปล่า BRPOP ทำงานเหมือนกันแต่ดึงจากท้ายลิสต์ timeout ที่เป็น 0 หมายความว่ารอไปเรื่อยๆ ไม่มีกำหนด

127.0.0.1:6379> BLPOP jobs 5
1) "jobs"
2) "job:3"
(if jobs is empty, the client blocks for up to 5 seconds)

หมายเหตุ: BLPOP ต้องใช้กับ connection แบบ blocking สด — ทดสอบใน redis-cli ของคุณเอง

ใน worker process คุณจะรัน BLPOP jobs 0 วนซ้ำในลูป เมื่อ producer เรียก LPUSH jobs "new-job" การเรียกแบบ blocking จะ return ทันทีพร้อมกับงานใหม่ — ไม่ต้อง polling แต่อย่างใด

ตัวเลือกBenefitCost
BLPOP/BRPOP (blocking pop)ไม่ต้องเขียน polling loop เอง worker ตื่นทันทีเมื่อมีงานใหม่ต้องเปิด connection ทิ้งไว้บล็อกยาว ๆ ต่อ worker หนึ่งตัว
Redis List เป็น queueตั้งค่าง่าย latency ต่ำ ไม่ต้องติดตั้ง broker แยกไม่มี retry, dead-letter, หรือ delivery guarantee ในตัวเหมือน RabbitMQ/Kafka
RPOPLPUSH ย้ายไป processing listป้องกันงานหายถ้า worker crash ระหว่างประมวลผลต้องเขียน logic เคลียร์ processing list เองเมื่อ worker ทำงานเสร็จ
  • ใช้ RPOPLPUSH ย้ายงานไป processing list แต่ไม่เคลียร์ออกเมื่อประมวลผลเสร็จ — processing list โตขึ้นเรื่อย ๆ จนกลายเป็น memory leak
  • ใช้ Redis List queue แทน message broker เต็มรูปแบบเมื่อจำเป็นต้องมี retry, dead-letter queue หรือ delivery guarantee ที่เข้มงวด — Redis List ไม่มี feature เหล่านี้ในตัว ต้องเขียนเองทั้งหมด
  • ปล่อย unbounded list โดยไม่ LTRIM — ถ้า consumer ตามไม่ทัน producer list จะโตไม่มีขีดจำกัดจนหน่วยความจำหมด

💡 ตัวอย่างจากของจริง

Sidekiq — background job library ยอดนิยมของ Ruby ใช้ Redis List เป็น queue หลัก โดยมี worker process วน BLPOP รอรับงานใหม่แบบเดียวกับที่เรียนในบทนี้

GitHub — ใช้ Redis-backed queue (ผ่าน Sidekiq) ประมวลผล background job อย่างการส่ง notification และ webhook นับล้านครั้งต่อวัน

คำสั่งคู่ใดที่ใช้สร้าง FIFO queue?
`BLPOP jobs 0` จะทำอะไรเมื่อลิสต์ว่างเปล่า?
RPOPLPUSH ทำงานอย่างไร?
คำสั่งคู่ใดที่ใช้สร้าง stack (LIFO)?