参考文章:
- https://blog.csdn.net/hengyunabc/article/details/44244951
- https://www.jianshu.com/p/955909e1bd71
- https://tech.meituan.com/2017/04/21/mt-leaf.html
参考项目:https://github.com/hengyunabc/redis-id-generator值。
evalsha教程:https://www.runoob.com/redis/scripting-evalsha.html
eval教程:https://www.runoob.com/redis/scripting-eval.html
一、分布式id生成器需要满足的要求
1.全局唯一
2.尽可能保证id的递增
因为在查询的时候经常会有例如分页以及排序之类的需求,这个时候如果主键的id本身能够体现出时许效率会更加好。而对于常见的排序还有分页,我们解决办法有两种:
- 在数据表中添加一个时间字段,对其创建一个普通索引。
- id本生就是按照时间大致有序的。
因为常见的普通索引的访问效率是比聚集索引要慢的,所以我们尽可能使用第二种解决方案
3.其他的一些要求
- id要尽可能的短,这样可以减少存储的空间以及增加查询的效率。
- 要有足够数量的id可以使用,不然当数据量非常大时,id耗尽就不行了
- 要考虑不同机器之间的时间不一致问题
- QPS尽量要高,这样就可以,否则例如像类SNOWFLAKE算法会在64位ID中利用部分位数(如12)表示单位时间内生成的ID序号,这部分序号用完了,这个单位时间就不能再生成序号了
二、常见的id生成器方案:
1.利用mysql数据库的自增主键特性
优点:
- 简单,代码方便,性能还行
- 数字的id是递增的,方便进行分页和排序
缺点:
- 不同的数据库语法和实现不同,实现数据迁移或者多数据库版本的时候可能会出现一些问题
- 我们常见的是一主多从数据库,这会产生单点故障,以及性能瓶颈
- 数据量大时需要考虑分库分表
优化方案:
- 使用多个master,对每个master设置的初始id不同,步长不同,例如有四个master,我们可以让master1生成(1,5,9),master2生成(2,6,10),master3生成(3,7,11),master4生成(4,8,12),这样可以降低单个数据库的压力
2.UUID
常见的一种分布式id生成器,可以利用数据库也可以利用代码。
优点:
- 简单,方便
- 生成id的性能好,基本上不会有性能问题
- 全球唯一,对于数据库合并,迁移等问题不会存在冲突
缺点
- 不是有序的
- UUID的字符串长度较长,查询效率不高,且消耗存储空间比较大,如果是海量数据库就需要考虑存储量的问题了
- 可读性差
3.redis生成id
redis的大致原理和普通数据库的生成原理是大致相同的,只不过redis不是使用自增组件,而是使用原子操作 INCR和INCRBY来实现。
优点:
- 不依赖于数据库,灵活方便,且性能优于数据库。
数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点:
- 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
- 需要编码和配置的工作量比较大。
4.snowflake算法
- 一个ID由64位生成
- 41bit作为时间戳,记录当前时间到标记的起始时间(如到2018.1.1)差,精确到毫秒,那么服务可用时长为(1<<41)/(1000 60 60 24 365) = 69.73年
- 10bit作为机器ID,也就是可以有1024台机器
- 12bit作为序列号,代表单位时间(这里是毫秒)内允许生成的ID总数,也就是1ms内允许生成4096个ID
优点:
- 不依赖于数据库,灵活方便,且性能优于数据库。
- ID按照时间在单机上是递增的。
缺点:
- 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。
5.类SNOWFLAKE算法
SNOWFLAKE给出的主要是一个思想,把ID划分为多个段,有不同的含义,可以结合自己的要求进行重新划分。按照个人理解,时间戳位数少了,机器位数多了,序列号位数多了。借鉴snowflake的思想,结合各公司的业务逻辑和并发量,可以实现自己的分布式ID生成算法。
举例,假设某公司ID生成器服务的需求如下:
- 单机高峰并发量小于1W,预计未来5年单机高峰并发量小于10W
- 有2个机房,预计未来5年机房数量小于4个
- 每个机房机器数小于100台
- 目前有5个业务线有ID生成需求,预计未来业务线数量小于10个
分析过程如下:
- 高位取从2017年1月1日到现在的毫秒数(假设系统ID生成器服务在这个时间之后上线),假设系统至少运行10年,那至少需要10年 365天 24小时 3600秒 1000毫秒 = 320 * 10 ^ 9,差不多预留39bit给毫秒数
- 每秒的单机高峰并发量小于10W,即平均每毫秒的单机高峰并发量小于100,差不多预留7bit给每毫秒内序列号
- 5年内机房数小于4个,预留2bit给机房标识
- 每个机房小于100台机器,预留7bit给每个机房内的服务器标识
- 业务线小于10个,预留4bit给业务线标识
这样设计的64bit标识,可以保证:
- 每个业务线、每个机房、每个机器生成的ID都是不同的
- 同一个机器,每个毫秒内生成的ID都是不同的
- 同一个机器,同一个毫秒内,以序列号区区分保证生成的ID是不同的
- 将毫秒数放在最高位,保证生成的ID是趋势递增的
缺点:
- 由于“没有一个全局时钟”,每台服务器分配的ID是绝对递增的,但从全局看,生成的ID只是趋势递增的(有些服务器的时间早,有些服务器的时间晚)
三、实现一个简易的redis的id生成器
利用redis的lua脚本执行功能,在每个节点上通过lua脚本生成唯一id,其中使用的是雪花算法。
生成的ID是64位的:
- 使用41 bit来存放时间,精确到毫秒,可以使用41年。
- 使用12 bit来存放逻辑分片ID,最大分片ID是4095
- 使用10 bit来存放自增长ID,意味着每个节点,每毫秒最多可以生成1024个ID
比如GTM时间 Fri Mar 13 10:00:00 CST 2015
,它的距1970年的毫秒数是 1426212000000,假定分片ID是53,自增长序列是4,则生成的ID是:1
2// 左移22位,指代最前面14bit的存储信息,再左移10位表示中间存储分片信息的12bit
5981966696448054276 = 1426212000000 << 22 + 53 << 10 + 4
redis提供了TIME命令,可以取得redis服务器上的秒数和微秒数。因些lua脚本返回的是一个四元组。1
second, microSecond, partition, seq
客户端要自己处理,生成最终ID。1
((second * 1000 + microSecond / 1000) << (12 + 10)) + (shardId << 10) + seq;
seq对应的是集群中的节点值
如集群里有3个节点,则节点1返回的seq是:1
0, 3, 6, 9, 12 ...
节点2返回的seq是1
1, 4, 7, 10, 13 ...
节点3返回的seq是1
2, 5, 8, 11, 14 ...
我们可以将lua脚本转换成sha1值,然后通过EVALSHA指令传递这个
下面我们直接看代码
项目主程序1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Main {
public static void main(String[] args) {
String tab = "order";
long userId = 123456789;
IdGenerator idGenerator = IdGenerator.builder()
.addHost("127.0.0.1", 6379, "c5809078fa6d652e0b0232d552a9d06d37fe819c")
// .addHost("127.0.0.1", 7379, "accb7a987d4fb0fd85c57dc5a609529f80ec3722")
// .addHost("127.0.0.1", 8379, "f55f781ca4a00a133728488e15a554c070b17255")
.build();
long id = idGenerator.next(tab, userId);
System.out.println("id:" + id);
List<Long> result = IdGenerator.parseId(id);
System.out.println("miliSeconds:" + result.get(0) + ", partition:"
+ result.get(1) + ", seq:" + result.get(2));
}
}
id生成器相关代码
1 | public class IdGenerator { |
至此我们的基于redis的id生成器就完成了