mysql雪花算法主键_雪花算法分布式ID应用

本文介绍了分布式ID的需求和常见的实现方式,重点探讨了Twitter的雪花算法(Snowflake),详细解析了其ID结构和工作原理,并给出了适用于中小企业自实现雪花算法的调整建议和Java实现代码示例。
摘要由CSDN通过智能技术生成

# 雪花算法分布式ID应用

本文分享分布式ID相关内容。

## 分布式ID的需求

* 全局唯一性

* 趋势递增:MySQL InnoDB中,通过有序主键保障写入性能和查询性能。

* 数据安全:ID不是连续自然数,避免人工猜测或爬虫抓取,达到和UUID一样效果。

## 常见的实现

* MySQL 单主自增ID:Insert前无法提前获取ID,多表依赖时必须先执行Insert取ID

* MySQL 多主自增ID:除单主限制外,还需要通过自增初始值+增长步长进行控制

* UUID:空间消耗大,无规则,索引效率低

* Redis:利用INCR命令生成,依赖第三方软件会提高复杂度,容易成为性能瓶颈,Redis宕机或重启需要考虑持久化问题

* [MongoDB’s ObjectIDs](https://docs.mongodb.com/manual/reference/method/ObjectId/) :12字节数值,由4字节时间戳(秒)+5字节随机值+3字节计数器组成

* snowflake:Twitter开源的"雪花算法"

## snowflake 雪花算法

> [Twitter:Twitter Ids(snowflake)](https://developer.twitter.com/en/docs/basics/twitter-ids)

> [Twitter Archive:snowflake](https://github.com/twitter-archive/snowflake/tree/snowflake-2010)

> [Generating unique IDs in a distributed environment at high scale](https://www.callicoder.com/distributed-unique-id-sequence-number-generator/)

Snowflake 用于在 Twitter 内部生成唯一的ID,用于 Tweets, Direct Messages, Users, Collections, Lists 等产品。

ID由 64位无符号数组成,组成结构为:

* 时间戳(timestamp):41位,毫秒单位,可用69年

* 机器号(worker number):10位,最大支持1024机器

* 序列号(sequence number):12位,每毫秒支持4096个数值,TPS可达 1000ms * 4096=400W/s

下面是64位组成结构,首位为符号位(有符号数,0表示正数),未使用。

```

|--------|--------|--------|--------|--------|--------|--------|--------|

|01111111|11111111|11111111|11111111|11111111|11111111|11111111|11111111|

|-xxxxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx|--------|--------|--------|

|--------|--------|--------|--------|--------|--xxxxxx|xxxx----|--------|

|--------|--------|--------|--------|--------|--------|----xxxx|xxxxxxxx|

```

**Twitter提醒:由于一些编程语言(如JavaScript) 支持的数值不能超过53位,因此最好同时提供id、idstr两个字段**。

## 自实现雪花算法

对于并发量不大的中小企业来说,可以适当调整雪花算法组成结构中的位数。

例如:

> 以来来自[廖雪峰老师:分布式唯一ID生成器](https://www.liaoxuefeng.com/article/1280526512029729)

* 时间戳:TPS很难达到400W,时间戳可降到秒为单位,32位可满足68年需求

* 机器:未采用容器时机器往往达不到1024台,可用5位(32台)足够。

* 序列号:16位可到65535,6W/s的TPS已挺高

* 以上,32+5+16=53位,可避免JS问题

下面是自实现ID生成工具:

* 使用了58位,由于容器化时代,机器位使用了10位,支持1024个容器

* 机器ID根据HOSTNAME自动生成

```java

import lombok.extern.slf4j.Slf4j;

import java.net.InetAddress;

import java.net.UnknownHostException;

import java.time.LocalDate;

import java.time.ZoneId;

/**

* 分布式ID, 基于"雪花算法", 但对各段的位数进行了调整.

* 适合中小型企业, 单应用并发写 <= 6W/s, 集群节点(容器或虚拟机) < 1024.

* long 型共64位, 占用58位, 首6位空闲.

* |--------|--------|--------|--------|--------|--------|--------|--------|

* |00000000|11111111|11111111|11111111|11111111|11111111|11111111|11111111|

* |------xx|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxxxxx--|--------|--------|--------|

* |--------|--------|--------|--------|------xx|xxxxxxxx|--------|--------|

* |--------|--------|--------|--------|--------|--------|xxxxxxxx|xxxxxxxx|

*

* 时间戳位: 32位, 单位秒, (2的32次方=2147483647秒=68年)

* 机器位 : 10位, 1024台虚拟机(含Docker容器)

* 序列号位: 16位, 2的16次方=65535, 即支持单应用6W/s并发写入

*

* 若要适配更高TPS, 可将时间戳调整为毫秒为单位

*

* @see IdUtil

*/

@Slf4j

public class SnowFlake {

/**

* 开始时间, 2020-1-1. 时间戳位上并不存储实际的时间, 而是当前时间与开始时间的差值(秒)

*/

private final static long START_TIMESTAMP = LocalDate.of(2020, 1, 1)

.atStartOfDay(ZoneId.of("Z")).toEpochSecond();

/**

* ID结构: 32位时间戳(秒) + 10位机器 + 16位序列号

*/

private final static long TIMESTAMP_BIT = 32;

private final static long MACHINE_BIT = 10;

private final static long SEQUENCE_BIT = 16;

/**

* 最大机器号

*/

private final static long MAX_MACHINE = 1 << MACHINE_BIT - 1;

/**

* 最大序列号

*/

private static final long MAX_SEQUENCE = 1 << SEQUENCE_BIT - 1;

/**

* 机器位和时间戳左移位数.

*/

private final static long MACHINE_LEFT = SEQUENCE_BIT;

private final static long TIMESTAMP_LEFT = MACHINE_BIT + SEQUENCE_BIT;

/**

* 机器标识

*/

private static long machineId = 1;

private static boolean machineInitialized = false;

/**

* 序列号

*/

private static long sequence = 0L;

/**

* 上一次时间戳

*/

private static long lastTimestamp = -1L;

static {

initMachineId();

}

/**

* 获取分布式ID

*

* @return

*/

public static long nextId() {

return nextId(getNowSeconds());

}

private static synchronized long nextId(long nowSeconds) {

long currentTimestamp = nowSeconds;

// 当前时间异常, 自动校准

if (currentTimestamp < lastTimestamp) {

currentTimestamp = lastTimestamp;

}

// 相同秒内,序列号自增

if (currentTimestamp == lastTimestamp) {

long nextSequence = sequence + 1;

//同一秒的序列数已经达到最大, 获取下一秒的ID(直接使用下一秒)

if (nextSequence >= MAX_SEQUENCE) {

return nextId(nowSeconds + 1);

} else {

sequence = nextSequence;

}

} else {

// 不同秒内,序列号置重置为0, 重新计数

sequence = 0L;

}

lastTimestamp = currentTimestamp;

// 当前时间与开始时间的差值

long timeDelta = currentTimestamp - START_TIMESTAMP;

// 各段结构拼接("|"操作)成ID

return timeDelta << TIMESTAMP_LEFT

| machineId << MACHINE_LEFT

| sequence;

}

private static long getNowSeconds() {

return System.currentTimeMillis() / 1000;

}

/**

* 通过hostname初始化机器ID, 将hostname各字符累加并对machineId最大值取余

*

虚拟主机: 可手工设置, eg:prod1

*

普通容器: 默认容器ID为HOSTNAME, eg: b88598ebca38

*

K8s Pod: 默认Pod ID为HOSTNAME, eg: user-service-433cf-5f6d48c7f4-8fclh

*

* @return

*/

private static void initMachineId() {

try {

if (!machineInitialized) {

String hostname = InetAddress.getLocalHost().getHostName();

char[] hostnameChars = hostname.toCharArray();

int sumOfHostname = 0;

for (int i = 0, length = hostnameChars.length; i < length; i++) {

sumOfHostname += hostnameChars[i];

}

machineId = sumOfHostname % MAX_MACHINE;

log.info("====> Init machineId. hostname[" + hostname + "], machineId[" + machineId + "]");

}

} catch (UnknownHostException e) {

log.error("====> Init machineId failure");

e.printStackTrace();

} finally {

machineInitialized = true;

}

}

/**

* 覆盖默认machineID

*

* @param mid machineId

*/

public static void setMachineId(long mid) {

if (mid > 0) {

machineId = mid;

}

}

}

```

扫码或搜索 codercyj 关注微信公众号, 结伴学习, 一起努力

265940be9fc1ad6bbfd2aa32400f6565.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值