-
1、UUID
-
2. データベースの自己インクリメントID
-
2.1、主キーテーブル
-
2.2. ID自己増加ステップ設定
-
-
3. ナンバーセグメントモード
-
4、Redis INCR
-
5. スノーフレークアルゴリズム
-
6. 美団(葉)
-
7.Baidu (Uidgenerator)
-
8. ディディ (TinyID)
-
概要比較
バックグラウンド
複雑な分散システムでは、大量のデータを一意に識別する必要があることがよくありますが、たとえば、注文テーブルがデータベースとテーブルに分割されている場合、データベースの自動インクリメント ID をデータベースの一意の識別として使用できないことは明らかです。オーダー。さらに、他の分散シナリオにおける分散 ID にはいくつかの要件があります。
-
この傾向は増加しています。 ほとんどの RDBMS は B ツリー データ構造を使用してインデックス データを保存するため、主キーの選択における書き込みパフォーマンスを確保するために、順序付けされた主キーを使用するように努める必要があります。
-
単調増加: 並べ替え要件など、次の ID が前の ID より大きくなければならないことを確認します。
-
情報セキュリティ: IDが連続している場合、悪意のあるユーザーが盗むのは非常に簡単ですが、注文番号である場合はさらに危険であり、注文数量を直接知ることができます。したがって、一部のアプリケーション シナリオでは、ID を不規則かつ不規則にする必要があります。
さまざまなシナリオや要件に応じて、多くの分散 ID ソリューションが市場に誕生しました。この記事では、複数の分散 ID ソリューションを、それぞれの長所と短所、使用シナリオ、コード例を含めて紹介します。
1、UUID
UUID( Universally Unique Identifier
) は、現在時刻、カウンター (カウンター)、ハードウェア ID (通常はワイヤレス ネットワーク カードの MAC アドレス) などのデータに基づいて計算および生成されます。ハイフンで 5 つのセグメントに分割された 32 個の 16 進数、8-4-4-4-12 の形式の 36 文字が含まれており、グローバルに一意のコードを高いパフォーマンスで生成できます。
JDK は UUID 生成ツールを提供します。コードは次のとおりです。
import java.util.UUID;
public class Test {
public static void main(String[] args) {
System.out.println(UUID.randomUUID());
}
}
出力は次のとおりです
b0378f6a-eeb7-4779-bffe-2a9f3bc76380
UUID は分散固有の識別を完全に満たすことができますが、次の理由により、実際のアプリケーション プロセスでは通常使用されません。
-
ストレージ コストが高い: UUID は 16 バイト、128 ビットと長すぎます。通常は 36 長さの文字列で表され、多くのシナリオには適用できません。
-
情報の安全性の確保: MAC アドレスに基づいて生成された UUID アルゴリズムにより MAC アドレスが公開され、Melissa ウイルスの作成者は UUID に基づいて特定されました。
-
MySQL の主キーの要件を満たしていません。MySQL 公式は、主キーはできるだけ短くするべきであると明確に提案しています。長すぎると MySQL インデックスに良くないからです。データベースの主キーとして使用する場合は、InnoDB エンジンで使用します。 、UUID の障害により、データの位置が頻繁に変更され、パフォーマンスに重大な影響を与える可能性があります。
2. データベースの自己インクリメントID
Mysql の特性 ID の自己インクリメントを使用すると、データの一意の識別を実現できますが、サブデータベース以降のサブテーブルは、テーブル内の ID の一意性を保証するだけで、全体の ID の一意性は保証できません。このような状況を回避するために、次の 2 つの解決方法があります。
2.1、主キーテーブル
IDの出力元として一意の識別子を保持する主キーテーブルを別途作成することで、ID全体の一意性を保証することができます。例えば:
主キーテーブルを作成する
CREATE TABLE `unique_id` (
`id` bigint NOT NULL AUTO_INCREMENT,
`biz` char(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `biz` (`biz`)
) ENGINE = InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET =utf8;
企業は、更新操作を通じて ID 情報を取得し、それをサブテーブルに追加します。
BEGIN;
REPLACE INTO unique_id (biz) values ('o') ;
SELECT LAST_INSERT_ID();
COMMIT;
2.2. ID自己増加ステップ設定
Mysql の主キーの自動インクリメント ステップ サイズを設定できるため、異なるインスタンスに分散されたテーブル データ ID が重複せず、全体的な一意性が保証されます。
以下のように、Mysql インスタンス 1 のステップ サイズを 1 に、インスタンス 1 のステップ サイズを 2 に設定できます。
主キーの自動インクリメントの属性を表示する
show variables like '%increment%'
同時実行の量が比較的多い場合、この方法でスケーラビリティを確保する方法が実際に問題となるのは明らかです。
3. ナンバーセグメントモード
ナンバーセグメントモードは、現在の分散IDジェネレータの主流の実装方法の1つです。原則は次のとおりです。
-
数値範囲モードは、毎回データベースから数値範囲をフェッチし、サービス メモリにロードします。ビジネスが買収されると、ID はこの範囲の値を直接増加させることができます。
-
この数値セグメントのバッチの ID が使い果たされると、データベースから新しい数値セグメントを再度申請し、max_id フィールドで更新操作を実行します。新しい数値セグメントの範囲は ( , ]
max_id
ですmax_id +step
。 -
複数の業務端末が同時に動作する可能性があるため、バージョン番号versionの楽観的ロック方式で更新します。
たとえば、(1,1000] は 1000 個の ID を表し、特定のビジネス サービスは 1 ~ 1000 の範囲の自動インクリメント ID を生成します。テーブル構造は次のとおりです。
CREATE TABLE id_generator (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '当前最大id',
step int(20) NOT NULL COMMENT '号段的长度',
biz_type int(20) NOT NULL COMMENT '业务类型',
version int(20) NOT NULL COMMENT '版本号,是一个乐观锁,每次都更新version,保证并发时数据的正确性',
PRIMARY KEY (`id`)
)
この分散 ID 生成方法はデータベースに強く依存せず、頻繁にデータベースにアクセスすることもなく、データベースへの負担も大幅に軽減されます。ただし、サーバーの再起動、単一障害点により ID の不連続性が発生するなど、いくつかの欠点もあります。
4、Redis INCR
グローバル一意 ID の特性に基づいて、Redis の INCR コマンドを通じてグローバル一意 ID を生成できます。
Redis 分散 ID の単純なケース
/**
* Redis 分布式ID生成器
*/
@Component
public class RedisDistributedId {
@Autowired
private StringRedisTemplate redisTemplate;
private static final long BEGIN_TIMESTAMP = 1659312000l;
/**
* 生成分布式ID
* 符号位 时间戳[31位] 自增序号【32位】
* @param item
* @return
*/
public long nextId(String item){
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
// 格林威治时间差
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
// 我们需要获取的 时间戳 信息
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序号 --》 从Redis中获取
// 当前当前的日期
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 获取对应的自增的序号
Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date);
return timestamp << 32 | increment;
}
}
Redis の使用には、それに対応する欠点もあります。ID 生成の永続化の問題、Redis がダウンした場合の回復方法などです。
5. スノーフレークアルゴリズム
Snowflake は、Twitter がオープンソース化した分散 ID 生成アルゴリズムで、名前空間を分割することで 64 ビットのビットを複数の部分に分割し、各部分が特定の異なる意味を持ちます。Java では、64 ビット整数は Long 型です。 , そのため、Java の Snowflake アルゴリズムによって生成された ID は、long 形式で保存されます。詳細は次のとおりです。
-
最初の部分: 1 ビットを占め、最初のビットは符号ビットであり、適用されません
-
2 番目の部分: 41 ビットのタイムスタンプ、41 ビットは 241 個の数値を表すことができ、各数値はミリ秒を表し、スノーフレーク アルゴリズムの制限時間は
(241)/(1000×60×60×24×365)=69
年です -
3 番目の部分: 10 ビットはマシンの数、つまり
2^ 10 = 1024
1 台のマシンを示します。通常はそれほど多くのマシンは展開されません。 -
4 番目の部分: 12 ビットは自動インクリメント シーケンスであり、
2^12=4096
数値を表すことができ、1 秒以内に 4096 個の ID を生成できます。理論的には、スノーフレーク ソリューションの QPS は約409.6w/s
スノーフレークアルゴリズムのケースコード:
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/**
* 开始时间截 (2020-11-03,一旦确定不可更改,否则时间被回调,或者改变,可能会造成id重复或冲突)
*/
private final long twepoch = 1604374294980L;
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大数据标识id,结果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 数据标识id向左移17位(12+5)
*/
private final long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
*
*/
public SnowflakeIdWorker() {
this.workerId = 0L;
this.datacenterId = 0L;
}
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
/**
* 随机id生成,使用雪花算法
*
* @return
*/
public static String getSnowId() {
SnowflakeIdWorker sf = new SnowflakeIdWorker();
String id = String.valueOf(sf.nextId());
return id;
}
//=========================================Test=========================================
/**
* 测试
*/
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
}
}
スノーフレーク アルゴリズムはマシンの時計に大きく依存しているため、マシンの時計がダイヤルバックされると、番号の発行が繰り返されることになります。これは通常、最後の使用時間を記録することで処理されます。
6. 美団(葉)
Meituan によって開発された、オープンソース プロジェクトのリンク:
https://github.com/Meituan-Dianping/Leaf
Leaf は、数値セグメント モードとスノーフレーク アルゴリズム モードの両方をサポートしており、切り替えることができます。
スノーフレーク モードは ZooKeeper に依存しています。元のスノーフレーク アルゴリズムとは異なり、主に workId の生成にあります。Leaf の workId は、ZooKeeper のシーケンス ID に基づいて生成されます。各アプリケーションが Leaf-snowflake を使用する場合、Zookeeper に含まれます。シーケンスノードに対応するマシンに相当するシーケンスID、つまりworkIdを生成します。
数値セグメント モードは、データベースの自動インクリメント ID を分散 ID として直接使用するための最適化であり、データベースの操作頻度を削減します。これは、データベースから自己インクリメント ID をバッチで取得するのと同じです。毎回、データベースから数値の範囲が取り出されます。たとえば、(1,1000] は 1000 個の ID を表します。ビジネス サービスはローカルで自動インクリメント ID を生成します。 1 ~ 1000 の範囲の ID をメモリにロードします。
7.Baidu (Uidgenerator)
送信元アドレス:
https://github.com/baidu/uid-generator
中国語の文書アドレス:
https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
UidGenerator は、Baidu のオープンソース Java 言語実装であり、Snowflake アルゴリズムに基づいた一意の ID ジェネレーターです。これは分散されており、Snowflake アルゴリズムの同時実行制限を克服します。単一インスタンスの QPS は 6,000,000 を超える場合があります。必要な環境: JDK8+、MySQL (WorkerId の割り当て用)。
Baidu の Uidgenerator は、次のように構造にいくつかの調整を加えました。
時間部分はわずか 28 ビットです。つまり、UidGenerator はデフォルト ( 2^28-1/86400/365
) で 8.5 年しか耐えられませんが、UidGenerator はデルタ秒、ワーカー ノード ID、シーケンスが占める桁数を適切に調整できます。
8. ディディ (TinyID)
Didi によって開発された、オープンソース プロジェクトのリンク:
https://github.com/didi/tinyid
Tinyid は Meituan (Leaf) のアルゴリズムに基づいてアップグレードされleaf-segment
、データベースのマルチマスター ノード モードをサポートするだけでなく、tinyid-client
より使いやすいクライアント アクセス方法も提供します。ただし、Meituan (Leaf) とは異なり、Tinyid は数値セグメントの 1 つのモードのみをサポートし、スノーフレーク モードをサポートしません。Tinyid は 2 つの呼び出しメソッドを提供します。1 つは提供された http メソッドに基づくものでTinyid-server
、もう 1 つはTinyid-client
クライアント側のメソッドです。
概要比較