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

การ model Object ด้วย Hashes

วิธีที่ถูกต้องในการ model domain object ใน Redis คือการเก็บทุกฟิลด์ไว้ภายใต้ hash key เดียว ตัวอย่างเช่น ข้อมูลผู้ใช้จะอยู่ทั้งหมดภายใต้ user:1:

127.0.0.1:6379> HSET user:1 name "Ada" email "[email protected]" age "28" points "0"
(integer) 4
127.0.0.1:6379> HGETALL user:1
1) "name"
2) "Ada"
3) "email"
5) "age"
6) "28"
7) "points"
8) "0"

เปรียบเทียบกับแนวทางการใช้หลาย key:

แนวทางจำนวน key ที่ต้องใช้
Hashuser:1 — หนึ่ง key, หนึ่ง HSET
String แยกuser:1:name, user:1:email, user:1:age, user:1:points — สี่ key

แนวทาง hash ใช้หน่วยความจำน้อยกว่า และช่วยให้คุณดึงหรืออัปเดตฟิลด์เฉพาะได้ด้วย HGET / HSET โดยไม่ต้องโหลดหรือแตะฟิลด์อื่นในออบเจกต์

HSET user:1 name "Ada" email "[email protected]" age "28" points "0"
HGETALL user:1

เมื่อฟิลด์ใน hash เก็บค่าตัวเลข Redis สามารถเพิ่มค่าได้แบบ atomic — ไม่มีปัญหา race condition จาก read-modify-write:

  • HINCRBY key field increment — เพิ่มค่าฟิลด์ integer ด้วยจำนวนเต็ม
  • HINCRBYFLOAT key field increment — เพิ่มค่าฟิลด์ float ด้วยตัวเลขทศนิยม
127.0.0.1:6379> HINCRBY user:1 points 10
(integer) 10
127.0.0.1:6379> HINCRBY user:1 age 1
(integer) 29
127.0.0.1:6379> HINCRBYFLOAT user:1 score 1.5
"1.5"
127.0.0.1:6379> HINCRBYFLOAT user:1 score 0.5
"2"
127.0.0.1:6379> HGET user:1 points
"10"

หากฟิลด์ยังไม่มีอยู่ ทั้งสองคำสั่งจะถือว่าค่าเริ่มต้นคือ 0 ก่อนจะนำค่า increment ไปบวก ดังนั้น HINCRBY user:1 points 5 บนฟิลด์ points ที่ไม่มีอยู่จะสร้างฟิลด์ใหม่ด้วยค่า 5

HSET user:1 name "Ada" email "[email protected]" age "28" points "0"
HINCRBY user:1 points 10
HINCRBY user:1 age 1
HINCRBYFLOAT user:1 score 1.5
HINCRBYFLOAT user:1 score 0.5
HGET user:1 points

HSETNX key field value ตั้งค่าฟิลด์ เฉพาะเมื่อฟิลด์นั้นยังไม่มีอยู่เท่านั้น คำสั่งจะคืนค่า 1 ถ้าสร้างฟิลด์สำเร็จ หรือ 0 ถ้าฟิลด์มีอยู่แล้วและไม่ได้เปลี่ยนแปลง

127.0.0.1:6379> HSETNX user:1 email "[email protected]"
(integer) 0
127.0.0.1:6379> HGET user:1 email
127.0.0.1:6379> HSETNX user:1 referrer "bob"
(integer) 1
127.0.0.1:6379> HGET user:1 referrer
"bob"

กรณีใช้งานจริง: บันทึกฟิลด์ created_at เพียงครั้งเดียวเมื่อสร้าง object ครั้งแรก เนื่องจาก HSETNX ทำงานแบบ atomic จึงมั่นใจได้ว่าถ้ามี writer สองตัวแข่งกัน จะมีเพียงตัวแรกเท่านั้นที่ได้รับ 1

HSET user:1 name "Ada" email "[email protected]"
HSETNX user:1 email "[email protected]"
HGET user:1 email
HSETNX user:1 referrer "bob"
HGET user:1 referrer
Hash (หนึ่ง key)หลาย string key
หน่วยความจำกะทัดรัด (listpack สำหรับ hash ขนาดเล็ก)overhead ต่อ key สูงกว่า
ดึงหนึ่งฟิลด์HGET user:1 nameGET user:1:name
ดึงทุกฟิลด์HGETALL user:1MGET user:1:name user:1:email ...
เพิ่มค่าแบบ atomicHINCRBYINCR user:1:points
TTL ต่อฟิลด์ไม่สามารถทำได้ได้ (แต่ละ key มี TTL ของตัวเอง)

กฎทั่วไป: เลือกใช้ hash หนึ่งอันต่อหนึ่ง object เสมอ ใช้ string key แยกเฉพาะเมื่อต้องการกำหนด TTL แยกต่างหากในแต่ละฟิลด์

ตัวเลือกBenefitCost
Hash เดียวต่อหนึ่ง objectหน่วยความจำน้อยกว่า อัปเดตทีละ field ได้ด้วย HGET/HSET โดยไม่แตะ field อื่นตั้ง TTL แยกต่อ field ไม่ได้ ต้องหมดอายุทั้ง hash พร้อมกัน
HINCRBY/HINCRBYFLOAT สำหรับ field ตัวเลขเพิ่มค่าแบบ atomic ไม่มี race condition จาก read-modify-writeใช้ได้เฉพาะ field ที่เป็นตัวเลขเท่านั้น field อื่นต้องใช้ HSET ตามปกติ
HSETNX สำหรับ field แบบมีเงื่อนไขรับประกันว่ามีเพียง writer ตัวแรกเท่านั้นที่ set ค่าได้ เหมาะกับ created_atไม่สามารถอัปเดตค่าที่มีอยู่แล้วได้ ต้องใช้ HSET ธรรมดาถ้าต้องการ overwrite
  • แยก field ของ object เดียวออกเป็นหลาย string key เช่น user:1:name, user:1:email — เปลืองหน่วยความจำมากกว่า ควรรวมเป็น hash เดียวเสมอ ยกเว้นต้องการ TTL แยกต่อ field จริง ๆ
  • ใช้ HSET ธรรมดาแทน HINCRBY เมื่ออัปเดต field ตัวเลข — ต้อง HGET ค่าก่อนแล้วค่อย HSET กลับ เปิดช่องให้เกิด race condition เมื่อมี writer พร้อมกันหลายตัว
  • ลืมว่า HSETNX ไม่ overwrite ค่าที่มีอยู่แล้ว — เข้าใจผิดว่าเป็น “set หรือ update” ทั้งที่จริง HSETNX ทำงานเฉพาะตอนที่ field ยังไม่มีอยู่เท่านั้น

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

GitHub — เก็บ metadata ของ object อย่าง repository stats เป็น hash เดียวต่อหนึ่ง object ใช้ HINCRBY อัปเดตตัวนับ เช่น star count แบบ atomic โดยไม่ต้องล็อกฝั่ง application

Instagram — ใช้ hash เก็บ field ของ media object (like count, view count) รวมกันภายใต้ key เดียว ลด round-trip เมื่อต้องอ่านหลาย field พร้อมกันสำหรับหน้า feed

`HINCRBY user:1 points 5` จะทำอะไรถ้าฟิลด์ `points` ยังไม่มีอยู่?
`HSETNX user:1 email '[email protected]'` จะคืนค่าอะไรถ้าฟิลด์มีอยู่แล้ว?
แนวทางใดใช้หน่วยความจำน้อยกว่าในการเก็บข้อมูลผู้ใช้ใน Redis?
คำสั่งใดเพิ่มค่าฟิลด์ float ใน hash แบบ atomic?