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

Counters

Redis เก็บทุกอย่างเป็น string แต่รู้ได้ว่าเมื่อไรที่ string เป็นจำนวนเต็ม (หรือ float) ที่ถูกต้อง และให้คุณทำเลขคณิตกับค่านั้นได้ผ่านคำสั่งเฉพาะ จุดสำคัญคือ operation เหล่านี้เป็น atomic — Redis ทำแต่ละคำสั่งเป็นขั้นตอนเดียวที่ขัดจังหวะไม่ได้

INCR key เพิ่มค่าจำนวนเต็มขึ้นทีละ 1 ส่วน DECR key ลดลงทีละ 1 ถ้า key ไม่มีอยู่ Redis จะถือว่าค่าที่หายไปนั้นเป็น 0 ก่อนจะลงมือทำ operation

127.0.0.1:6379> SET pageviews:home 0
OK
127.0.0.1:6379> INCR pageviews:home
(integer) 1
127.0.0.1:6379> INCR pageviews:home
(integer) 2
127.0.0.1:6379> INCR pageviews:home
(integer) 3
127.0.0.1:6379> DECR pageviews:home
(integer) 2
SET pageviews:home 0
INCR pageviews:home
INCR pageviews:home
INCR pageviews:home
DECR pageviews:home

เมื่อคุณต้องการบวกหรือลบค่าอื่นที่ไม่ใช่ 1 ให้ใช้ INCRBY และ DECRBY:

127.0.0.1:6379> SET score:player1 100
OK
127.0.0.1:6379> INCRBY score:player1 50
(integer) 150
127.0.0.1:6379> DECRBY score:player1 30
(integer) 120
127.0.0.1:6379> INCRBY score:player1 -10
(integer) 110
SET score:player1 100
INCRBY score:player1 50
DECRBY score:player1 30
INCRBY score:player1 -10

สำหรับเลขคณิตแบบ floating-point (ราคา, ค่าเฉลี่ย, ข้อมูล sensor) ให้ใช้ INCRBYFLOAT ไม่มีคำสั่ง DECRBYFLOAT — แค่ส่ง delta ที่เป็นค่าลบเข้าไปแทน

127.0.0.1:6379> SET temperature 22.5
OK
127.0.0.1:6379> INCRBYFLOAT temperature 1.3
"23.8"
127.0.0.1:6379> INCRBYFLOAT temperature -5.0
"18.8"
127.0.0.1:6379> GET temperature
"18.8"
SET temperature 22.5
INCRBYFLOAT temperature 1.3
INCRBYFLOAT temperature -5.0
GET temperature

เพราะ INCR จะตั้งค่า key ที่หายไปเป็น 0 ก่อนเพิ่มค่า คุณจึงไม่จำเป็นต้อง SET 0 ก่อนเลย รวมเข้ากับ EXPIRE เพื่อทำ counter แบบ rolling-window:

127.0.0.1:6379> INCR views:article:42
(integer) 1
127.0.0.1:6379> INCR views:article:42
(integer) 2
127.0.0.1:6379> INCR views:article:42
(integer) 3
127.0.0.1:6379> GET views:article:42
"3"
INCR views:article:42
INCR views:article:42
INCR views:article:42
GET views:article:42

ติดตามว่า IP หนึ่งยิง request มากี่ครั้งภายในหนึ่ง window ใช้ INCR ร่วมกับ EXPIRE ที่ตั้งเฉพาะตอน increment ครั้งแรก:

127.0.0.1:6379> INCR ratelimit:ip:203.0.113.5
(integer) 1
127.0.0.1:6379> EXPIRE ratelimit:ip:203.0.113.5 60
(integer) 1
127.0.0.1:6379> INCR ratelimit:ip:203.0.113.5
(integer) 2
127.0.0.1:6379> TTL ratelimit:ip:203.0.113.5
(integer) 57

ใน code ของแอปพลิเคชันคุณจะเช็กว่า: ถ้าผลลัพธ์ของ INCR เกิน limit ที่ตั้งไว้ (เช่น 100) ก็ปฏิเสธ request นั้น key จะลบตัวเองอัตโนมัติหลังผ่านไป 60 วินาที เป็นการ reset window ใหม่

INCR ratelimit:ip:203.0.113.5
EXPIRE ratelimit:ip:203.0.113.5 60
INCR ratelimit:ip:203.0.113.5
TTL ratelimit:ip:203.0.113.5
ตัวเลือกBenefitCost
ใช้ INCR/INCRBY ทำ counter ใน Redisatomic โดย default ไม่ต้อง lock ฝั่ง application รองรับ throughput สูงค่าอยู่แค่ใน key เดียว ไม่มี history หรือ audit trail แบบที่ table ใน DB ให้ได้
ผูก EXPIRE เข้ากับ counter (rate limiter)ได้ rolling window ฟรี ไม่ต้องมี cleanup job ลบ counter เก่าถ้าลืมตั้ง EXPIRE ตอน increment ครั้งแรก counter จะกลายเป็น persistent ค้างอยู่ตลอดไป
ใช้ INCRBYFLOAT แทน read-modify-write ฝั่ง clientเลี่ยง race condition ได้เหมือน INCRไม่มี DECRBYFLOAT ต้องส่งค่าติดลบเอง และ floating-point อาจสะสม rounding error เล็กน้อยเมื่อบวกลบซ้ำหลายรอบ
  • อ่านค่าปัจจุบันด้วย GET แล้วบวกเองก่อนเขียนกลับด้วย SET — เปิดช่องให้เกิด race condition แบบคลาสสิกเมื่อมีหลาย client เขียนพร้อมกัน ควรใช้ INCR/INCRBY เสมอเพื่อความเป็น atomic
  • ลืมตั้ง EXPIRE ให้ counter ที่ควรเป็น rolling window (เช่น rate limiter) — ถ้าไม่ตั้ง TTL ตอน increment ครั้งแรก counter จะไม่มีวันรีเซ็ต และ key จะค้างอยู่ใน memory ตลอดไป
  • คิดว่า INCR ต้องมี key อยู่ก่อนถึงจะใช้ได้ — จริง ๆ แล้ว INCR จะตั้งค่าเริ่มต้นเป็น 0 ให้อัตโนมัติถ้า key ยังไม่มี ไม่จำเป็นต้อง SET key 0 ล่วงหน้า

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

GitHub — ใช้ counter แบบ INCR เก็บ API rate-limit ต่อ token ผูกกับ EXPIRE เพื่อ reset window อัตโนมัติโดยไม่ต้องมี cron job มาล้างข้อมูลเก่า

Reddit/YouTube-style platforms — ใช้ INCR นับ view count และ upvote count แบบ near-real-time แทนการอัปเดตแถวใน relational DB ทุกครั้งที่มีคนดู

INCR ใช้ค่าอะไรเมื่อ key ยังไม่มีอยู่?
คำสั่งใดเพิ่มค่าแบบ floating-point?
ทำไม INCR ถึงปลอดภัยสำหรับ counter ที่ใช้พร้อมกันหลายเธรด?
คุณจะลบ 5 ด้วย INCRBYFLOAT ได้อย่างไร?