Redis在缓存架构中承担着降低数据库压力、提升响应速度的关键角色。单实例Redis面临的容量瓶颈和单点故障问题,通过Cluster模式可以得到有效解决。但在实际生产环境中,缓存击穿、缓存穿透和缓存雪崩三大经典问题同样是系统稳定性的重要威胁。本文介绍Redis Cluster的部署配置,并针对三大缓存问题给出完整的防护方案。
Redis Cluster架构与数据分片原理
Redis Cluster采用无中心架构,通过一致性哈希槽(Hash Slot)实现数据分片。Cluster共有16384个哈希槽,每个节点负责一部分槽位。客户端发送命令时,Redis根据CRC16算法计算key的哈希值,取模16384得到槽位号,再路由到对应节点。
集群最少需要6个节点(3主3从)保证高可用。主节点负责读写,从节点做数据冗余。主节点宕机时,从节点通过Raft协议选举提升为主节点。
哈希标签(Hash Tag)机制允许将多个key分配到同一槽位,使跨key操作(如MGET、SUNION)在同一节点执行:
# 使用花括号指定Hash Tag
SET {user:1001}:profile "..."
SET {user:1001}:settings "..."
SET {user:1001}:cart "..."
# 这三个key会被分配到同一槽位
# CRC16("user:1001") % 16384 = 槽位号
Redis Cluster集群搭建实战配置
以6节点集群(3主3从)为例,使用Docker Compose部署:
# docker-compose.yml
version: '3.8'
services:
redis-node-1:
image: redis:7.2-alpine
command: redis-server /etc/redis/redis.conf
volumes:
- ./redis-node-1.conf:/etc/redis/redis.conf
- redis-1-data:/data
ports:
- "7001:7001"
- "17001:17001"
networks:
- redis-cluster
redis-node-2:
image: redis:7.2-alpine
command: redis-server /etc/redis/redis.conf
volumes:
- ./redis-node-2.conf:/etc/redis/redis.conf
- redis-2-data:/data
ports:
- "7002:7002"
- "17002:17002"
networks:
- redis-cluster
# redis-node-3 到 redis-node-6 配置类似,端口7003-7006
volumes:
redis-1-data:
redis-2-data:
# ...
networks:
redis-cluster:
driver: bridge
每个节点的配置文件:
# redis-node-1.conf
port 7001
cluster-enabled yes
cluster-config-file nodes-1.conf
cluster-node-timeout 5000
cluster-announce-ip 127.0.0.1
cluster-announce-port 7001
cluster-announce-bus-port 17001
appendonly yes
appendfsync everysec
maxmemory 4gb
maxmemory-policy allkeys-lru
# 开启保护模式
protected-mode no
# 持久化配置
save 900 1
save 300 10
创建集群:
# 创建3主3从集群
redis-cli --cluster create \
127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \
127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \
--cluster-replicas 1
# 检查集群状态
redis-cli -p 7001 cluster nodes
redis-cli -p 7001 cluster info
# 验证槽位分配
redis-cli -p 7001 cluster slots
Spring Boot接入Redis Cluster:
# application.yml
spring:
data:
redis:
cluster:
nodes:
- 127.0.0.1:7001
- 127.0.0.1:7002
- 127.0.0.1:7003
- 127.0.0.1:7004
- 127.0.0.1:7005
- 127.0.0.1:7006
max-redirects: 3
topology-refresh: 10 # 每10秒刷新集群拓扑
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
cluster:
refresh:
adaptive: true # 自适应刷新拓扑
period: 10s
// RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setEnableTransactionSupport(false);
template.afterPropertiesSet();
return template;
}
}
缓存击穿防护:互斥锁与热点预加载
缓存击穿是指某个热点key在过期的瞬间,大量并发请求同时访问数据库,导致数据库压力骤增。解决方案是互斥锁和热点预加载。
互斥锁方案:缓存未命中时,先获取分布式锁,只有持有锁的请求查数据库并回填缓存,其他请求等待后重试:
@Service
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
private static final String LOCK_PREFIX = "lock:product:";
private static final long LOCK_EXPIRE = 10; // 秒
private static final long CACHE_EXPIRE = 30; // 分钟
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
String lockKey = LOCK_PREFIX + productId;
// 1. 查缓存
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 2. 获取互斥锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", LOCK_EXPIRE, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 3. 双重检查,防止等待期间已被其他请求回填
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 4. 查数据库
product = productMapper.selectById(productId);
if (product != null) {
// 5. 回填缓存,设置随机过期时间防雪崩
long expire = CACHE_EXPIRE * 60 +
ThreadLocalRandom.current().nextInt(300);
redisTemplate.opsForValue().set(
cacheKey, product, expire, TimeUnit.SECONDS);
}
return product;
} finally {
// 6. 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 7. 未获取到锁,短暂等待后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getProduct(productId); // 递归重试,实际需加最大重试次数
}
}
}
热点预加载:对已知的热点数据,在过期前主动刷新缓存,避免过期时刻的并发穿透:
@Scheduled(fixedRate = 20 * 60 * 1000) // 每20分钟执行
public void preloadHotProducts() {
List<Long> hotProductIds = getHotProductIds(); // 从统计接口获取
for (Long id : hotProductIds) {
String cacheKey = "product:" + id;
Long ttl = redisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
// 剩余TTL小于5分钟时提前刷新
if (ttl != null && ttl > 0 && ttl < 300) {
Product product = productMapper.selectById(id);
if (product != null) {
long expire = 30 * 60 + ThreadLocalRandom.current().nextInt(300);
redisTemplate.opsForValue().set(
cacheKey, product, expire, TimeUnit.SECONDS);
}
}
}
}
缓存穿透防护:布隆过滤器实现
缓存穿透是指查询不存在的数据,缓存和数据库都没有,每次请求都穿透到数据库。恶意攻击或爬虫会放大这个问题。布隆过滤器可以在请求到达数据库前拦截掉不存在的key。
@Service
public class BloomFilterService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
// 布隆过滤器key
private static final String BLOOM_KEY = "bloom:product:ids";
// 预期元素数量
private static final long EXPECTED_INSERTIONS = 1_000_000L;
// 误判率
private static final double FPP = 0.01;
@PostConstruct
public void initBloomFilter() {
// 从数据库加载所有商品ID到布隆过滤器
List<Long> allIds = productMapper.selectAllIds();
for (Long id : allIds) {
redisTemplate.opsForValue()
.setBit(BLOOM_KEY, hash(id), true);
}
}
// 简化版布隆过滤器(实际项目中用Redisson的RBloomFilter)
private long hash(Long id) {
// 使用Guava的布隆过滤器算法计算bit位
// 这里简化展示,实际使用Redisson RBloomFilter
return Math.abs(id.hashCode() * 31L) %
(EXPECTED_INSERTIONS * 10); // 近似位图大小
}
public Product getProductWithBloom(Long productId) {
// 1. 布隆过滤器先判断
Boolean exists = redisTemplate.opsForValue()
.getBit(BLOOM_KEY, hash(productId));
if (!Boolean.TRUE.equals(exists)) {
// 布隆过滤器说不存在,一定不存在
return null;
}
// 2. 走正常缓存查询流程
String cacheKey = "product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 3. 缓存空值,防止同一个不存在的key反复穿透
product = productMapper.selectById(productId);
if (product == null) {
// 缓存空值,短过期时间
redisTemplate.opsForValue().set(
cacheKey, "", 5, TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(
cacheKey, product, 30, TimeUnit.MINUTES);
return product;
}
}
使用Redisson的布隆过滤器更可靠:
// Redisson布隆过滤器
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("productBloomFilter");
// 初始化布隆过滤器
bloomFilter.tryInit(1_000_000L, 0.01);
// 添加元素
bloomFilter.add(productId);
// 判断元素是否可能存在
boolean mightContain = bloomFilter.contains(productId);
缓存雪崩防护:随机过期与多级缓存架构
缓存雪崩是指大量key在同一时间过期,或Redis整体宕机,导致所有请求涌入数据库。核心防范策略是打散过期时间和构建多级缓存。
随机过期时间:所有缓存key的过期时间添加随机偏移量,避免同时失效:
// 统一的缓存写入工具方法
public <T> void cacheWithJitter(String key, T value,
long baseTTLMinutes) {
// 基础TTL + 随机0到5分钟的偏移
int jitter = ThreadLocalRandom.current().nextInt(300);
long ttlSeconds = baseTTLMinutes * 60 + jitter;
redisTemplate.opsForValue().set(key, value,
ttlSeconds, TimeUnit.SECONDS);
}
// 使用
cacheWithJitter("product:1001", product, 30); // 30分钟 + 0~5分钟随机
多级缓存架构:本地缓存(Caffeine)+ 分布式缓存(Redis)+ 数据库三层结构。Redis整体不可用时,本地缓存仍能服务部分请求:
@Service
public class MultiLevelCacheService {
// L1: 本地缓存 (Caffeine)
private Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES) // 本地缓存5分钟
.recordStats()
.build();
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
public Product getProduct(Long id) {
String key = "product:" + id;
// L1: 查本地缓存
Product product = (Product) localCache.getIfPresent(key);
if (product != null) {
return product;
}
// L2: 查Redis
try {
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
if (!"".equals(product)) {
localCache.put(key, product);
}
return "".equals(product) ? null : product;
}
} catch (Exception e) {
// Redis异常时降级,直接查数据库
// 记录日志
}
// L3: 查数据库
product = productMapper.selectById(id);
if (product != null) {
int jitter = ThreadLocalRandom.current().nextInt(300);
redisTemplate.opsForValue().set(key, product,
30 * 60 + jitter, TimeUnit.SECONDS);
localCache.put(key, product);
} else {
// 缓存空值
redisTemplate.opsForValue().set(key, "",
5, TimeUnit.MINUTES);
}
return product;
}
}
Redis Cluster的运维要点:监控集群健康状态(cluster info中cluster_state为ok),关注槽位是否全覆盖(cluster slots),主从延迟是否在容忍范围。通过Redis Exporter + Prometheus采集内存使用率、连接数、命中率、慢查询等指标。当某个节点内存接近maxmemory时,需要扩容(添加新节点并迁移槽位)或调整数据分布策略。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/rediscluster-ji-qun-bu-shu-shi-zhan-yu-huan-cun-ji-chuan/