Redis缓存策略与高可用架构设计:从主从复制到Cluster分片

Redis缓存在现代Web架构中承担着减轻数据库压力、加速数据访问的核心角色。缓存策略选择不当会导致缓存穿透、雪崩、击穿等线上事故。本文从Redis缓存策略的选型原则出发,逐步展开高可用架构设计方案,覆盖主从复制、Sentinel哨兵、Cluster分片三种部署模式的配置与运维实践。

缓存策略选型:Cache-Aside与Write-Through的权衡

Redis缓存策略的核心决策点是读写路径的选择。Cache-Aside(旁路缓存)是最常用的策略:读请求先查缓存,未命中则查数据库并回填缓存;写请求先更新数据库再删除缓存。Write-Through(穿透写入)策略中所有写操作同时更新缓存和数据库,读请求只查缓存。

Cache-Aside的典型实现:

public class UserService {
    
    @Autowired
    private RedisTemplate<String, User> redis;
    @Autowired
    private UserMapper userMapper;
    
    private static final long CACHE_TTL = 3600; // 缓存1小时
    
    public User getUser(Long id) {
        String key = "user:" + id;
        User user = redis.opsForValue().get(key);
        
        if (user != null) {
            return user;
        }
        
        // 缓存未命中,查询数据库
        user = userMapper.selectById(id);
        if (user != null) {
            // 随机TTL防止缓存雪崩
            long ttl = CACHE_TTL + ThreadLocalRandom.current().nextLong(300);
            redis.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
        } else {
            // 空值缓存防止穿透,短TTL
            redis.opsForValue().set(key, new User(), 60, TimeUnit.SECONDS);
        }
        
        return user;
    }
    
    public void updateUser(User user) {
        // 先更新数据库
        userMapper.updateById(user);
        // 再删除缓存(而非更新缓存,避免并发不一致)
        redis.delete("user:" + user.getId());
    }
}

写操作选择删除缓存而非更新缓存的原因:在高并发场景下,如果线程A更新缓存、线程B同时更新缓存,两个线程的交叉执行可能导致缓存中存储的是过期数据。删除缓存是幂等操作,下次读请求会从数据库加载最新值回填。

TTL设置添加随机偏移(300秒内)是防止缓存雪崩的标准做法。如果大量Key在同一时间过期,瞬间的大量数据库查询会压垮后端。随机TTL将过期时间分散开,平滑缓存重建压力。

缓存击穿防护:互斥锁与逻辑过期

缓存击穿指热点Key失效瞬间,大量并发请求同时穿透到数据库。解决方案有两种:互斥锁重建和逻辑过期。互斥锁方案仅允许一个线程查询数据库并重建缓存,其他线程等待或返回旧值:

public User getUserWithLock(Long id) {
    String key = "user:" + id;
    User user = redis.opsForValue().get(key);
    
    if (user != null) {
        return user;
    }
    
    // 获取分布式锁,只有一个线程查库
    String lockKey = "lock:user:" + id;
    String lockValue = UUID.randomUUID().toString();
    
    Boolean locked = redis.opsForValue()
        .setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
    
    if (Boolean.TRUE.equals(locked)) {
        try {
            // 双重检查
            user = redis.opsForValue().get(key);
            if (user != null) {
                return user;
            }
            
            user = userMapper.selectById(id);
            redis.opsForValue().set(key, user, CACHE_TTL, TimeUnit.SECONDS);
            return user;
        } finally {
            // Lua脚本保证原子性释放锁
            String script =
                "if redis.call('get',KEYS[1])==ARGV[1] then " +
                "  return redis.call('del',KEYS[1]) " +
                "else return 0 end";
            redis.execute(new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(lockKey), lockValue);
        }
    } else {
        // 获取锁失败,短暂等待后重试读缓存
        try { Thread.sleep(50); } catch (InterruptedException ignored) {}
        return getUser(id);  // 递归重试
    }
}

逻辑过期方案不设置物理TTL,而在Value中保存逻辑过期时间。读请求发现逻辑过期后异步触发缓存重建,当前请求返回旧值。这种方案不阻塞请求,但短时间内返回的是过期数据,适合对实时性要求不极端的场景。

布隆过滤器拦截缓存穿透

缓存穿透指查询不存在的数据,每次请求都穿透到数据库。空值缓存能处理已知不存在的Key,但对于攻击者不断变换Key的场景,布隆过滤器是更有效的方案:

public class BloomFilterGuard {
    
    private final BloomFilter<Long> userIdFilter;
    
    public BloomFilterGuard(UserMapper userMapper) {
        // 预计元素数量100万,误判率0.01%
        this.userIdFilter = BloomFilter.create(
            Funnels.longFunnel(), 1_000_000, 0.0001);
        
        // 启动时加载所有用户ID到布隆过滤器
        try (Cursor<Long> cursor = userMapper.selectAllIds()) {
            while (cursor.hasNext()) {
                userIdFilter.put(cursor.next());
            }
        }
    }
    
    public User getUserSafe(Long id) {
        // 布隆过滤器拦截
        if (!userIdFilter.mightContain(id)) {
            return null;  // 一定不存在,直接返回
        }
        // 可能存在,走正常缓存逻辑
        return getUser(id);
    }
    
    public void onUserCreated(Long id) {
        userIdFilter.put(id);
    }
}

布隆过滤器的特性是”可能存在”表示不一定存在(存在误判率),”一定不存在”则确实不存在。误判率由位数组大小和哈希函数数量决定,100万元素、0.01%误判率的布隆过滤器仅需约1.8MB内存。布隆过滤器无法删除元素,如果用户被删除,布隆过滤器仍会放行该ID的查询,此时依靠空值缓存兜底。

Redis Sentinel主从切换与高可用部署

Redis Sentinel(哨兵)模式通过Sentinel进程监控主从节点状态,在主节点故障时自动执行主从切换。三节点Sentinel集群的配置:

# sentinel.conf
port 26379
dir /var/redis/sentinel

# 监控的主节点:名称 IP 端口 quorum(判定主观下线的哨兵数)
sentinel monitor mymaster 10.0.0.1 6379 2

# 主观下线判定时间(ms)
sentinel down-after-milliseconds mymaster 5000

# 故障转移超时时间
sentinel failover-timeout mymaster 60000

# 故障转移时同时向新主复制的从节点数
sentinel parallel-syncs mymaster 1

# Sentinel自身密码
requirepass sentinel_password

# 访问Redis节点的密码
sentinel auth-pass mymaster redis_password

quorum设置为2表示至少2个Sentinel同意才能判定主节点客观下线(ODOWN),触发自动故障转移。Sentinel集群建议部署3或5个节点(奇数),避免脑裂。客户端连接Sentinel而非直接连接Redis节点:

# Spring Boot配置
spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - sentinel1:26379
        - sentinel2:26379
        - sentinel3:26379
      password: sentinel_password
    password: redis_password
    lettuce:
      pool:
        max-active: 16
        max-idle: 8
        min-idle: 2
      read-from: replica-preferred  # 读写分离,优先读从节点

Sentinel模式的局限是单主节点写入瓶颈。当写入QPS超过单节点处理能力时,需要引入Cluster分片模式横向扩展写入能力。

Redis Cluster分片与数据迁移

Redis Cluster使用16384个哈希槽(slot)分布数据,每个节点负责一部分槽位。客户端使用CRC16算法计算Key的槽位编号,直接路由到对应节点。Cluster的配置和扩缩容操作:

# 创建6节点Cluster(3主3从)
redis-cli --cluster create \
  10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \
  10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \
  --cluster-replicas 1

# 查看Cluster状态
redis-cli -c cluster info
redis-cli -c cluster nodes

# 检查槽位分布
redis-cli --cluster check 10.0.0.1:6379

# 扩容:添加新节点
redis-cli --cluster add-node 10.0.0.7:6379 10.0.0.1:6379
redis-cli --cluster add-node 10.0.0.8:6379 10.0.0.1:6379 --cluster-slave --cluster-master-id <node7_id>

# 迁移槽位到新节点
redis-cli --cluster reshard 10.0.0.1:6379 \
  --cluster-from <source_node_id> \
  --cluster-to <target_node_id> \
  --cluster-slots 4096 \
  --cluster-yes

槽位迁移过程中Cluster保持在线服务。迁移单个槽位的底层操作是:源节点将槽位标记为MIGRATING状态,逐步将Key迁移到目标节点,迁移完成后源节点删除本地Key。迁移期间客户端请求该槽位的Key,如果Key还在源节点则正常返回,如果已迁移则返回ASK重定向到目标节点。

Cluster模式下的限制需要预先评估:不支持跨槽位的多Key操作(MGET、事务),如需操作多个Key需使用Hash Tag确保它们落在同一槽位(如{user}:100和{user}:100:profile保证哈希到同一槽位);Pub/Sub广播会放大到所有节点,大规模使用时考虑用Stream替代。数据备份恢复方面,Cluster模式不能直接使用BGSAVE生成全量RDB,需对每个主节点单独执行BGSAVE或使用redis-cli –cluster backup命令。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-ce-lyue-yu-gao-ke-yong-jia-gou-she-ji-cong/

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

相关推荐