Redis单实例在内存容量和吞吐量上存在物理上限。Redis Cluster通过分片将数据分布在多个节点上,线性扩展存储容量和处理能力。但分片后的缓存系统面临穿透、击穿、雪崩三类典型故障。本文演示Redis Cluster的完整搭建流程,并给出三类缓存故障的防护代码实现。
Redis Cluster架构与哈希槽分片原理
Redis Cluster将全体数据划分为16384个哈希槽(hash slot),每个节点负责一部分槽位。客户端写入key时,使用CRC16算法计算槽位号:slot = CRC16(key) mod 16384,再根据槽位映射表将请求路由到对应节点。
以6节点集群为例(3主3从),每个主节点负责约5461个槽位。主节点故障时对应从节点自动提升为主节点,实现高可用。Redis Cluster采用去中心化架构,节点间通过Gossip协议通信,无需独立的协调器。
集群部署与节点配置
# redis-7000.conf (每个节点修改对应的port和dir)
port 7000
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 15000
cluster-announce-ip 192.168.1.10
cluster-announce-port 7000
cluster-announce-bus-port 17000
appendonly yes
appendfsync everysec
dir /data/redis/7000
maxmemory 8gb
maxmemory-policy allkeys-lru
bind 0.0.0.0
cluster-enabled yes启用集群模式。cluster-node-timeout设置节点心跳超时,超过15秒未收到节点响应则判定为故障,触发主从切换。
批量启动Redis实例并创建集群:
for port in 7000 7001 7002 7003 7004 7005; do
redis-server /etc/redis/redis-${port}.conf
done
redis-cli --cluster create \
192.168.1.10:7000 192.168.1.10:7001 192.168.1.11:7002 \
192.168.1.11:7003 192.168.1.12:7004 192.168.1.12:7005 \
--cluster-replicas 1
redis-cli -c -p 7000 cluster info
redis-cli -c -p 7000 cluster nodes
–cluster-replicas 1表示每个主节点配1个从节点,6个实例组成3主3从的集群。-c参数启用集群模式重定向,当key不在当前节点时自动跳转到正确节点。
Python客户端连接与读写操作
import redis
import json
class RedisClusterClient:
def __init__(self, startup_nodes):
self.client = redis.RedisCluster(
startup_nodes=startup_nodes,
max_connections=100,
retry_on_timeout=True,
socket_connect_timeout=2,
socket_timeout=3,
health_check_interval=30,
)
def set(self, key, value, ttl=3600):
return self.client.set(key, value, ex=ttl)
def get(self, key):
return self.client.get(key)
nodes = [
redis.cluster.ClusterNode('192.168.1.10', 7000),
redis.cluster.ClusterNode('192.168.1.10', 7001),
redis.cluster.ClusterNode('192.168.1.11', 7002),
redis.cluster.ClusterNode('192.168.1.11', 7003),
redis.cluster.ClusterNode('192.168.1.12', 7004),
redis.cluster.ClusterNode('192.168.1.12', 7005),
]
rcc = RedisClusterClient(nodes)
缓存穿透防护:布隆过滤器
缓存穿透是指大量请求查询不存在的key,缓存和数据库都不会命中,请求直达数据库造成压力。布隆过滤器在缓存前加一层拦截:所有存在的key预先加入布隆过滤器,请求到达时先检查布隆过滤器。
import mmh3
import math
class BloomFilter:
'''基于Redis Bitmap的分布式布隆过滤器'''
def __init__(self, redis_client, key, capacity=1000000, error_rate=0.001):
self.redis = redis_client
self.key = key
self.bit_size = int(-capacity * math.log(error_rate) / (math.log(2) ** 2))
self.hash_num = int(self.bit_size / capacity * math.log(2))
def add(self, value):
positions = self._get_positions(value)
pipe = self.redis.pipeline()
for pos in positions:
pipe.setbit(self.key, pos, 1)
pipe.execute()
def exists(self, value):
positions = self._get_positions(value)
pipe = self.redis.pipeline()
for pos in positions:
pipe.getbit(self.key, pos)
results = pipe.execute()
return all(results)
def _get_positions(self, value):
positions = []
for i in range(self.hash_num):
hash_val = mmh3.hash(value, seed=i) % self.bit_size
positions.append(abs(hash_val))
return positions
bloom = BloomFilter(redis_client, 'bloom:user_ids', capacity=5000000)
def get_user_with_bloom(user_id):
if not bloom.exists(str(user_id)):
return None # 不存在的key直接返回
cache_key = f'user:{user_id}'
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
user = db.query('SELECT * FROM users WHERE id = %s', user_id)
if user:
redis_client.setex(cache_key, 3600, json.dumps(user))
return user
布隆过滤器的误差率设置为0.1%时,100万key的bitmap仅需约1.4MB内存。false positive概率极低,false negative概率为0——布隆过滤器说不存在就一定不存在。
缓存击穿防护:互斥锁方案
缓存击穿是指热点key过期的瞬间,大量并发请求同时穿透到数据库。互斥锁方案确保同一时刻只有一个请求查询数据库。
import uuid
import time
def get_user_with_lock(redis_client, user_id):
cache_key = f'user:{user_id}'
lock_key = f'lock:{user_id}'
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
lock_token = str(uuid.uuid4())
acquired = redis_client.set(lock_key, lock_token, nx=True, ex=10)
if acquired:
try:
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
user = db.query('SELECT * FROM users WHERE id = %s', user_id)
if user:
redis_client.setex(cache_key, 3600, json.dumps(user))
return user
else:
redis_client.setex(cache_key, 60, json.dumps(None))
return None
finally:
lua_script = '''
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
'''
redis_client.eval(lua_script, 1, lock_key, lock_token)
else:
time.sleep(0.1)
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
return None
缓存雪崩防护:随机过期与多级缓存
缓存雪崩是指大量key在同一时间过期,请求全部打到数据库。解决方案是给TTL添加随机偏移量:
import random
def cache_set_with_jitter(redis_client, key, value, base_ttl=3600, jitter_range=600):
'''设置缓存时添加随机TTL偏移'''
ttl = base_ttl + random.randint(-jitter_range, jitter_range)
redis_client.setex(key, ttl, value)
def multi_level_get(redis_client, key, db_loader, local_cache=None):
'''多级缓存:本地缓存 -> Redis -> 数据库'''
if local_cache is not None:
val = local_cache.get(key)
if val is not None:
return val
val = redis_client.get(key)
if val is not None:
if local_cache is not None:
local_cache.set(key, val, ttl=30)
return json.loads(val)
val = db_loader()
if val is not None:
cache_set_with_jitter(redis_client, key, json.dumps(val), base_ttl=3600)
if local_cache is not None:
local_cache.set(key, json.dumps(val), ttl=30)
return val
多级缓存中L1本地缓存使用进程内缓存,L1的TTL设为30秒与L2的1小时拉开梯度,即使L2全部过期L1仍能挡住短时流量。数据库高可用架构方面,Redis Cluster本身已具备自动故障转移能力,监控需关注cluster_state和各节点内存使用率。当某节点内存接近maxmemory,allkeys-lru策略自动淘汰旧数据,若淘汰速率跟不上写入速率则需考虑扩容——添加新节点后执行redis-cli –cluster reshard重新分配槽位。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/rediscluster-fen-pian-ji-qun-bu-shu-yu-huan-cun-chuan-tou/