← Redis
Architecture Mastery • Phase 3

Advanced Operations, High Availability & Production Realities

Distributed Atomicity, Sentinel Failover, 16,384 Hash Slot Sharding, Hotkey Mitigation & Banking Caching Blueprints

1. Transactions & Server-Side Atomicity: Beyond RDBMS ACID

ในฐานะวิศวกรสาย JVM/Spring Boot เรามักคุ้นเคยกับ @Transactional บน RDBMS ซึ่งอาศัย Row-Level Locks และ Transaction Isolation Levels (Read Committed / Serializable) แต่ในโลก In-Memory ของ Redis ที่ขับเคลื่อนด้วย Single-Threaded Event Loop กลไก Transaction ถูกออกแบบด้วยวิธีที่แตกต่างกันอย่างสิ้นเชิง

MULTI / EXEC / WATCH (Optimistic Locking) vs RDBMS Pessimistic Locking

คำสั่ง MULTI ใน Redis ไม่ใช่การเปิด Lock บนข้อมูล แต่เป็นเพียงการ "สั่งให้อัดคำสั่งลง Queue ฝั่ง Client/Connection Buffer" และคำสั่งทั้งหมดจะถูกยิงเข้าไปรันรวดเดียวเมื่อเรียก EXEC:

Client 1 Redis Single-Thread Event Loop | | |--- WATCH ledger:balance:acc_101 -------------------->| (Register watch key) |--- GET ledger:balance:acc_101 ---------------------->| (Return 5,000 THB) | | | (Compute new balance = 4,000 in JVM) | <--- Client 2 modifies acc_101! | | (Redis flags key as DIRTY) |--- MULTI ------------------------------------------->| (Start queueing) |--- DECRBY ledger:balance:acc_101 1000 -------------->| (QUEUED) |--- LPUSH ledger:tx:acc_101 "TXN_992" --------------->| (QUEUED) |--- EXEC -------------------------------------------->| (Checks dirty flag -> ABORT!) |<-- (nil / Empty Array) ------------------------------| (Transaction Fails: Retry required)
จุดบอดของ MULTI/EXEC ใน High Concurrency:
หากมีธุรกรรมถอนเงินเข้าสู่บัญชีเดียวกันพร้อมกัน 1,000 TPS กลไก WATCH จะล้มเหลวเกือบทั้งหมด (High Contention Abort) บังคับให้ JVM App ต้องทำ Exponential Backoff Retry ส่งผลให้เกิด Network Round-Trip และ Thread Exhaustion ใน Spring Boot ทันที

Server-Side Atomicity ด้วย Lua Scripting (EVAL / EVALSHA)

เพื่อแก้ปัญหา Round-Trip และ Concurrency Retry ทางออกระดับสถาปัตยกรรมธนาคารคือการย้าย Business Logic ไปรัน Atomic Server-Side ภายใน Event Loop ของ Redis ผ่านภาษา Lua:

-- atomic_withdraw.lua
-- KEYS[1]: ledger:balance:acc_id
-- ARGV[1]: withdraw_amount
-- ARGV[2]: txn_id

local current_bal = redis.call('GET', KEYS[1])
if not current_bal then
    return redis.error_reply("ACCOUNT_NOT_FOUND")
end

local bal_num = tonumber(current_bal)
local amount = tonumber(ARGV[1])

if bal_num < amount then
    return -1 -- INSUFFICIENT_FUNDS
end

-- Deduct balance and record transaction log atomically
redis.call('DECRBY', KEYS[1], amount)
redis.call('LPUSH', 'ledger:tx:' .. KEYS[1], ARGV[2])
return bal_num - amount
ทำไม Lua Script ถึงการันตี 100% Atomicity?
เพราะขณะที่ Redis กำลัง Execute สคริปต์ Lua Event Loop จะไม่สลับไปรันคำสั่งของ Client อื่นเลย สคริปต์จะถูกประมวลผลประหนึ่งคำสั่งคำสั่งเดียว (Single Atomic Instruction) ทำให้ปลอดภัยจาก Race Condition โดยสิ้นเชิง ปราศจากปัญหา Deadlock และไม่ต้องรัน Network Round-Trips สลับไปมา

2. High Availability: Replication & Sentinel Architecture

ในระบบการเงิน ความพร้อมใช้งาน (High Availability) และการป้องกันข้อมูลสูญหาย (Zero-Loss Tolerance) เป็นหัวใจสำคัญ การเข้าใจวงจร Failover จึงเป็นสิ่งจำเป็น

+-------------------+ | Sentinel Quorum | | (3+ Node Monitor) | +---------+---------+ | Heartbeat (PING) +------------------+------------------+ | | v v +-----------------+ +-----------------+ | Master Node | -- PSYNC Buffer ->| Replica Node | | (Active R/W) | (Replication) | (Read-Only) | +-----------------+ +-----------------+

Master-Replica Synchronization Mechanics: PSYNC vs Full Sync

Sentinel Consensus & Split-Brain Mitigation

Sentinel ทำหน้าที่เป็น Distributed Coordinator (แนะนำให้รัน 3 Nodes ขึ้นไปเพื่อให้เกิด Quorum):

  1. SDOWN (Subjective Down): Sentinel ตัวหนึ่งตัวใด PING หา Master แล้วไม่ตอบกลับภายใน down-after-milliseconds
  2. ODOWN (Objective Down): เมื่อ Sentinel ได้รับการยืนยันจาก Sentinel ตัวอื่นจนครบ Quorum (เช่น 2 ใน 3) จะยกระดับเป็นภาวะล่มจริง และทำ Leader Election ผ่าน Raft algorithm เพื่อเลือก Sentinel ตัวทำ Failover
  3. Split-Brain Protection: หากเกิด Network Partition แล้ว Master เดิมถูกตัดขาดไปอยู่ฝั่งเสียงข้างน้อย มันอาจยังรับ Write จาก Client เก่าต่อไป เพื่อป้องกันเหตุการณ์นี้ ต้องคอนฟิกใน Redis Master:
    min-replicas-to-write 1
    min-replicas-max-lag 10
    หาก Master ไม่ได้รับการยืนยัน ACK จาก Replica อย่างน้อย 1 โหนดภายใน 10 วินาที Master จะปฏิเสธการ Write ทันที ป้องกันไม่ให้เกิด Split-Brain และ Data Divergence

3. Redis Cluster Architecture: 16,384 Hash Slots & Sharding

เมื่อปริมาณ Write Throughput หรือขนาด Memory ใน RAM พุ่งเกินขีดจำกัดของเซิร์ฟเวอร์เครื่องเดียว Redis จะเปลี่ยนผ่านสู่ Multi-Master Distributed Sharding ผ่านสถาปัตยกรรม Redis Cluster

CRC16(key) % 16384 = HASH SLOT ================================================================================ Node A (Master) : Handles Slots [0 - 5460] (e.g., balance:acc_101) Node B (Master) : Handles Slots [5461 - 10922] (e.g., user:profile:883) Node C (Master) : Handles Slots [10923 - 16383] (e.g., ledger:tx:009) ================================================================================ Client Command: GET balance:acc_101 ---> CRC16("balance:acc_101") % 16384 = 1250 Client connects to Node B ---> Node B returns: "-MOVED 1250 10.0.0.1:6379" Client redirects & caches slot map ---> Direct connection to Node A

ทำไมต้องมี 16,384 Slots พอดี? (Hardware & Network Physics)

ทำไม Salvatore Sanfilippo ถึงเลือกตัวเลข 214 = 16,384 แทนที่จะเป็น 65,536 (216)?:

The Multi-Key Limitation & Hash Tags ({...}) Solution

ใน Redis Cluster คำสั่งที่ต้องแตะหลาย Keys พร้อมกัน (เช่น MGET, Transaction MULTI/EXEC, หรือ EVAL Lua) จะ ล้มเหลวทันทีด้วย Error: CROSSSLOT Keys in request don't hash to the same slot หาก Keys เหล่านั้นตกอยู่คนละเครื่อง

การบังคับ Hash Slot ด้วย Hash Tags:
หากเราครอบวงเล็บปีกกา {...} ลงในชื่อ Key Redis จะนำเฉพาะตัวอักษรข้างในปีกกามาคำนวณ CRC16 เท่านั้น!
-- ทั้งสอง Keys จะได้ค่า CRC16 จากคำว่า "acc_9871" เหมือนกัน 100%
-- ทำให้วิ่งไปตกบน Node เดียวกันเสมอ รองรับ Multi-key Atomic Lua ได้อย่างราบรื่น
KEYS[1] = "ledger:{acc_9871}:balance"
KEYS[2] = "ledger:{acc_9871}:audit_log"

4. Production Anti-Patterns & Mitigation Engineering

1. Hotkey Mitigation: เมื่อ Key เดียวรองรับ 100,000 TPS

เกิดจากข้อมูลเพียงชิ้นเดียวได้รับความนิยมสูงลิ่ว เช่น ข้อมูลสต็อกสินค้า Flash Sale หรืออัตราแลกเปลี่ยนเงินตราประจำวัน ส่งผลให้ CPU Core ของ Redis Node นั้นทำงานแตะ 100% ขณะที่โหนดอื่นว่างงาน

TIERED CACHING ARCHITECTURE (L1 Local JVM + L2 Redis) ====================================================== Incoming Request (100k TPS) | v +-------------------------------------------------------------+ | Spring Boot Application (JVM) | | +-------------------------------------------------------+ | | | L1: Caffeine Local In-Memory Cache | | | | (Near-Zero Latency ~10ns, Absorbs 95% of Hot Read) | | | +---------------------------+---------------------------+ | +-------------------------------|-----------------------------+ | L1 Cache Miss (5% Traffic) v +-------------------------------------------------------------+ | L2: Redis Cluster Distributed Cache (Absorbs remaining load)| +-------------------------------+-----------------------------+ | L2 Cache Miss (0.1% Traffic) v +-------------------------------------------------------------+ | System of Record: PostgreSQL Ledger / Core Banking Database | +-------------------------------------------------------------+

2 กลยุทธ์หลักในการทลาย Hotkey:

  1. L1 In-Process Cache (Caffeine): เก็บ Hotkey ไว้ใน JVM Heap Memory โดยตรง มีอายุสั้นๆ (TTL 1-5 วินาที) สามารถสกัดกั้น Traffic ได้มากกว่า 90% โดยไม่ให้คำขอหลุดไปยัง Redis Network Socket
  2. Random Suffix Sharding (Write-Spread): หากต้อง Write/Increment Hotkey ให้กระจาย Key ออกเป็น M ส่วน:
    val shardIndex = ThreadLocalRandom.current().nextInt(10) // 0 to 9
    val shardKey = "flash_sale:stock_item_99:$shardIndex"
    redisTemplate.opsForValue().increment(shardKey, -1)
    
    // เวลาอ่านยอดรวม: ใช้ MGET ดึงทั้ง 10 Shards มารวมผลบวกที่ JVM App

2. Big Key Diagnostics & Elimination

Big Key คือ Key ที่มีขนาดข้อมูลใหญ่เกินมาตรฐาน (เช่น String ขนาด > 5MB หรือ Hash/List/Set ที่มีสมาชิกเกิน 10,000 รายการ) ซึ่งทำให้เกิด:

เครื่องมือตรวจสอบใน Production (CLI Diagnostics):

# 1. สแกนหา Big Keys ทั่วระบบโดยไม่บล็อก Event Loop (ใช้ SCAN ภายใน)
redis-cli -h 10.0.0.1 -p 6379 --bigkeys

# 2. สแกนหา Hotkeys แบบ Real-time (ต้องเปิด maxmemory-policy LFU)
redis-cli -h 10.0.0.1 -p 6379 --hotkeys

# 3. ดูประวัติคำสั่งที่กินเวลา CPU เกินเกณฑ์ (Default: 10,000 microseconds)
SLOWLOG GET 20
Zero-Downtime Deletion: ห้ามใช้คำสั่ง DEL big_key บน Production เด็ดขาด! ให้ใช้ UNLINK big_key เสมอ เพื่อให้ Redis ปลด Key ออกจาก Keyspace ทันที แล้วส่ง Memory ไป Deallocate บน Background BIO Threads โดยไม่กระทบ Latency ของระบบ

3. Memory Eviction Policies Deep Dive

Policy Mode พฤติกรรมเมื่อ RAM ชนเพดาน maxmemory Use Case ที่เหมาะสม
noeviction ปฏิเสธทุกคำสั่ง Write พร้อม Return Error OOM command not allowed Financial Ledger, Transaction State ที่ข้อมูลห้ามหายเด็ดขาด
allkeys-lru เตะ Key ที่ไม่ได้ใช้งานนานที่สุด (Least Recently Used) ออกจากทุก Key Web App Cache ทั่วไปที่มีการเข้าถึงข้อมูลแบบ Power-law
volatile-lru เตะ Key เก่าสุดเฉพาะกลุ่มที่มีการตั้งค่า Expire (TTL) ไว้เท่านั้น เมื่อต้องการแชร์ Redis ก้อนเดียวกันระหว่าง Static Config กับ Dynamic Cache
allkeys-lfu เตะ Key ที่มีความถี่ในการเรียกใช้น้อยที่สุด (Least Frequently Used) ป้องกันปัญหา One-hit Wonder (Traffic กวาดรอบเดียวแล้วไม่มาอีก)

5. Production Blueprint: Banking Idempotent Payment Shield

สถาปัตยกรรมป้องกันการจ่ายเงินซ้ำซ้อน (Idempotent Payment Ingestion) และการหักบัญชีความเร็วสูงระดับ Core Banking โดยผสาน Redis และ PostgreSQL อย่างสมบูรณ์แบบ:

@Service
class BankingPaymentService(
    private val redisTemplate: StringRedisTemplate,
    private val ledgerRepository: PostgresLedgerRepository
) {
    // โหลด Compiled Lua Script ไว้ล่วงหน้า (ใช้ EVALSHA อัตโนมัติ)
    private val paymentScript = DefaultRedisScript(
        """
        local set_idemp = redis.call('SET', KEYS[1], 'PROCESSING', 'EX', ARGV[2], 'NX')
        if not set_idemp then
            return -1 -- ALREADY_PROCESSED_OR_IN_FLIGHT
        end
        
        local bal = redis.call('GET', KEYS[2])
        if not bal then
            return -2 -- ACCOUNT_CACHE_MISS
        end
        
        local current_bal = tonumber(bal)
        local deduct = tonumber(ARGV[1])
        if current_bal < deduct then
            return -3 -- INSUFFICIENT_FUNDS
        end
        
        redis.call('DECRBY', KEYS[2], deduct)
        return current_bal - deduct
        """.trimIndent(), Long::class.java
    )

    @Transactional
    fun processPayment(requestId: String, accountId: String, amount: Long): PaymentResult {
        val keys = listOf("idemp:{$accountId}:$requestId", "balance:{$accountId}")
        val result = redisTemplate.execute(paymentScript, keys, amount.toString(), "300")

        return when (result) {
            -1L -> throw DuplicateRequestException("Duplicate transaction: $requestId")
            -3L -> throw InsufficientBalanceException("Balance insufficient in account $accountId")
            -2L -> {
                // Cache Miss: โหลดจาก PostgreSQL Buffer Pool แล้ว Hydrate กลับเข้า Redis
                hydrateCacheAndRetry(accountId, requestId, amount)
            }
            else -> {
                // Success In-Memory: ยิง Event เข้า Kafka Transactional Outbox ส่งต่อให้ RDBMS Persistence
                persistLedgerAsync(accountId, amount, requestId)
                PaymentResult.Success(remainingBalance = result)
            }
        }
    }
}
Key Architectural Highlights ของ Blueprint นี้:
  • ใช้ Hash Tags {$accountId} บังคับให้ Idempotency Key และ Balance Key ตกอยู่บน Cluster Hash Slot เดียวกัน ทำให้รัน Atomic Lua Script ข้าม Keys ได้อย่างถูกต้อง
  • ใช้ SET NX EX ผสานกับ Balance Verification ในเสี้ยวไมโครวินาที ป้องกัน Race Condition และ Duplicate Payment ในระดับ Hardware Single Thread
  • ลดภาระ Disk I/O ของ PostgreSQL ลงกว่า 98% โดยใช้ Redis เป็นตัวสกัดกั้น Transaction Concurrency และปล่อยให้ RDBMS บันทึก Financial Ledger ถาวรผ่าน Asynchronous Event Stream

6. Phase 3 Architectural Assessment Bank (25 Production Realities Items)

ชุดข้อสอบประเมินสถาปัตยกรรมระดับลึกสำหรับ Phase 3 เพื่อทดสอบความเข้าใจเรื่อง Concurrency, Failover, Distributed Sharding, Hotkey/Big Key และ Production Tuning พร้อมระบบตรวจคำตอบทันที

Phase 3 Assessment Score: 0 / 25