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

加群联系作者vx:xiaoda0423

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

1024bat.cn/

github.com/webVueBlog/...

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 的映射(举例)

ini 复制代码
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 实际使用的。

比如:

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

这种写法一般出现在:

  • 自定义 Consistent Hash 场景
  • 模拟环境下简化的整数 token
  • 用来说明分片原理

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

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

✅ 原理

  • 每个节点有自己的 token 范围(可配置或自动生成)
  • 新增节点后,会重新分配 token 范围
  • 旧节点会将自己 token 范围内的一部分数据迁移给新节点

✅ 扩容步骤

假设原来:

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

新增 Node3:

makefile 复制代码
系统将新的 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)

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

✅ Java Murmur3 分区模拟代码

ini 复制代码
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;
    }
}

✅ 示例输出

makefile 复制代码
设备: 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 范围的数据。

🧮 你这段代码做了什么?

ini 复制代码
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 就将该数据分配给对应的节点。


java 复制代码
/**
 * 限流配置类
 * 用于配置限流功能所需的Redis模板和拦截器
 */
@Configuration
public class 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() {
        return new RateLimitInterceptor(rateLimitRedisTemplate());
    }
}

/**
 * 限流拦截器
 * 实现HandlerInterceptor接口,用于处理请求限流逻辑
 */
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    
    // Redis模板,用于操作Redis
    private final RedisTemplate<String, String> redisTemplate;
    // 限流键的前缀
    private final 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));
        } else if (Integer.parseInt(count) >= 100) { // 每分钟100次请求
            // 如果超过限制,返回429状态码
            response.setStatus(429);
            return false;
        } else {
            // 否则计数加1
            redisTemplate.opsForValue().increment(key);
        }
        
        return true;
    }
    
    /**
     * 获取客户端真实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 的含义

你在代码中写的是:

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

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

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

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

✅ 正确使用方式

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

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

然后执行:

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

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


✅ 如果是 Cassandra 的真实环境

在 Cassandra 中:

  • 并不使用 % totalTokens 来判断分布,而是直接用 token 落在哪个节点负责的 token 范围。
  • 每个节点的 token 是配置文件(cassandra.yaml)里定义的,比如:
makefile 复制代码
initial_token: -9223372036854775808

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


✅ 总结

内容
totalTokens 是什么 示例代码中人为设定的变量
是 Cassandra 内置的吗 ❌ 不是
应该怎么处理 手动定义一个整型,比如:int totalTokens = 10;
实际 Cassandra 怎么用 直接用 token 范围来决定数据在哪个节点
相关推荐
楼田莉子29 分钟前
C++算法题目分享:二叉搜索树相关的习题
数据结构·c++·学习·算法·leetcode·面试
最初的↘那颗心1 小时前
Java HashMap深度解析:原理、实现与最佳实践
java·开发语言·面试·hashmap·八股文
小兔兔吃萝卜1 小时前
Spring 创建 Bean 的 8 种主要方式
java·后端·spring
CoderJia程序员甲1 小时前
GitHub 热榜项目 - 日榜(2025-08-14)
ai·github·开源项目·github热榜
热爱2331 小时前
前端面试必备:原型链 & this 指向总结
前端·javascript·面试
Spider_Man1 小时前
面试官:你能手写 bind 吗?——JS this 全家桶趣味深度剖析
前端·javascript·面试
Java中文社群1 小时前
26届双非上岸记!快手之战~
java·后端·面试
whitepure2 小时前
万字详解Java中的面向对象(一)——设计原则
java·后端
autumnTop2 小时前
为什么访问不了同事的服务器或者ping不通地址了?
前端·后端·程序员