分布式微服务系统架构第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 范围来决定数据在哪个节点
相关推荐
半聋半瞎1 天前
Flowable快速入门(Spring Boot整合版)
java·spring boot·后端·flowable
困惑阿三1 天前
CloudflarePages+GitHub零成本博客搭建手册
github
毕设源码-邱学长1 天前
【开题答辩全过程】以 基于SpringBoot的理工学院学术档案管理系统为例,包含答辩的问题和答案
java·spring boot·后端
修己xj1 天前
SpringBoot解析.mdb文件实战指南
java·spring boot·后端
lpfasd1231 天前
Spring Boot 定时任务详解(从入门到实战)
spring boot·后端·python
moxiaoran57531 天前
Go语言的文件操作
开发语言·后端·golang
赴前尘1 天前
记一次golang进程执行卡住的问题排查
开发语言·后端·golang
猫头虎1 天前
2026全网最热Claude Skills工具箱,GitHub上最受欢迎的7大Skills开源AI技能库
langchain·开源·prompt·github·aigc·ai编程·agi
码农小卡拉1 天前
Prometheus 监控 SpringBoot 应用完整教程
spring boot·后端·grafana·prometheus
CoderJia程序员甲1 天前
GitHub 热榜项目 - 日榜(2026-02-03)
git·ai·开源·llm·github