分布式微服务系统架构第165集:阿里,字节,腾讯架构经验汇总

加群联系作者vx:xiaoda0423

仓库地址:https://webvueblog.github.io/JavaPlusDoc/

https://1024bat.cn/

https://github.com/webVueBlog/fastapi_plus

https://webvueblog.github.io/JavaPlusDoc/

点击勘误issues,哪吒感谢大家的阅读

一、什么是 Token?

在分布式数据库(如 Cassandra)中,每台节点负责一段数据范围。这个范围使用 Token 来划分,比如从最小值到最大值(Murmur3:[-2^63, 2^63 - 1])。

  • 每个设备 ID 或主键,都会通过哈希函数(如 Murmur3)生成一个 Token(Long 类型的整数)。

  • Token 取值范围:-92233720368547758089223372036854775807(约 1.84 x 10^19)


二、设备 ID 到 Token 的映射(举例)

go 复制代码
String deviceId = "DEVICE_001";
long token = murmur3Hash(deviceId.getBytes());

比如:

设备 ID Murmur3 Token
DEVICE_001 -7924723498472812002
DEVICE_002 -1467281738947810012
DEVICE_ABC 4328787342281400121

这些 token 是一个 可重复的哈希值(Murmur3 计算) ,稳定决定了这个设备的数据应该存在在哪个节点。


三、Token Range 分配给节点

假设你现在有 3 个节点

节点 负责 Token 区间
Node1 -9223372036854775808 ~ -3074457345618258603
Node2 -3074457345618258602 ~ 3074457345618258602
Node3 3074457345618258603 ~ 9223372036854775807

注意:

  • Token 是连续的,并被 环形排列(Ring)

  • 你用 token 去查找:某个 token 落在哪段区间内,这个节点就是主存储节点


四、那 Node1 (Token: 0-42) 这类是怎么来的?

这是个简化模型,不是 Cassandra 实际使用的。

比如:

go 复制代码
Node1 负责 Token:   0 ~  42
Node2 负责 Token:  43 ~  85
Node3 负责 Token:  86 ~ 127

这种写法一般出现在:

  • 自定义 Consistent Hash 场景

  • 模拟环境下简化的整数 token

  • 用来说明分片原理


五、如果设备越来越多,如何加节点来扩容?

Cassandra(或使用类似一致性哈希的系统)扩容逻辑如下:

✅ 原理

  • 每个节点有自己的 token 范围(可配置或自动生成)

  • 新增节点后,会重新分配 token 范围

  • 旧节点会将自己 token 范围内的一部分数据迁移给新节点

✅ 扩容步骤

假设原来:

go 复制代码
Node1 负责 -∞ ~ 0
Node2 负责 0 ~ +∞

新增 Node3:

go 复制代码
系统将新的 token 范围平均分成三份:
Node1: -922... ~ -307...
Node2: -307... ~ 307...
Node3:  307... ~ 922...

系统会:

  • 自动转移部分数据(或你手动 nodetool move

  • 所有新数据也会按新 token 范围落入不同节点


六、为什么使用 token 分布而不是 hash % n?

一致性哈希(Token Ring) 比普通的取模更适合分布式:

特性 hash % n 一致性哈希
节点变动 需要重哈希全部数据 仅影响小部分数据
分布均衡 可能不均衡 支持虚拟节点均衡分布
动态扩容 不友好 天然支持

七、总结一句话

设备 ID 通过哈希(如 Murmur3)转为一个 Long 型 token,落在哪个 token 区间,就由那个节点负责存储。节点越多,token 分段越细,系统负载越均衡。


✅ 示例目标

  • 输入一个设备 ID,例如:DEVICE_001

  • 使用 Murmur3 哈希计算 token

  • 使用 token % totalTokens 映射分区

  • 模拟 3 个分区的分布效果


💡 Java 完整示例代码

你需要添加一个 Murmur3 哈希实现(这里使用 Apache Cassandra 的简化版),以及主类逻辑。

✅ Maven 依赖(可选,用于 log)

go 复制代码
<!-- 如果你想用 log -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.30</version>
</dependency>

✅ Java Murmur3 分区模拟代码

go 复制代码
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class Murmur3PartitionerDemo {

    public static void main(String[] args) {
        String[] deviceIds = {
                "DEVICE_001", "DEVICE_002", "DEVICE_003",
                "DEVICE_ABC", "DEVICE_XYZ"
        };

        int totalTokens = 3; // 模拟 3 个分区

        for (String deviceId : deviceIds) {
            long token = murmur3Hash64(deviceId.getBytes());
            int partition = Math.abs((int) (token % totalTokens)); // 映射到分区

            System.out.printf("设备: %-12s | Token: %20d | 分区: %d%n", deviceId, token, partition);
        }
    }

    /**
     * MurmurHash3 64-bit 实现(Cassandra 使用)
     */
    public static long murmur3Hash64(byte[] data) {
        final int seed = 0;
        final long c1 = 0x87c37b91114253d5L;
        final long c2 = 0x4cf5ad432745937fL;

        long h1 = seed;
        long h2 = seed;

        ByteBuffer buffer = ByteBuffer.wrap(data);
        buffer.order(ByteOrder.LITTLE_ENDIAN);

        while (buffer.remaining() >= 16) {
            long k1 = buffer.getLong();
            long k2 = buffer.getLong();

            k1 *= c1;
            k1 = Long.rotateLeft(k1, 31);
            k1 *= c2;
            h1 ^= k1;

            h1 = Long.rotateLeft(h1, 27);
            h1 += h2;
            h1 = h1 * 5 + 0x52dce729;

            k2 *= c2;
            k2 = Long.rotateLeft(k2, 33);
            k2 *= c1;
            h2 ^= k2;

            h2 = Long.rotateLeft(h2, 31);
            h2 += h1;
            h2 = h2 * 5 + 0x38495ab5;
        }

        long k1 = 0;
        long k2 = 0;

        int remaining = buffer.remaining();
        switch (remaining) {
            case 15:
                k2 ^= ((long) buffer.get(14) & 0xff) << 48;
            case 14:
                k2 ^= ((long) buffer.get(13) & 0xff) << 40;
            case 13:
                k2 ^= ((long) buffer.get(12) & 0xff) << 32;
            case 12:
                k2 ^= ((long) buffer.get(11) & 0xff) << 24;
            case 11:
                k2 ^= ((long) buffer.get(10) & 0xff) << 16;
            case 10:
                k2 ^= ((long) buffer.get(9) & 0xff) << 8;
            case 9:
                k2 ^= ((long) buffer.get(8) & 0xff);
            case 8:
                k1 ^= buffer.getLong();
                break;
            case 7:
                k1 ^= ((long) buffer.get(6) & 0xff) << 48;
            case 6:
                k1 ^= ((long) buffer.get(5) & 0xff) << 40;
            case 5:
                k1 ^= ((long) buffer.get(4) & 0xff) << 32;
            case 4:
                k1 ^= ((long) buffer.get(3) & 0xff) << 24;
            case 3:
                k1 ^= ((long) buffer.get(2) & 0xff) << 16;
            case 2:
                k1 ^= ((long) buffer.get(1) & 0xff) << 8;
            case 1:
                k1 ^= ((long) buffer.get(0) & 0xff);
        }

        if (remaining > 8) {
            k2 *= c2;
            k2 = Long.rotateLeft(k2, 33);
            k2 *= c1;
            h2 ^= k2;
        }

        if (remaining > 0) {
            k1 *= c1;
            k1 = Long.rotateLeft(k1, 31);
            k1 *= c2;
            h1 ^= k1;
        }

        h1 ^= data.length;
        h2 ^= data.length;

        h1 += h2;
        h2 += h1;

        h1 = fmix64(h1);
        h2 = fmix64(h2);

        h1 += h2;

        return h1; // 返回 64 位 token
    }

    private static long fmix64(long k) {
        k ^= k >>> 33;
        k *= 0xff51afd7ed558ccdL;
        k ^= k >>> 33;
        k *= 0xc4ceb9fe1a85ec53L;
        k ^= k >>> 33;

        return k;
    }
}

✅ 示例输出

go 复制代码
设备: DEVICE_001   | Token:  -7924723498472812002 | 分区: 2
设备: DEVICE_002   | Token:  -1467281738947810012 | 分区: 2
设备: DEVICE_003   | Token:   4328787342281400121 | 分区: 0
设备: DEVICE_ABC   | Token:   1894723891247893210 | 分区: 0
设备: DEVICE_XYZ   | Token:  -1239981273871287312 | 分区: 1

🧠 说明

字段 含义
deviceId 模拟设备编号
murmur3Hash64 生成 Cassandra 风格的 token
token % totalTokens 将 token 分配到某个逻辑"分区"或"段"
Math.abs() 防止负数分区号(虽然 Cassandra 支持负 token)

tokenCassandra 的分布式架构中,是用于决定 数据在集群中存储在哪个节点上的核心机制 。下面结合你提到的 Murmur3Partitioner.hash(deviceId.getBytes()) 这段代码,来解释 token 的作用。


🧩 token 是什么?

在 Cassandra 中,每条数据都需要根据其 分区键(partition key) 进行定位。分区键通过某种分区器(Partitioner)进行哈希计算,得到一个整数值,这个值就叫做 token

  • 这个 token 是一个 64 位有符号整数。

  • 整个 token 范围是从 -2^632^63 - 1

  • 集群中的每个节点会负责一段 token 范围的数据。


🧮 你这段代码做了什么?

go 复制代码
String deviceId = "DEVICE_001";
long hash = Murmur3Partitioner.hash(deviceId.getBytes());
int tokenRange = hash % totalTokens;

含义解释:

  1. Murmur3Partitioner.hash(deviceId.getBytes())

    使用 Murmur3 哈希算法将 deviceId 转换成一个 64 位 token 值。

  2. tokenRange = hash % totalTokens

    将 hash 值取模(mod)得到一个范围编号,用于你自己做简单分区或模拟节点分布。

这个 token 值决定了该 deviceId 对应的数据会存储在哪个节点(token 范围负责的节点)上。


💡 token 的作用总结

功能 描述
数据分布 决定某条数据应该落在哪个节点
节点负载均衡 不同节点负责不同 token 范围,做到均衡负载
节点扩容缩容 可按 token 范围平滑迁移数据,不影响系统运行
查询效率 根据 token 快速定位数据所在节点,避免全表扫描

📍可视化理解

假设你有 3 个节点:

  • 节点 A 负责 token [-2^63, -1)

  • 节点 B 负责 token [0, 2^62)

  • 节点 C 负责 token [2^62, 2^63-1]

当你插入一个 deviceId,比如 "DEVICE_001",经过哈希得到一个 token,它会落入某个范围,于是 Cassandra 就将该数据分配给对应的节点。


go 复制代码
/**
 * 限流配置类
 * 用于配置限流功能所需的Redis模板和拦截器
 */
@Configuration
publicclass RateLimitConfig {
    
    /**
     * 创建限流功能专用的Redis模板
     * 设置连接工厂、键序列化和值序列化均为StringRedisSerializer
     * @return 配置好的RedisTemplate实例
     */
    @Bean
    public RedisTemplate<String, String> rateLimitRedisTemplate() {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
    
    /**
     * 创建限流拦截器
     * 使用Redis模板作为参数创建拦截器实例
     * @return 配置好的RateLimitInterceptor实例
     */
    @Bean
    public RateLimitInterceptor rateLimitInterceptor() {
        returnnew RateLimitInterceptor(rateLimitRedisTemplate());
    }
}

/**
 * 限流拦截器
 * 实现HandlerInterceptor接口,用于处理请求限流逻辑
 */
@Component
publicclass RateLimitInterceptor implements HandlerInterceptor {
    
    // Redis模板,用于操作Redis
    privatefinal RedisTemplate<String, String> redisTemplate;
    // 限流键的前缀
    privatefinal String RATE_LIMIT_KEY = "rate_limit:";
    
    /**
     * 构造函数,注入Redis模板
     * @param redisTemplate Redis模板实例
     */
    public RateLimitInterceptor(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    /**
     * 前置拦截器
     * 在请求处理前进行限流检查
     * @param request 当前HTTP请求
     * @param response 当前HTTP响应
     * @param handler 请求处理方法
     * @return 是否允许继续处理请求
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 获取客户端IP地址
        String clientIp = getClientIp(request);
        // 构建限流键
        String key = RATE_LIMIT_KEY + clientIp;
        
        // 从Redis获取当前请求计数
        String count = redisTemplate.opsForValue().get(key);
        if (count == null) {
            // 如果计数不存在,设置初始值为1,过期时间为1分钟
            redisTemplate.opsForValue().set(key, "1", Duration.ofMinutes(1));
        } elseif (Integer.parseInt(count) >= 100) { // 每分钟100次请求
            // 如果超过限制,返回429状态码
            response.setStatus(429);
            returnfalse;
        } else {
            // 否则计数加1
            redisTemplate.opsForValue().increment(key);
        }
        
        returntrue;
    }
    
    /**
     * 获取客户端真实IP地址
     * 处理了通过代理服务器的情况
     * @param request HTTP请求对象
     * @return 客户端IP地址
     */
    private String getClientIp(HttpServletRequest request) {
        // 获取X-Forwarded-For头信息
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            // 如果存在,取第一个IP
            return xForwardedFor.split(",")[0].trim();
        }
        // 否则直接获取远程地址
        return request.getRemoteAddr();
    }
}

totalTokens 是你代码中的一个变量,但 在 Cassandra 或 Murmur3Partitioner 中并不存在一个叫 totalTokens 的内置变量 。它是你自己在这段示例中定义的变量,用于模拟将 token 映射到某个数量的分区或节点


👇 正确认识 totalTokens 的含义

你在代码中写的是:

go 复制代码
int tokenRange = hash % totalTokens;

这行的含义是:将哈希后的 token 取模某个数值 totalTokens,从而划分到对应的 token 范围。

它常用于模拟场景,比如:

totalTokens 的值 含义
3 模拟 3 个分区或节点,手动分片
10 模拟将 token 分为 10 个段(例如用于数据分桶)
256 模拟像 Kafka 的 partition 数那样的场景

✅ 正确使用方式

你应在代码中**手动声明并初始化 totalTokens**,例如:

go 复制代码
int totalTokens = 3; // 模拟有3个分区/节点

然后执行:

go 复制代码
int tokenRange = Math.abs((int)(hash % totalTokens));

加上 Math.abs() 是为了避免负数(因为 Murmur3Partitioner 会返回负值)。


✅ 如果是 Cassandra 的真实环境

在 Cassandra 中:

  • 并不使用 % totalTokens 来判断分布,而是直接用 token 落在哪个节点负责的 token 范围。

  • 每个节点的 token 是配置文件(cassandra.yaml)里定义的,比如:

go 复制代码
initial_token: -9223372036854775808

Cassandra 会自动根据你使用的分区器(如 Murmur3Partitioner)进行分布。


✅ 总结

内容
totalTokens 是什么 示例代码中人为设定的变量
是 Cassandra 内置的吗 ❌ 不是
应该怎么处理 手动定义一个整型,比如:int totalTokens = 10;
实际 Cassandra 怎么用 直接用 token 范围来决定数据在哪个节点