文章目录
- 分布式ID必要性
- 1.UUID
- 2.基于DB的自增主键方案
- 3.数据库多主模式
- 4.号段模式
- 5.Redis
- 6.Zookeeper
- 7.ETCD
- 8.雪花算法
- 9.百度(Uidgenerator)
- 10.美团(Leaf)
- 11.滴滴(TinyID)
分布式ID必要性
业务量小于500W的时候单独一个mysql即可提供服务,再大点的时候就进行读写分离也可以应付过来。
但当主从同步也扛不住的是就需要分表分库了,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;
特别一点的如订单、优惠券也都需要有唯一ID做标识。
此时一个能够生成全局唯一ID的系统是非常必要的。
那么这个全局唯一ID就叫分布式ID,分布式ID需满足哪些条件?
条件 | 解释 |
---|---|
全局唯一 | 基本要求就是必须保证ID是全局性唯一的。 |
高性能 | 高可用低延时,ID生成响应要快。 |
高可用 | 无限接近于100%的可用性 |
好接入 | 遵循拿来主义原则,在系统设计和实现上要尽可能的简单 |
趋势递增 | 最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求 |
比如我们的身份证,就完美地满足了分布式ID的特性:
组成 | 特点 |
---|---|
2位省级或直辖市编码 | 全局唯一 |
4位地区编码 | 非唯一 |
8位出生日期 | 非唯一(时钟回退),递增 |
顺序码(2位出生序列+1位性别) | 非唯一,递增 |
1位校验码 | 验证组合序列的正确性 |
一言以蔽之: 全局唯一 + 局部唯一 + 按情况递增 全局唯一 + 局部唯一 + 按情况递增 全局唯一+局部唯一+按情况递增。
不过由于身份证是18位的10进制数最多表示 1 0 18 10^{18} 1018条数据,这在计算机的二进制条件下是远远不够的。
1.UUID
UUID (Universally Unique Identifier),通用唯一识别码。
UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。
UUID由以下几部分的组合:
- 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
- 时钟序列。
- 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
UUID 是由一组32位数的16进制数字所构成「 8 16 = 128 b i t 8^{16}=128bit 816=128bit」,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。
import java.util.UUID;
public class UU_ID {
public static void main(String[] args) {
// ef0d86ea-b95d-4f31-bf22-9c8a0f39bc0a 基于随机数的UUID
String uuid = UUID.randomUUID().toString().replace("-", "-");
System.out.println(uuid);
}
}
优点 | 缺点 |
---|---|
简单、代码方便 | 没有排序,无法保证趋势递增 |
性能好,全球唯一 | 使用字符存储,查询效率低 |
本地生成无网络消耗 | 占用空间大,传输效率低 |
如果需求是只保证唯一性,那么UUID也是可以使用的,但是按照上面的分布式id的要求, UUID其实是不能做成分布式id的,原因如下:
- 首先分布式id一般都会作为主键,但是安装mysql官方推荐主键要尽量越短越好,UUID每一个都很长,所以不是很推荐。
- 既然分布式id是主键,然后主键是包含索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键生成的b+树进行很大的修改,带来很大压力,这一点很不好。
- 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
对于第二条:UUID的无序产生的IO压力,可以将UUID作为逻辑主键,物理主键仍然使用自增ID进行解决。
UUID生成策略:
UUID Version 1:基于时间的UUID
基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。
由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。
但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。
如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址--Java的UUID往往是这样实现的(当然也考虑了获取MAC的难度)
UUID Version 2:DCE安全的UUID
DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。
这个版本的UUID在实际中较少用到。
UUID Version 3:基于名字的UUID(MD5)
基于名字的UUID通过计算名字和名字空间的MD5散列值得到。
这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;
不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。
UUID Version 4:随机UUID
根据随机数,或者伪随机数生成UUID。
这种UUID产生重复的概率是可以计算出来的,
但随机的东西就像是买彩票:你指望它发财是不可能的,但狗屎运通常会在不经意中到来。
UUID Version 5:基于名字的UUID(SHA1)
和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。
2.基于DB的自增主键方案
针对表结构的主键,我们常规的操作是在创建表结构的时候给对应的ID设置 auto_increment
也就是自增选项。
但是这种方式我们清楚在单个数据库的场景中我们是可以这样做的,但如果是在分库分表的环境下。
直接利用单个数据库的自增肯定会出现问题。
因为ID要唯一,但是分表分库后只能保证一个表中的ID的唯一,而不能保证整体的ID唯一。
上面的情况我们可以通过单独创建主键维护表来处理。
举个例子来看看:
创建一个表结构
CREATE TABLE `sequence_id`
(
`id` bigint(20) unsigned NOT NULL auto_increment,
`value` char(10) NOT NULL default '',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP on UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
然后我们通过更新ID操作来获取ID信息
BEGIN;
REPLACE INTO sequence_id(value) VALUES ('values');
SELECT last_insert_id();
COMMIT;
优点 | 缺点 |
---|---|
实现简单,ID单调自增 | DB单点存在宕机风险 |
数值类型查询速度快 | 无法扛住高并发场景 |
3.数据库多主模式
单点数据库方式存在明显的性能问题,可以对数据库进行高可以优化,担心一个主节点挂掉没法使用,可以选择做双主模式集群,也就是两个MySQL实例都能单独生产自增的ID。
查看主键自增的属性
show variables like '%increment%'
Variable_name | Value |
---|---|
auto_increment_increment | 1 (步长) |
auto_increment_offset | 1(起始值) |
div_precision_increment | 4 |
innodb_autoextend_increment | 64 |
我们可以设置主键自增的步长从2开始,这样两个MySQL实例的自增ID分别就是:
但是如果两个还是无法满足咋办呢?
增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台机器的ID起始生成位置设定在比现有最大自增ID的位置远一些。
但必须在一、二两台MySQL实例ID还没有增长到第三台MySQL实例的起始ID值的时候,否则自增ID就要出现重复了,必要时可能还需要停机修改。
所以这种在并发量比较高的情况下,如何保证扩展性其实会是一个问题。
在高并发情况下无能为力,依旧无法满足高并发场景。
4.号段模式
号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围。
例如 (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 '版本号',
PRIMARY KEY (`id`)
)
字段说明:
biz_type :代表不同业务类型
max_id :当前最大的可用id
step :代表号段的长度
version :是一个乐观锁,每次都更新version,保证并发时数据的正确性
等这批号段ID用完,再次向数据库申请新号段,对 max_id 字段做一次 update 操作,update max_id = max_id + step,update 成功则说明新号段获取成功,新的号段范围是(max_id, max_id +step]
。
update id_generator set max_id = ${ max_id + step }, version = ${ version + 1 }
where version = ${ version } and biz_type = 1;
由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
但同样也会存在一些缺点比如:服务器重启,单点故障会造成ID不连续。
5.Redis
基于全局唯一ID的特性,我们可以通过Redis的INCR命令来生成全局唯一ID。
同样使用Redis也有对应的缺点:
- RDB 数据备份存在丢失数据的问题,理论上ID可能重复。
- ID 生成的持久化问题,加上AOF 性能也会存在损耗。
- 如果Redis宕机了怎么进行恢复,单个节点宕机问题。
当然针对故障问题我们可以通过Redis集群来处理,比如我们有三个Redis的Master节点。
可以初始化每台Redis的值分别是1,2,3;然后分别把分布式ID的KEY用Hash Tags固定每一个master节点,步长就是master节点的个数。各个Redis生成的ID为:
-
A:1,4,7
-
B:2,5,8
-
C:3,6,9
优点 | 缺点 |
---|---|
不依赖于数据库,灵活方便,且性能优于数据库 | 如果没有Redis数据库,需要安装配置,增加复杂度 |
数字ID有序,对分页处理和排序都很友好 | 分布式环境下,数据一致性策略需要考虑 |
防止了Redis的单机故障 | 集群节点确定是3个后,后面调整不是很友好 |
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;
}
}
6.Zookeeper
原理:利用zookeeper中的顺序节点的特性,制作分布式的序列号生成器(ID生成器)。
- 在ID下创建持久的顺序节点。
- 获取返回的节点名称。
- 删除该节点。
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class DistributedIDGenerator {
private static final String ZK_ADDRESS = "localhost:2181";
private static final String ZK_PATH = "/distributed_id";
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
// 创建 ZooKeeper 客户端
ZooKeeper zooKeeper = new ZooKeeper(ZK_ADDRESS, 5000, event -> {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("ZooKeeper connected.");
}
});
// 确保父节点存在
if (zooKeeper.exists(ZK_PATH, false) == null) {
zooKeeper.create(ZK_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
// 创建顺序节点
String sequenceNodePath = zooKeeper.create(ZK_PATH + "/id-", "data".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取节点名称
String id = sequenceNodePath.substring(ZK_PATH.length() + 1);
// 删除节点
zooKeeper.delete(sequenceNodePath, -1);
// 输出生成的 ID
System.out.println("Generated ID: " + id);
// 关闭 ZooKeeper 客户端
zooKeeper.close();
}
}
7.ETCD
ETCD 生成全局唯一 ID 的原理:
- 在 ETCD 中,每个事务(tx)都有一个唯一的事务 ID,称为 main ID。这个 main ID 是全局递增且不重复的。
- 一个事务可以包含多个修改操作(如
put
和delete
),每个操作称为一个 revision(修订)。这些修订共享同一个 main ID。 - 在一个事务内,连续的多个修改操作会被从 0 开始递增编号,这个编号称为 sub ID。每个 revision(修订) 都有一个唯一的 「main ID,sub ID」 组成唯一标识。
Revision的定义:
// A revision indicates modification of the key-value space.
// The set of changes that share same main revision changes the key-value space atomically. type revision struct {
// main is the main revision of a set of changes that happen atomically. main int64
// sub is the the sub revision of a change in a set of changes that happen
// atomically. Each change has different increasing sub revision in that
// set. sub int64 }
8.雪花算法
Snowflake,雪花算法是有Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64bit 位分割成了多个部分,每个部分都有具体的不同含义。
在Java中64Bit位的整数是Long类型,所以在Java中Snowflake算法生成的ID就是long来存储的,具体如下:
-
第一部分:占用1bit,第一位为符号位,无意义。因为二进制里第一个 bit 位如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
-
第二部分:41位的时间戳。41bit位可以表示 2 41 2 ^ {41} 241 个数,每个数代表的是毫秒,那么雪花算法的时间年限是 ( 2 41 ) / ( 1000 × 60 × 60 × 24 × 365 ) = 69 (2^{41})/(1000×60×60×24×365)=69 (241)/(1000×60×60×24×365)=69年。
-
第三部分:5位的机房id。意思就是最多代表 2 5 2 ^ 5 25 个机房(32 个机房)。
-
第四部分:5位的机器id。每个机房里可以有 2 5 2 ^ 5 25 个机器(32 台机器),也可以根据自己公司的实际情况确定。
-
第五部分:12bit位是自增序列。可以表示 2 12 = 4096 2^{12}=4096 212=4096个数,一秒内可以生成4096个ID。
案例代码:
package com.boge.vip.utils;
/**
* Twitter_Snowflake
* SnowFlake的结构如下(每部分用-分开):
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
* 10位的数据机器位(机房和机器不再区分统一占用10bit),可以部署在1024个节点,包括5位datacenterId和5位workerId
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
* 加起来刚好64位,为一个Long型。
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
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);
}
}
}
优点 | 缺点 |
---|---|
不依赖于数据库,完全在内存中生成,性能好。 | 严重依赖与系统时间的一致性, |
每秒中能生成数百万的自增ID,容量大。 | 如果系统时间被回调,或者改变, |
存入数据库中,索引效率高,ID自增。 | 可能会造成id冲突或者重复。 |
实际生产环境中我们应该怎么来应用雪花算法来实现分布式ID——实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化成业务表或者和我们系统相关的业务。
时针回拨问题【20:28 --》20:26】 【20:28 --》 20:30】:
问题场景 | 解决方案? |
---|---|
回拨时间很短:(<=100毫秒) | 直接阻塞100毫秒 |
回拨时间适中:(>100毫秒 <500毫秒) | 维护这500毫秒时间戳的最大ID信息 |
回拨时间比较长:(>=500毫秒 <1000毫秒) | 通过分布式ID服务器的轮询处理 |
回拨时间很长:(>=1000毫秒) | 直接下线 |
9.百度(Uidgenerator)
源码地址:https://github.com/baidu/uid-generator。
中文文档地址:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。
它是分布式的,并克服了雪花算法的并发限制。
UidGenerator以组件形式工作在应用项目中,支持自定义workerId位数和初始化策略,从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。
在实现上,UidGenerator通过借用未来时间来解决sequence天然存在的并发限制;
采用RingBuffer来缓存已生成的UID,并行化UID的生产和消费,同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题,最终单机QPS可达600万。
其实现原理和雪花算法并无二致,自定义号段,并且采用RingBuffer作为缓冲 从而提升性能。
需要的环境:JDK8+,MySQL(用于分配WorkerId)。
百度的Uidgenerator对结构做了部分的调整,具体如下:
UidGenerator的时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年( 2 28 − 1 / 86400 / 365 2^{28}-1/86400/365 228−1/86400/365)。
也可以根据你业务的需求,UidGenerator可以适当调整delta seconds
、worker node id
和sequence
占用位数。
摘自官网 CachedUidGenerator
RingBuffer环形数组,数组每个元素成为一个slot。RingBuffer容量,默认为Snowflake算法中sequence最大值,且为
2
N
2^N
2N。可通过boostPower
配置进行扩容,以提高RingBuffer 读写吞吐量。
Tail指针、Cursor指针用于环形数组上读写slot:
-
Tail指针
表示Producer生产的最大序号(此序号从0开始,持续递增)。Tail不能超过Cursor,即生产者不能覆盖未消费的slot。当Tail已赶上curosr,此时可通过rejectedPutBufferHandler
指定PutRejectPolicy -
Cursor指针
表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail,此时可通过rejectedTakeBufferHandler
指定TakeRejectPolicy
CachedUidGenerator采用了双RingBuffer,Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)
由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。但同时会带来「伪共享」FalseSharing问题,为此在Tail、Cursor指针、Flag-RingBuffer中采用了CacheLine 补齐方式。
RingBuffer填充时机
-
初始化预填充
RingBuffer初始化时,预先填充满整个RingBuffer. -
即时填充
Take消费时,即时检查剩余可用slot量(tail
-cursor
),如小于设定阈值,则补全空闲slots。阈值可通过paddingFactor
来进行配置,请参考Quick Start中CachedUidGenerator配置 -
周期填充
通过Schedule线程,定时补全空闲slots。可通过scheduleInterval
配置,以应用定时填充功能,并指定Schedule时间间隔
10.美团(Leaf)
由美团开发,开源项目链接:https://github.com/Meituan-Dianping/Leaf,相关博客链接:https://tech.meituan.com/2017/04/21/mt-leaf.html。
Leaf同时支持号段模式和snowflake算法模式,可以切换使用。
ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。
leaf.snowflake.enable=true
leaf.snowflake.enable=true
leaf.snowflake.enable=true
Leaf的号段模式是对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的频率操作。
相当于从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000]
代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。
特性:
-
全局唯一,绝对不会出现重复的ID,且ID整体趋势递增。
-
高可用,服务完全基于分布式架构,即使MySQL宕机,也能容忍一段时间的数据库不可用。
-
高并发低延时,在CentOS 4C8G的虚拟机上,远程调用QPS可达5W+,TP99在1ms内。
-
接入简单,直接通过公司RPC服务或者HTTP调用即可接入。
Leaf 采用双 buffer 的方式,它的服务内部有两个号段缓存区segment。
当前号段已消耗10%时,还没能拿到下一个号段,则会另启一个更新线程去更新下一个号段,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。
- 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS(秒处理事务数)的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
- 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。
简而言之就是Leaf保证了总是会多缓存两个号段,即便哪一时刻数据库挂了,也会保证发号服务可以正常工作一段时间。
--数据库表结构
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
`max_id` bigint NOT NULL DEFAULT '1',
`step` int NOT NULL,
`description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
配置:
leaf.name=leaf-server
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://192.168.150.101:3306/sl_leaf?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
leaf.jdbc.username=root
leaf.jdbc.password=123
leaf.snowflake.enable=false
11.滴滴(TinyID)
由滴滴开发,开源项目链接:https://github.com/didi/tinyid。
Tinyid是在美团(Leaf)的leaf-segment
算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了tinyid-client客户端的接入方式,使用起来更加方便。
但和美团(Leaf)不同的是,Tinyid只支持号段一种模式不支持雪花模式。
Tinyid提供了两种调用方式,一种基于Tinyid-server提供的http方式,另一种Tinyid-client客户端方式。
每个服务获取一个号段(1000,2000]
、(2000,3000]
、(3000,4000]
。
特性:
-
全局唯一的long型ID
-
趋势递增的id
-
提供 http 和 java-client 方式接入
-
支持批量获取ID
-
支持生成 1,3,5,7,9… 序列的ID
-
支持多个db的配置
适用场景 | 不适用场景 |
---|---|
只关心ID是数字 | 像类似于订单ID的业务 |
趋势递增的系统 | 因生成的ID大部分是连续的 |
可以容忍ID不连续 | 容易被扫库 |
可以容忍ID的浪费 | 或者推算出订单量等信息 |