Reflection Real Systems ยท Redis Cluster
/**
* redis-cluster.ts โ Redis Cluster: Hash Slot Routing & Async Replication
*
* Run with: npx ts-node redis-cluster.ts
*
* Demonstrates Redis Cluster's approach to distribution:
* - 16384 fixed hash slots (CRC16 mod 16384)
* - Asynchronous master-replica replication
* - Cluster bus gossip
* - MOVED redirects on topology change
*/
const CRC16_TABLE: number[] = []
function initCRC16Table() {
for (let i = 0; i < 256; i++) {
let crc = i
for (let j = 0; j < 8; j++) {
if ((crc & 1) !== 0) crc = (crc >> 1) ^ 0xa001
else crc = crc >> 1
}
CRC16_TABLE[i] = crc
}
}
initCRC16Table()
function crc16(buf: Buffer): number {
let crc = 0
for (let i = 0; i < buf.length; i++) {
crc = (crc >> 8) ^ CRC16_TABLE[(crc ^ buf[i]) & 0xff]
}
return crc
}
function hashSlot(key: string): number {
// Redis hash slot: CRC16(key) mod 16384
// With hash tags: only the part inside {} is hashed
const tagMatch = key.match(/\{(.+?)\}/)
const effective = tagMatch ? tagMatch[1] : key
return crc16(Buffer.from(effective, 'utf-8')) % 16384
}
// --- Cluster Node ---
type NodeRole = 'master' | 'replica'
class RedisClusterNode {
id: string
host: string
port: number
role: NodeRole
master: RedisClusterNode | null = null
replicas: RedisClusterNode[] = []
slots: number[] = [] // slots this node is responsible for
data: Map<string, string> = new Map()
epoch: number = 0 // cluster config epoch (Raft-like term)
isAlive: boolean = true
constructor(id: string, host: string, port: number, role: NodeRole) {
this.id = id
this.host = host
this.port = port
this.role = role
}
}
// --- Cluster Router ---
class RedisCluster {
nodes: Map<string, RedisClusterNode> = new Map()
slotTable: (RedisClusterNode | null)[] = new Array(16384).fill(null)
addNode(node: RedisClusterNode) {
this.nodes.set(node.id, node)
}
assignSlots(node: RedisClusterNode, slots: number[]) {
// Clear previous assignments for these slots
for (const slot of slots) {
const old = this.slotTable[slot]
if (old) old.slots = old.slots.filter(s => s !== slot)
this.slotTable[slot] = node
node.slots.push(slot)
}
}
setReplica(replica: RedisClusterNode, master: RedisClusterNode) {
replica.master = master
master.replicas.push(replica)
}
getNodeForKey(key: string): RedisClusterNode | null {
const slot = hashSlot(key)
return this.slotTable[slot]
}
// --- Key Operations ---
set(key: string, value: string): { ok: boolean, redirect?: string } {
const node = this.getNodeForKey(key)
if (!node || !node.isAlive) return { ok: false }
node.data.set(key, value)
return { ok: true }
}
get(key: string): { value?: string, redirect?: string } {
const node = this.getNodeForKey(key)
if (!node || !node.isAlive) return {}
const value = node.data.get(key)
return { value }
}
// --- Failover Simulation ---
failNode(nodeId: string) {
const node = this.nodes.get(nodeId)
if (!node) return
node.isAlive = false
console.log(`\n๐ฅ Node ${nodeId} FAILED`)
if (node.role === 'master') {
// Promote a replica (simplified: pick first alive replica)
const replica = node.replicas.find(r => r.isAlive)
if (replica) {
this.promoteReplica(replica)
} else {
console.log(` No replica available for ${nodeId}`)
}
}
}
promoteReplica(replica: RedisClusterNode) {
console.log(` Promoting replica ${replica.id} to master`)
replica.role = 'master'
replica.master = null
// Reassign its master's slots
const oldMaster = [...this.nodes.values()]
.find(n => n.replicas.includes(replica))
if (oldMaster) {
oldMaster.replicas = oldMaster.replicas.filter(r => r.id !== replica.id)
this.assignSlots(replica, [...oldMaster.slots])
}
}
// --- Print Cluster State ---
printState() {
console.log('\n๐ Cluster State:')
for (const node of this.nodes.values()) {
const slotRange = node.slots.length > 0
? `${node.slots.length} slots [${node.slots[0]}..${node.slots[node.slots.length - 1]}]`
: 'no slots'
const dataSize = node.data.size
console.log(` ${node.id} (${node.role}) ${node.isAlive ? 'โ
' : 'โ'} | ${slotRange} | ${dataSize} keys`)
if (node.role === 'replica' && node.master) {
console.log(` โณ master: ${node.master.id}`)
}
}
}
}
// --- Simulation ---
function simulateRedisCluster() {
console.log('=== REDIS CLUSTER โ HASH SLOT ROUTING & REPLICATION ===\n')
const cluster = new RedisCluster()
// Create 3 masters
const m1 = new RedisClusterNode('M1', '10.0.0.1', 6379, 'master')
const m2 = new RedisClusterNode('M2', '10.0.0.2', 6380, 'master')
const m3 = new RedisClusterNode('M3', '10.0.0.3', 6381, 'master')
cluster.addNode(m1)
cluster.addNode(m2)
cluster.addNode(m3)
// Create 3 replicas
const r1 = new RedisClusterNode('R1', '10.0.0.4', 6382, 'replica')
const r2 = new RedisClusterNode('R2', '10.0.0.5', 6383, 'replica')
const r3 = new RedisClusterNode('R3', '10.0.0.6', 6384, 'replica')
cluster.addNode(r1)
cluster.addNode(r2)
cluster.addNode(r3)
// Assign slots: 16384 / 3 โ 5461 per master
const slotsPerNode = Math.floor(16384 / 3)
cluster.assignSlots(m1, Array.from({ length: slotsPerNode }, (_, i) => i))
cluster.assignSlots(m2, Array.from({ length: slotsPerNode }, (_, i) => i + slotsPerNode))
cluster.assignSlots(m3, Array.from({ length: 16384 - 2 * slotsPerNode }, (_, i) => i + 2 * slotsPerNode))
// Set up replication
cluster.setReplica(r1, m1)
cluster.setReplica(r2, m2)
cluster.setReplica(r3, m3)
cluster.printState()
// --- Phase 1: Write keys and observe slot routing ---
console.log('\n=== PHASE 1: Writing keys (slot routing) ===')
const keys = ['user:100', 'order:abc', 'session:xyz', 'product:42', 'cart:99']
for (const key of keys) {
const slot = hashSlot(key)
const result = cluster.set(key, `value-${key}`)
if (result.ok) {
const node = cluster.getNodeForKey(key)
console.log(` SET ${key} โ slot ${slot} โ ${node?.id}`)
}
}
// --- Phase 2: Verify key distribution ---
console.log('\n=== PHASE 2: Key distribution across nodes ===')
cluster.printState()
// --- Phase 3: Demonstrate MOVED redirect (slot ownership change) ---
console.log('\n=== PHASE 3: Reassigning slot 0 and demonstrating redirect ===')
// Migrate slot 0 from M1 to M2
cluster.assignSlots(m2, [0])
const keyInSlot0 = 'user:100' // this maps to slot 0
const newOwner = cluster.getNodeForKey(keyInSlot0)
console.log(` ${keyInSlot0} originally went to M1, now goes to ${newOwner?.id}`)
// --- Phase 4: Failover simulation ---
console.log('\n=== PHASE 4: Master failover ===')
cluster.failNode('M1')
cluster.printState()
// Verify keys are still accessible via the promoted replica
console.log('\n=== PHASE 5: Key access after failover ===')
for (const key of keys) {
const result = cluster.get(key)
const node = cluster.getNodeForKey(key)
if (result.value) {
console.log(` GET ${key} โ ${result.value} (from ${node?.id})`)
} else {
console.log(` GET ${key} โ KEY MISSING from ${node?.id} (async replication gap)`)
}
}
console.log('\nโ
Redis Cluster simulation complete.')
console.log(' Key insight: Redis trades strong consistency for performance.')
console.log(' Async replication means failover can lose recently written keys.')
}
simulateRedisCluster()
โฌ Download redis-cluster.ts