Skip to content
清晨的一缕阳光
返回

Redis 数据分片策略详解

Redis 数据分片策略详解

数据分片是 Redis 水平扩展的核心技术。如何将数据合理分布到多个节点,直接影响系统性能和扩展性。本文将深入解析各种分片策略及其应用场景。

一、分片基础概念

1.1 为什么需要分片

分片的必要性:

单节点限制:
┌─────────────────────────────────────┐
│ 内存限制:单机内存有限(通常<64GB) │
│ CPU 限制:单线程处理能力的瓶颈       │
│ 网络限制:单节点网络带宽限制         │
│ 并发限制:单节点连接数限制           │
└─────────────────────────────────────┘

分片优势:
┌─────────────────────────────────────┐
│ 存储容量:多节点内存叠加             │
│ 处理能力:并行处理提升 QPS           │
│ 可用性:节点故障不影响全局           │
│ 扩展性:按需添加节点                 │
└─────────────────────────────────────┘

1.2 分片目标

二、哈希分片

2.1 原理

哈希分片流程:

key → hash(key) → hash % N → 节点编号

示例:
key = "user:1001"
hash(key) = CRC16("user:1001") = 12345
节点数 N = 3
节点编号 = 12345 % 3 = 0 → 节点 0

2.2 Java 实现

import redis.clients.jedis.Jedis;
import java.util.*;

public class HashSharding {
    private List<Jedis> shards;
    private int shardCount;
    
    public HashSharding(List<Jedis> shards) {
        this.shards = shards;
        this.shardCount = shards.size();
    }
    
    /**
     * 计算 key 应该路由到哪个分片
     */
    private int getShardIndex(String key) {
        int hash = Math.abs(key.hashCode());
        return hash % shardCount;
    }
    
    /**
     * 获取分片
     */
    private Jedis getShard(String key) {
        int index = getShardIndex(key);
        return shards.get(index);
    }
    
    /**
     * 设置值
     */
    public void set(String key, String value) {
        Jedis shard = getShard(key);
        shard.set(key, value);
    }
    
    /**
     * 获取值
     */
    public String get(String key) {
        Jedis shard = getShard(key);
        return shard.get(key);
    }
    
    /**
     * 删除
     */
    public void delete(String key) {
        Jedis shard = getShard(key);
        shard.del(key);
    }
    
    /**
     * 批量设置(跨分片)
     */
    public void mset(Map<String, String> data) {
        // 按分片分组
        Map<Integer, Map<String, String>> grouped = new HashMap<>();
        
        for (Map.Entry<String, String> entry : data.entrySet()) {
            int shardIndex = getShardIndex(entry.getKey());
            grouped.computeIfAbsent(shardIndex, k -> new HashMap<>())
                   .put(entry.getKey(), entry.getValue());
        }
        
        // 并行执行
        for (Map.Entry<Integer, Map<String, String>> entry : grouped.entrySet()) {
            int shardIndex = entry.getKey();
            Map<String, String> shardData = entry.getValue();
            
            new Thread(() -> {
                Jedis shard = shards.get(shardIndex);
                String[] args = new String[shardData.size() * 2];
                int i = 0;
                for (Map.Entry<String, String> e : shardData.entrySet()) {
                    args[i++] = e.getKey();
                    args[i++] = e.getValue();
                }
                shard.mset(args);
            }).start();
        }
    }
}

2.3 优缺点

优点

缺点

三、范围分片

3.1 原理

范围分片流程:

key → 按范围划分 → 节点

示例(按用户 ID 范围):
┌─────────────┐
│ 节点 1      │  user:1 - user:10000
│ 节点 2      │  user:10001 - user:20000
│ 节点 3      │  user:20001 - user:30000
└─────────────┘

3.2 Java 实现

import redis.clients.jedis.Jedis;
import java.util.*;

public class RangeSharding {
    private List<ShardRange> shards;
    
    public RangeSharding() {
        shards = new ArrayList<>();
        // 初始化分片范围
        shards.add(new ShardRange(1, 10000, getJedis(1)));
        shards.add(new ShardRange(10001, 20000, getJedis(2)));
        shards.add(new ShardRange(20001, 30000, getJedis(3)));
    }
    
    /**
     * 分片范围
     */
    static class ShardRange {
        int start;
        int end;
        Jedis jedis;
        
        ShardRange(int start, int end, Jedis jedis) {
            this.start = start;
            this.end = end;
            this.jedis = jedis;
        }
        
        boolean inRange(int id) {
            return id >= start && id <= end;
        }
    }
    
    /**
     * 从 key 中提取 ID
     */
    private int extractId(String key) {
        // 假设 key 格式:user:12345
        String[] parts = key.split(":");
        if (parts.length >= 2) {
            return Integer.parseInt(parts[1]);
        }
        throw new IllegalArgumentException("Invalid key format: " + key);
    }
    
    /**
     * 获取分片
     */
    private Jedis getShard(String key) {
        int id = extractId(key);
        
        for (ShardRange shard : shards) {
            if (shard.inRange(id)) {
                return shard.jedis;
            }
        }
        
        throw new RuntimeException("No shard found for id: " + id);
    }
    
    /**
     * 设置值
     */
    public void set(String key, String value) {
        Jedis shard = getShard(key);
        shard.set(key, value);
    }
    
    /**
     * 获取值
     */
    public String get(String key) {
        Jedis shard = getShard(key);
        return shard.get(key);
    }
    
    /**
     * 获取某个范围内的所有数据
     */
    public Map<String, String> getRange(int startId, int endId) {
        Map<String, String> result = new HashMap<>();
        
        for (ShardRange shard : shards) {
            // 判断是否有重叠
            if (shard.start <= endId && shard.end >= startId) {
                // 扫描该分片
                String pattern = "user:*";
                String cursor = ScanParams.SCAN_POINTER_START;
                ScanParams scanParams = new ScanParams();
                scanParams.match(pattern);
                scanParams.count(1000);
                
                while (true) {
                    ScanResult<String> scanResult = shard.jedis.scan(cursor, scanParams);
                    cursor = scanResult.getCursor();
                    
                    for (String key : scanResult.getResult()) {
                        int id = extractId(key);
                        if (id >= startId && id <= endId) {
                            result.put(key, shard.jedis.get(key));
                        }
                    }
                    
                    if (ScanParams.SCAN_POINTER_START.equals(cursor)) {
                        break;
                    }
                }
            }
        }
        
        return result;
    }
    
    private Jedis getJedis(int shardId) {
        // 创建连接
        return new Jedis("192.168.1." + shardId, 6379);
    }
}

3.3 优缺点

优点

缺点

四、一致性哈希

4.1 原理

一致性哈希环:

         ┌─────────────────┐
         │    节点 A        │
         │   (hash: 1000)  │
         │        ●        │
         │                 │
    ●────┤                 ├────●
节点 C   │       ○ key1    │   节点 B
(4000)   │                 │  (2000)
         │        ●        │
         │   节点 D        │
         │  (hash: 3000)   │
         └─────────────────┘

路由规则:
1. 计算 key 的 hash 值
2. 顺时针找到第一个节点
3. 该节点负责存储

4.2 虚拟节点

带虚拟节点的一致性哈希:

物理节点:A, B, C
虚拟节点:A1, A2, B1, B2, C1, C2

┌─────────────────────────────────────┐
│        A1       B1       C1         │
│         ●        ●        ●         │
│                                     │
│              A2       B2       C2   │
│               ●        ●        ●   │
└─────────────────────────────────────┘

优势:
- 数据分布更均匀
- 减少节点变化的影响

4.3 Java 实现

import redis.clients.jedis.Jedis;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentSkipListMap;

public class ConsistentHashing {
    private final ConcurrentSkipListMap<Long, Jedis> circle = new ConcurrentSkipListMap<>();
    private final List<Jedis> shards;
    private final int virtualNodes;
    
    public ConsistentHashing(List<Jedis> shards, int virtualNodes) {
        this.shards = shards;
        this.virtualNodes = virtualNodes;
        initCircle();
    }
    
    /**
     * 初始化哈希环
     */
    private void initCircle() {
        for (Jedis shard : shards) {
            for (int i = 0; i < virtualNodes; i++) {
                String virtualNodeKey = shard.getPool().getHostAndPort() + "#" + i;
                long hash = hash(virtualNodeKey);
                circle.put(hash, shard);
            }
        }
    }
    
    /**
     * 获取 key 对应的节点
     */
    public Jedis getShard(String key) {
        long hash = hash(key);
        
        // 找到大于等于 hash 的第一个节点
        Map.Entry<Long, Jedis> entry = circle.ceilingEntry(hash);
        
        // 如果没找到,返回环首的节点
        if (entry == null) {
            entry = circle.firstEntry();
        }
        
        return entry.getValue();
    }
    
    /**
     * 计算 hash 值(MD5 算法)
     */
    private long hash(String key) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(key.getBytes(StandardCharsets.UTF_8));
            
            // 取前 8 个字节作为 hash 值
            long hash = 0;
            for (int i = 0; i < 8; i++) {
                hash = (hash << 8) | (digest[i] & 0xFF);
            }
            return hash;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5 algorithm not found", e);
        }
    }
    
    /**
     * 设置值
     */
    public void set(String key, String value) {
        Jedis shard = getShard(key);
        shard.set(key, value);
    }
    
    /**
     * 获取值
     */
    public String get(String key) {
        Jedis shard = getShard(key);
        return shard.get(key);
    }
    
    /**
     * 添加节点
     */
    public void addNode(Jedis newShard) {
        shards.add(newShard);
        
        // 添加虚拟节点到环
        for (int i = 0; i < virtualNodes; i++) {
            String virtualNodeKey = newShard.getPool().getHostAndPort() + "#" + i;
            long hash = hash(virtualNodeKey);
            circle.put(hash, newShard);
        }
    }
    
    /**
     * 移除节点
     */
    public void removeNode(Jedis shard) {
        shards.remove(shard);
        
        // 从环中移除虚拟节点
        for (int i = 0; i < virtualNodes; i++) {
            String virtualNodeKey = shard.getPool().getHostAndPort() + "#" + i;
            long hash = hash(virtualNodeKey);
            circle.remove(hash);
        }
    }
    
    /**
     * 数据迁移(添加节点时)
     */
    public void rebalance() {
        // 实际项目中需要实现数据迁移逻辑
        // 1. 扫描所有节点的数据
        // 2. 重新计算每个 key 的目标节点
        // 3. 迁移需要移动的 key
        System.out.println("数据重新平衡...");
    }
}

4.4 使用示例

public class ConsistentHashingDemo {
    public static void main(String[] args) {
        // 创建分片
        List<Jedis> shards = Arrays.asList(
            new Jedis("192.168.1.10", 6379),
            new Jedis("192.168.1.11", 6379),
            new Jedis("192.168.1.12", 6379)
        );
        
        // 创建一致性哈希环(每个节点 100 个虚拟节点)
        ConsistentHashing ch = new ConsistentHashing(shards, 100);
        
        // 存储数据
        for (int i = 1; i <= 1000; i++) {
            ch.set("user:" + i, "user_data_" + i);
        }
        
        // 添加新节点
        Jedis newShard = new Jedis("192.168.1.13", 6379);
        ch.addNode(newShard);
        
        // 数据重新平衡(只迁移部分数据)
        ch.rebalance();
        
        // 验证数据
        String value = ch.get("user:100");
        System.out.println("user:100 = " + value);
    }
}

4.5 优缺点

优点

缺点

五、Redis Cluster 分片

5.1 槽位机制

Redis Cluster 使用 16384 个槽位:

槽位分配:
┌─────────────────────────────────────┐
│ 节点 1: 槽位 0 - 5460               │
│ 节点 2: 槽位 5461 - 10922           │
│ 节点 3: 槽位 10923 - 16383          │
└─────────────────────────────────────┘

Key 路由:
CRC16(key) % 16384 → 槽位编号 → 节点

5.2 哈希标签

使用哈希标签控制 key 的分布:

# 这些 key 会被路由到同一个槽位
user:1001:profile
user:1001:orders
user:1001:cart

# 哈希标签 {user:1001} 内的内容用于计算槽位
# 确保相关数据在同一个节点

5.3 Java 使用

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

import java.util.HashSet;
import java.util.Set;

public class ClusterShardingExample {
    public static void main(String[] args) {
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.1.10", 7000));
        nodes.add(new HostAndPort("192.168.1.11", 7001));
        nodes.add(new HostAndPort("192.168.1.12", 7002));
        
        JedisCluster cluster = new JedisCluster(nodes);
        
        // 自动路由
        cluster.set("user:1001:name", "张三");
        cluster.set("user:1001:age", "25");
        
        // 哈希标签确保在同一个节点
        cluster.set("{user:1001}:profile", "profile_data");
        cluster.set("{user:1001}:orders", "orders_data");
        
        // 批量操作(必须在同一个槽位)
        cluster.mset(
            "{user:1001}:name", "张三",
            "{user:1001}:age", "25",
            "{user:1001}:email", "test@example.com"
        );
        
        cluster.close();
    }
}

六、分片策略对比

6.1 对比表

策略均匀性扩展性复杂度适用场景
哈希分片简单应用
范围分片范围查询
一致性哈希大规模分布式
Cluster生产环境

6.2 选择建议

选择分片策略决策树:

1. 是否使用 Redis Cluster?
   ├── 是 → 使用 Cluster 内置分片
   └── 否 → 2

2. 是否需要频繁扩缩容?
   ├── 是 → 一致性哈希
   └── 否 → 3

3. 是否有范围查询需求?
   ├── 是 → 范围分片
   └── 否 → 哈希分片

七、总结

7.1 核心要点

  1. 哈希分片:简单直接,适合固定节点数
  2. 范围分片:适合有序数据和范围查询
  3. 一致性哈希:扩展性最好,节点变化影响最小
  4. Cluster 分片:生产环境首选,自动化程度高

7.2 最佳实践

  1. 提前规划:根据业务增长预估分片数量
  2. 预留空间:分片数量应多于当前需求
  3. 监控均衡:定期检查各分片数据量和负载
  4. 平滑迁移:扩缩容时保证服务不中断

参考资料


分享这篇文章到:

上一篇文章
RocketMQ 最佳实践进阶指南
下一篇文章
Golang 系列完整学习指南