Redis Cluster集群部署实战与缓存击穿穿透雪崩防护策略

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/

(0)
小编小编
上一篇 4小时前
下一篇 4小时前

相关推荐