加群联系作者vx:xiaoda0423
仓库地址:webvueblog.github.io/JavaPlusDoc...
webvueblog.github.io/JavaPlusDoc...
点击勘误issues,哪吒感谢大家的阅读
一、什么是 Token?
在分布式数据库(如 Cassandra)中,每台节点负责一段数据范围。这个范围使用 Token 来划分,比如从最小值到最大值(Murmur3:[-2^63, 2^63 - 1]
)。
- 每个设备 ID 或主键,都会通过哈希函数(如 Murmur3)生成一个 Token(Long 类型的整数)。
- Token 取值范围:
-9223372036854775808
到9223372036854775807
(约 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) |
token
在 Cassandra 的分布式架构中,是用于决定 数据在集群中存储在哪个节点上的核心机制 。下面结合你提到的 Murmur3Partitioner.hash(deviceId.getBytes())
这段代码,来解释 token 的作用。
🧩 token 是什么?
在 Cassandra 中,每条数据都需要根据其 分区键(partition key) 进行定位。分区键通过某种分区器(Partitioner)进行哈希计算,得到一个整数值,这个值就叫做 token。
- 这个
token
是一个 64 位有符号整数。 - 整个 token 范围是从
-2^63
到2^63 - 1
。 - 集群中的每个节点会负责一段 token 范围的数据。
🧮 你这段代码做了什么?
ini
String deviceId = "DEVICE_001";
long hash = Murmur3Partitioner.hash(deviceId.getBytes());
int tokenRange = hash % totalTokens;
含义解释:
Murmur3Partitioner.hash(deviceId.getBytes())
使用 Murmur3 哈希算法将deviceId
转换成一个 64 位 token 值。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 范围来决定数据在哪个节点 |