Expiration & TTL
ทดสอบบน Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) ค่า TTL ที่แสดงคือสิ่งที่เซิร์ฟเวอร์คืนตอนรันคำสั่ง; ถ้าอ่านจาก transcript ในบทหลังจากนั้นหนึ่งวินาทีจะเห็นน้อยลงหนึ่ง
ทำไม expiration จึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไม expiration จึงสำคัญ”Redis มักถูกใช้เป็น cache หรือ session store ทั้งสองกรณีคุณอยากให้ key หายไปเองหลังเวลาหนึ่ง — ไม่ควรต้องเขียน cleanup job Redis จัดการเรื่องนี้ในตัวผ่าน key expiration
การตั้ง expiration
หัวข้อที่มีชื่อว่า “การตั้ง expiration”คุณผูก TTL (time-to-live) ได้ตอนสร้าง key หรือเพิ่มให้ key ที่มีอยู่แล้ว:
| คำสั่ง | คำอธิบาย |
|---|---|
SET key val EX secs | สร้าง + หมดอายุเป็นวินาที |
SET key val PX ms | สร้าง + หมดอายุเป็นมิลลิวินาที |
EXPIRE key secs | ตั้ง/อัปเดต expiry เป็นวินาทีบน key ที่มีอยู่ |
PEXPIRE key ms | ตั้ง/อัปเดต expiry เป็นมิลลิวินาที |
EXPIREAT key unix-secs | หมดอายุที่ Unix timestamp สัมบูรณ์ (วินาที) |
PEXPIREAT key unix-ms | หมดอายุที่ Unix timestamp สัมบูรณ์ (มิลลิวินาที) |
127.0.0.1:6379> SET session:abc "user:42" EX 300OK127.0.0.1:6379> TTL session:abc(integer) 300127.0.0.1:6379> SET cache:home "<html>...</html>"OK127.0.0.1:6379> EXPIRE cache:home 60(integer) 1127.0.0.1:6379> TTL cache:home(integer) 60127.0.0.1:6379> EXPIRE nokey 60(integer) 0อ่านค่าจริง: TTL ทันทีหลัง EX 300 คือ 300 ไม่ใช่ 299 — อีกประมาณหนึ่งวินาทีต่อมาจึงจะเห็น 299 EXPIRE คืน 1 ถ้าตั้ง timeout ได้ และ 0 ถ้า key ไม่มีอยู่
SET session:abc "user:42" EX 300
TTL session:abc
SET cache:home "<html>...</html>"
EXPIRE cache:home 60
TTL cache:homeTTL และ PTTL — ตรวจเวลาที่เหลือ
หัวข้อที่มีชื่อว่า “TTL และ PTTL — ตรวจเวลาที่เหลือ”TTL คืนวินาที; PTTL คืนมิลลิวินาที ทั้งสองมีค่าพิเศษสองค่า:
-1— key มีอยู่แต่ ไม่มี expiry (คงอยู่ถาวร)-2— key ไม่มีอยู่ (หรือหมดอายุไปแล้ว)
127.0.0.1:6379> SET persistent "I live forever"OK127.0.0.1:6379> TTL persistent(integer) -1127.0.0.1:6379> TTL nonexistent:key(integer) -2127.0.0.1:6379> SET brief "gone soon" PX 400OK127.0.0.1:6379> TTL brief(integer) 0127.0.0.1:6379> EXISTS brief(integer) 1127.0.0.1:6379> SET rounddown x PX 1400OK127.0.0.1:6379> TTL rounddown(integer) 1127.0.0.1:6379> SET roundup x PX 1600OK127.0.0.1:6379> TTL roundup(integer) 2TTL ถูกปัดเป็นวินาทีที่ใกล้ที่สุด ไม่ใช่ปัดลง และไม่ใช่นับถอยหลังลบหนึ่ง key ที่เหลือ 400 ms รายงาน TTL 0 ขณะที่ EXISTS ยังบอก 1 PX 1400 อ่านได้ 1 และ PX 1600 อ่านได้ 2 ใช้ PTTL เมื่อต้องการความแม่นยำระดับต่ำกว่าวินาที (PTTL brief อ่านได้ 278 ไม่นานหลังคำสั่งข้างบน)
SET persistent "I live forever"
TTL persistent
TTL nonexistent:key
SET brief "gone soon" PX 5000
PTTL briefPERSIST — ยกเลิก expiration
หัวข้อที่มีชื่อว่า “PERSIST — ยกเลิก expiration”ถ้าต้องการยกเลิก expiry ของ key และทำให้มันคงอยู่ถาวรอีกครั้ง ใช้ PERSIST:
127.0.0.1:6379> SET promo "SALE10" EX 3600OK127.0.0.1:6379> TTL promo(integer) 3600127.0.0.1:6379> PERSIST promo(integer) 1127.0.0.1:6379> TTL promo(integer) -1127.0.0.1:6379> PERSIST promo(integer) 0PERSIST คืน 1 เฉพาะเมื่อมี timeout ให้ลบจริง
SET promo "SALE10" EX 3600
TTL promo
PERSIST promo
TTL promoEXPIRE พร้อม NX, XX, GT, LT (Redis 7.0+)
หัวข้อที่มีชื่อว่า “EXPIRE พร้อม NX, XX, GT, LT (Redis 7.0+)”EXPIRE ธรรมดาเขียนทับ timeout เดิมไม่ว่าจะเป็นอะไร ตั้งแต่ 7.0 มี flag ทำให้การอัปเดตมีเงื่อนไข ซึ่งแก้ bug คลาสสิกหลายแบบ (“sliding window” ที่บังเอิญ ทำให้ TTL สั้นลง หรือ counter ของ rate limit ที่ window เริ่มใหม่ไม่หยุด):
| Flag | ใช้ TTL ใหม่เฉพาะเมื่อ… |
|---|---|
NX | key ยังไม่มี TTL |
XX | key มี TTL อยู่แล้ว |
GT | TTL ใหม่ มากกว่า ปัจจุบัน (key ที่ไม่มี TTL นับเป็นอนันต์ ดังนั้น GT ไม่มีผลกับมัน) |
LT | TTL ใหม่ น้อยกว่า ปัจจุบัน (ไม่มี TTL นับเป็นอนันต์ ดังนั้น LT มีผล) |
127.0.0.1:6379> EXPIRE promo 100 NX(integer) 1127.0.0.1:6379> EXPIRE promo 200 NX(integer) 0127.0.0.1:6379> EXPIRE promo 50 GT(integer) 0127.0.0.1:6379> EXPIRE promo 300 GT(integer) 1127.0.0.1:6379> EXPIRE promo 400 LT(integer) 0127.0.0.1:6379> EXPIRE promo 10 LT(integer) 1127.0.0.1:6379> EXPIRE promo 10 XX(integer) 1127.0.0.1:6379> EXPIRE promo -1(integer) 1127.0.0.1:6379> EXISTS promo(integer) 0ไล่ทีละขั้น จาก key ถาวร: NX 100 ได้ผล (เพราะยังไม่มี TTL), NX 200 ถูกปฏิเสธ (ตอนนี้มีแล้ว), GT 50 ถูกปฏิเสธ (50 < 100), GT 300 ต่ออายุ, LT 400 ถูกปฏิเสธ, LT 10 ทำให้สั้นลง, XX 10 ได้ผล สองบรรทัดสุดท้ายแสดง timeout แบบ ติดลบ: EXPIRE promo -1 คืน 1 และ key ถูกลบทันที
หากต้องการอ่านกำหนดเวลาเป็นเวลาสัมบูรณ์แทนการนับถอยหลัง ใช้ EXPIRETIME / PEXPIRETIME (7.0+): EXPIRETIME promo พิมพ์ 1790747663 (Unix วินาที — ของคุณจะต่าง), -1 สำหรับ key ถาวร และ -2 สำหรับ key ที่ไม่มี
Redis ลบ key ที่หมดอายุจริง ๆ อย่างไร
หัวข้อที่มีชื่อว่า “Redis ลบ key ที่หมดอายุจริง ๆ อย่างไร”Redis ใช้สองกลยุทธ์ที่เสริมกัน — คุณไม่ต้องทำอะไร แต่การเข้าใจช่วยให้คิดเรื่องหน่วยความจำได้ถูก:
Lazy expiry — เมื่อคุณเข้าถึง key (GET, EXISTS ฯลฯ) Redis ตรวจ expiry ก่อน ถ้าหมดอายุ Redis ลบทันทีและคืน (nil) ราวกับไม่เคยมี
Active expiry — Redis รอบการทำงานเบื้องหลัง (hz ค่าเริ่มต้น 10 ครั้งต่อวินาที — CONFIG GET hz คืน 10) สุ่มตัวอย่าง key ที่มี TTL และลบตัวที่หมดอายุ ป้องกันไม่ให้ keyspace เต็มไปด้วย key ที่ไม่เคยถูกเข้าถึง
ดูทั้งสองแบบทำงานได้จริง คำสั่ง debug DEBUG SET-ACTIVE-EXPIRE 0 (ต้องสตาร์ทด้วย --enable-debug-command yes จึงใช้เฉพาะใน container ทิ้ง) ปิดรอบ active สาม key ด้วย PX 100 แล้วครึ่งวินาทีที่ไม่มีใครแตะมัน:
127.0.0.1:6379> DEBUG SET-ACTIVE-EXPIRE 0OK127.0.0.1:6379> SET a 1 PX 100 # and b, c the same127.0.0.1:6379> DBSIZE # 500 ms later(integer) 3 # expired, but still in memory127.0.0.1:6379> INFO keyspacedb0:keys=3,expires=3,avg_ttl=0,subexpiry=0127.0.0.1:6379> GET a # lazy expiry: deleted on access(nil)127.0.0.1:6379> EXISTS b(integer) 0127.0.0.1:6379> DBSIZE(integer) 1127.0.0.1:6379> INFO stats # expired_keys:2 expired_keys_active:0127.0.0.1:6379> DEBUG SET-ACTIVE-EXPIRE 1 # 500 ms later, key c is gone by itself127.0.0.1:6379> DBSIZE(integer) 0127.0.0.1:6379> INFO stats # expired_keys:3 expired_keys_active:1expired_keys นับทุกการหมดอายุ; expired_keys_active (มีใน 8.10.2) นับตัวที่รอบเบื้องหลังลบ สองตัวถูกลบโดยการอ่าน (lazy) หนึ่งตัวโดยรอบเบื้องหลัง สองกลยุทธ์นี้รวมกันทำให้การใช้หน่วยความจำมีขอบเขตโดยไม่บล็อกแอป รอบ active จะหยุดก่อนเมื่อใช้งบ CPU หมด (INFO stats แสดง expired_time_cap_reached_count) การหมดอายุพร้อมกันเป็นจำนวนมากจึงอาจทำให้ key ที่ตายแล้วค้างในหน่วยความจำนานขึ้นเล็กน้อย
ตรวจ expiry จาก Node.js
หัวข้อที่มีชื่อว่า “ตรวจ expiry จาก Node.js”// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);const key = 'b1:tmp';
// the AI-written check: "TTL 0 means expired"const looksExpired = async (k: string) => (await redis.ttl(k)) === 0;
await redis.set(key, 'x', 'PX', 400);console.log('fresh key ttl:', await redis.ttl(key), 'looksExpired:', await looksExpired(key)); // 0 true (but it exists!)console.log('missing key ttl:', await redis.ttl('b1:missing'), 'looksExpired:', await looksExpired('b1:missing')); // -2 false
// the fix: ask about existence, and use the millisecond clock for short TTLsconst isGone = async (k: string) => (await redis.exists(k)) === 0;console.log('isGone now:', await isGone(key)); // falseawait new Promise((r) => setTimeout(r, 600));console.log('isGone 600 ms later:', await isGone(key)); // true
// EXPIRE flags: extend only (GT), set only if no TTL yet (NX)await redis.set(key, 'x', 'EX', 100);console.log(await redis.expire(key, 50, 'GT'), await redis.expire(key, 200, 'GT')); // 0 1console.log(await redis.expire(key, 10, 'NX')); // 0
await redis.del(key);await redis.quit();output จริง: fresh key ttl: 0 looksExpired: true, missing key ttl: -2 looksExpired: false, isGone now: false, isGone 600 ms later: true แล้ว 0 1 และ 0 predicate ผิด ทั้งสองทิศทาง: รายงานว่า key ที่ยังสดหมดอายุแล้ว และ key ที่ไม่มีว่าไม่เป็นไร
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”AI เขียน helper ที่ตัดสินว่ารายการใน cache ต้องสร้างใหม่หรือไม่:
🐛 หา bug ในโค้ดนี้
// returns true when the cached report has expired and must be regeneratedasync function needsRebuild(key: string): Promise<boolean> {const ttl = await redis.ttl(key);return ttl === 0;}เฉลย
TTL ไม่เคยคืน 0 สำหรับ key ที่หายไปแล้ว — มันคืน -2 และมัน คืน 0 ขณะที่ key ยังมีชีวิตอยู่และเหลือไม่ถึงครึ่งวินาที ดังนั้น needsRebuild เป็น false สำหรับ key ที่ไม่มีทุกตัว (รายงานไม่ถูกสร้างใหม่หลังหมดอายุ) และเป็น true สำหรับ key ที่กำลังจะถูกเสิร์ฟอีกไม่กี่ร้อยมิลลิวินาที เรารันกับ key PX 400 และ key ที่ไม่มี: looksExpired ได้ true และ false ตามลำดับ ตรงข้ามกับเจตนาเป๊ะ วิธีแก้คือ EXISTS (หรือ TTL === -2 หรือ PTTL) ดังใน helper isGone ข้างบน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”TTL คือ เส้นตายที่ประทับบน key ไม่ใช่นาฬิกาจับเวลาที่ key เดินเอง Redis เก็บเวลาหมดอายุแบบสัมบูรณ์; TTL แค่ลบ “ตอนนี้” ออกแล้วปัดเศษ เส้นตายถูกบังคับสองทาง: ใครก็ตามที่แตะ key หลังเส้นตายจะพบว่ามันหายไป (lazy) และตัวกวาดเบื้องหลังเดินไปหา key ที่มีเส้นตาย (active) อะไรก็ตามที่เขียน key ใหม่ (SET) ทิ้งเส้นตายไปเลย เว้นแต่จะบอก KEEPTTL
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
| ตั้ง TTL ให้ทุก key ของ cache/session | หน่วยความจำถูกคืนอัตโนมัติ ไม่ต้องเขียน cleanup job | TTL ที่คำนวณผิดอาจทำให้ข้อมูลหมดอายุก่อนที่ระบบยังต้องใช้ |
| พึ่ง lazy expiry (ตรวจตอนเข้าถึง) | ไม่มี overhead เพิ่มถ้าไม่มีใครอ่าน key นั้น | key ที่หมดอายุแล้วแต่ไม่มีใครเข้าถึงยังกินหน่วยความจำจนกว่ารอบ active จะเจอ |
ใช้ PERSIST ยกเลิก TTL ที่มีอยู่ | ต่ออายุข้อมูลสำคัญโดยไม่ต้องสร้าง key ใหม่ | ลืมง่ายว่า key นี้ตั้งใจให้ชั่วคราว ทำให้หน่วยความจำโตโดยไม่มีใครควบคุม |
EXPIRE ... NX/GT/LT (7.0+) | อัปเดตแบบมีเงื่อนไขในคำสั่ง atomic เดียว ไม่ต้อง read-modify-write | ไม่มีบน Redis < 7.0 หรือบริการ managed รุ่นเก่า; โค้ดเก่าต้องใช้ WATCH หรือ Lua |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
TTLนับถอยหลังทีละหนึ่งจากที่ตั้ง ดังนั้นEX 300อ่านได้ 299” อ่านได้300ก่อน และปัดเป็นวินาทีที่ใกล้ที่สุด - “
TTL 0แปลว่า key หายแล้ว” แปลว่าเหลือไม่ถึงครึ่งวินาที key ยังอยู่ ที่หายแล้วคือ-2 - “key หมดอายุที่เส้นตายเป๊ะ ๆ แล้วหายไป” การอ่านไม่เห็นมันหลังเส้นตาย แต่หน่วยความจำถูกคืนแบบ lazy หรือโดยตัวกวาด
- “
EXPIREต่ออายุ TTL” มัน แทนที่ TTL อาจเป็นค่าที่สั้นลง; ใช้GTเพื่อต่ออายุอย่างเดียว
Common mistakes
หัวข้อที่มีชื่อว่า “Common mistakes”- เช็ค
TTL key == 0เพื่อตรวจ key ที่ไม่มี — key ที่ไม่มีคือ-2และ0แปลว่า key ยังอยู่โดยเหลือไม่ถึงครึ่งวินาที แยกกรณีด้วย-2(ไม่มี) และ-1(ถาวร) หรือใช้EXISTS - ลืมตั้ง TTL ให้ key ที่ควรเป็นข้อมูลชั่วคราว — ถ้า
SETค่า cache หรือ session โดยไม่มีEX/PXมันจะกลายเป็นถาวรและสะสมจนmaxmemoryเต็ม - สับสนระหว่าง
EXPIRE(สัมพัทธ์) กับEXPIREAT(timestamp สัมบูรณ์) — ใช้ผิดตัวทำให้ TTL เพี้ยนจากที่ตั้งใจ โดยเฉพาะเมื่อนาฬิกา client กับเซิร์ฟเวอร์ไม่ตรงกัน
Real-world example
Stack Overflow ใช้ cache L1/L2 (หน่วยความจำในเครื่องบวก Redis) หน้า SQL Server ที่ขนาดรายงานไว้ราว 160 พันล้านการดำเนินการ Redis ต่อเดือน (Stack Overflow: The Architecture – 2016 Edition, Nick Craver, 2016 — ตัวเลขอายุสิบปี) การทำให้ค่าใน cache หมดอายุด้วย TTL ตามที่แสดงในบทนี้คือกลไกที่ป้องกันไม่ให้ cache แบบนั้นเสิร์ฟข้อมูลเก่าตลอดไป