分布式ID
分库分表、多态机器分布式的情况下,就需要分布式ID生成算法,
分布式ID
UUID
- 优势就是本地生成,简单。
- 缺点:
- 太长了,32个十六进制数字,128 位
- 无序,如果当做数据库主键的话或者索引列,无序插入会导致数据页频繁分裂,性能不好。
雪花算法
雪花算法是一种生成分布式全局唯一id的算法,会得到一个64bit长度的long类型的数据。由四个部分组成,第一个bit位是符号位,因为id一般不会是负数,所以一般是0,接着41个bit位表示毫秒单位的时间戳,后面10个bit表示工作机器的id,最后用12个bit位表示递增的序列号。最后把64位拼接成一个long类型的数字。
以时间戳打头可以保证趋势是有序性,其中分配了机器号避免了多机器之间重复 ID 的情况,后面的自增序号使得理论上每台机器每毫秒都可以产生 2 的 12 次方个 ID。
时间戳是怎么表示的?
表示距离起始时间的毫秒数,2^41毫秒,大约69年
雪花算法的优点和缺点?
简述优点:ID 从整体来看有序的;简单、灵活配置,无序依赖外部三方组件,常见的hutool工具包就有提供了雪花算法工具类。
缺点:依赖时钟,如果发生时钟回拨,可能会导致重复 ID。
雪花算法的机器 ID 如何动态配置?
(因为现在机器都是动态扩容部署,机器数都是不固定的,如果机器 ID 没配置好,容易导致冲突。)
可以借助 Redis 或者 zookeeper 来实现机器 ID 的分配。
redis 的执行是单线程的,机器启动时候调用 redis incr 即可得到自增 id ,可将这个 id 作为机器 ID。
zookeeper 的使用也很简单,机器启动时候可以利用 zookeeper持久顺序节点特性,将注册成功的顺序号作为机器 ID。
雪花算法有什么问题?
可能发生时钟回滚问题,导致id重复。
- 解决方案:可以存储上一次发号的时间,如果当前发号的时间小于之前的发号时间,则说明时钟回拨,此时拒绝发号,可以报警或者重试(重试几次时间可能就回来了)。
数据库自增主键
对 MySQL 来说,直接利用自增 id 即可实现。
REPLACE INTO table(bizTag) VALUES ('order');
select last_insert_id();
将bizTag 设为唯一索引,可以填写业务值(也可以不同业务多张表),REPLACE INTO 执行后自增 ID 会 + 1,通过 last_insert_id 即可获得自增的 ID 。
数据库自增主键的优缺点
优点:简单、利用数据库就能实现,且 ID 有序。
缺点:性能不足。因为每次ID生成都要去访问一次磁盘数据库。数据库很容易成为性能瓶颈?
怎么解决这个性能瓶颈?
可以考虑横向扩展多台数据库,利用 auto_increment_increment步长和 auto_increment_offset偏移量实现横向扩展。
比如现在有两台数据库,auto_increment_increment 都设置为 2,即步长是 2。第一台数据库表 auto_increment_offset 设置为 1,第二台数据库表 auto_increment_offset 设置为 2。
这样一来,第一台的 ID 增长值就是 1、3、5、7、9....,第二台的 ID 增加值就是 2、4、6、8、10....
多台数据库有什么缺点?
这样也能保证全局唯一性,多加几台机器弥补性能问题,只要指定好每个表的步长和初始值即可。
单调递增特性没了,且加机器的成本不低,动态扩容很不方便。
基于号段模式的数据库自增主键【leaf】
每次操作数据库就拿一个 ID ,我们如果一次性拿 1000 个,那不就大大减少操作数据库的次数了吗?性能不就上去了吗?
重新设计下表,主要字段如下:
- bizTag: 业务标识;
- maxId: 目前已经分配的最大 ID;
- step: 步长,可以设置为 1000 那么每次就是拿 1000 ,设置成 1w 就是拿 1w 个
【基于数据库事务的原子性】每次来获取 ID 的 SQL 如下:
start transaction;
UPDATE table SET maxId = max_id + step WHERE bizTag = xxx
SELECT maxId, step FROM table WHERE biz_tag = xxx
commit;
这其实就是批量思想,大大提升了 ID 获取的性能。
拿取一批id存在哪里?
存在本地缓存里:性能好,服务重启了也就是中间空了一点点 ID 罢了,整体还是有序的。
- 计算ID范围:应用本地缓存的ID范围为:
[旧max_id + 1, 新max_id]
,即3001 ~ 4000
。 - 存储与管理:使用本地缓存,使用原子类AtomicLong,避免出现线程安全问题。示例代码(Java):
public class IdCache {
private AtomicLong currentId; // 当前分配的ID(初始化为旧max_id +1)
private long endId; // 当前批次的结束ID(新max_id)
public synchronized long nextId() {
if (currentId.get() > endId) {
fetchNewBatch(); // 重新获取批次
}
return currentId.getAndIncrement();
}
}
性能毛刺问题怎么解决?
假设业务并发量很高,此时业务方一批 ID 刚好用完后,来获取下一批 ID ,因为当前数据库压力大,很可能就会产生性能抖动,即卡了一下才拿到 ID,从监控上看就是产生毛刺。
可以使用预处理思想。
发号器服务可以本地缓存两个 buffer,业务方请求 ID 每次从其中一个 buffer 里取,如果这个 buffer的ID已经用了20 %(或者另外的数量),则可以起一个后台线程,调用上面的 SQL 语句,先把下一批的 ID 放置到另一个 buffer 中。
当前面那个 buffer ID 都用完了,则使用另一个 buffer 取号,如此循环使用即可,这样就能避免毛刺问题。
数据库故障怎么办?
理论上数据库都故障了其实很多业务都无法执行下去,这属于不可抗拒因素。
- 有本地缓存,可以将 step 设置大一些,例如 qps 最高时候的 600 倍,这样至少有 10分钟的缓冲时间,可能数据库就恢复了。
- 可以考虑数据库主从架构来保证服务的高可用性,但是主从会有复制延迟,导致 maxId 重复,这里可以采取和雪花算法对抗时钟回拨一样的套路,服务记录上次 maxId,如果发现 maxId 变小了,则再执行一次update。
这种方式有什么缺点?
数据库实现的 ID 是完全连续的,如果用在订单场景,竞对早上下一单,晚上下一单,两单一减就知道你今天卖了多少单,所以很不安全,因此这种 ID 不适合用在这种场景(再比如文章的 id 容易被爬虫简单按序一次性爬完)。