分布式中的分库分表以后,ID 主键如何处理?

面试题

分库分表以后,id 主键如何处理?(惟一性,排序等)html

面试官心理分析

其实这是分库分表以后你必然要面对的一个问题,就是 id 咋生成?由于要是分红多个表以后,每一个表都是从 1 开始累加,那确定不对啊,须要一个全局惟一的 id 来支持,排序问题等。因此这都是你实际生产环境中必须考虑的问题。java

面试题剖析

基于数据库的实现方案

数据库自增 id

这个就是说你的系统里每次获得一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,而后获取一个数据库自增的一个 id。拿到这个 id 以后再往对应的分库分表里去写入。mysql

这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增 id,要是高并发的话,就会有瓶颈的;若是你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,而后本身递增几个 id,一次性返回一批 id,而后再把当前最大 id 值修改为递增几个 id 以后的一个值;可是不管如何都是基于单个数据库git

适合的场景:你分库分表就俩缘由,要不就是单库并发过高,要不就是单库数据量太大;除非是你并发不高,可是数据量太大致使的分库分表扩容,你能够用这个方案,由于可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键便可。程序员

设置数据库 sequence 或者表自增字段步长

能够经过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。github

好比说,如今有 8 个服务节点,每一个服务节点使用一个 sequence 功能来产生 ID,每一个 sequence 的起始 ID 不一样,而且依次递增,步长都是 8。面试

file

适合的场景:在用户防止产生的 ID 重复时,这种方案实现起来比较简单,也能达到性能目标。可是服务节点固定,步长也固定,未来若是还要增长服务节点,就很差搞了。redis

UUID

好处就是本地生成,不要基于数据库来了;很差之处就是,UUID 太长了、占用空间大,做为主键性能太差了;更重要的是,UUID 不具备有序性,会致使 B+ 树索引在写的时候有过多的随机写操做(连续的 ID 能够产生部分顺序写),还有,因为在写的时候不能产生有顺序的 append 操做,而须要进行 insert 操做,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操做在记录占用空间比较大的状况下,性能降低明显。算法

适合的场景:若是你是要随机生成个什么文件名、编号之类的,你能够用 UUID,可是做为主键是不能用 UUID 的。sql

UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf

获取系统当前时间

这个就是获取当前时间便可,可是问题是,并发很高的时候,好比一秒并发几千,会有重复的状况,这个是确定不合适的。基本就不用考虑了。

适合的场景:通常若是用这个方案,是将当前时间跟不少其余的业务字段拼接起来,做为一个 id,若是业务上你以为能够接受,那么也是能够的。你能够将别的业务字段值跟当前时间拼接起来,组成一个全局惟一的编号。

snowflake 算法

snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的  +  用其中的 41 bit 做为毫秒数  +  用 10 bit 做为工做机器 id  +  12 bit 做为序列号。

  • 1 bit:不用,为啥呢?由于二进制里第一个 bit 为若是是 1,那么都是负数,可是咱们生成的 id 都是正数,因此第一个 bit 统一都是 0。
  • 41 bit:表示的是时间戳,单位是毫秒。41 bit 能够表示的数字多达 2^41 - 1,也就是能够标识 2^41 - 1 个毫秒值,换算成年就是表示69年的时间。
  • 10 bit:记录工做机器 id,表明的是这个服务最多能够部署在 2^10台机器上哪,也就是1024台机器。可是 10 bit 里 5 个 bit 表明机房 id,5 个 bit 表明机器 id。意思就是最多表明 2^5个机房(32个机房),每一个机房里能够表明 2^5 个机器(32台机器)。
  • 12 bit:这个是用来记录同一个毫秒内产生的不一样 id,12 bit 能够表明的最大正整数是 2^12 - 1 = 4096,也就是说能够用这个 12 bit 表明的数字来区分同一个毫秒内的 4096 个不一样的 id。
0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000
public class IdWorker {

    private long workerId;
    private long datacenterId;
    private long sequence;

    public IdWorker(long workerId, long datacenterId, long sequence) {
        // sanity check for workerId
        // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0
        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));
        }
        System.out.printf(
                "worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    private long twepoch = 1288834974657L;

    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;

    // 这个是二进制运算,就是 5 bit最多只能有31个数字,也就是说机器id最多只能是32之内
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);

    // 这个是一个意思,就是 5 bit最多只能有31个数字,机房id最多只能是32之内
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private long sequenceBits = 12L;

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    private long lastTimestamp = -1L;

    public long getWorkerId() {
        return workerId;
    }

    public long getDatacenterId() {
        return datacenterId;
    }

    public long getTimestamp() {
        return System.currentTimeMillis();
    }

    public synchronized long nextId() {
        // 这儿就是获取当前时间戳,单位是毫秒
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format(
                    "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 这个意思是说一个毫秒内最多只能有4096个数字
            // 不管你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你本身传递个sequence超过了4096这个范围
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
        lastTimestamp = timestamp;

        // 这儿就是将时间戳左移,放到 41 bit那儿;
        // 将机房 id左移放到 5 bit那儿;
        // 将机器id左移放到5 bit那儿;将序号放最后12 bit;
        // 最后拼接起来成一个 64 bit的二进制数字,转换成 10 进制就是个 long 型
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    // ---------------测试---------------
    public static void main(String[] args) {
        IdWorker worker = new IdWorker(1, 1, 1);
        for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }
    }

}

怎么说呢,大概这个意思吧,就是说 41 bit 是当前毫秒单位的一个时间戳,就这意思;而后 5 bit 是你传递进来的一个机房 id(可是最大只能是 32 之内),另外 5 bit 是你传递进来的机器 id(可是最大只能是 32 之内),剩下的那个 12 bit序列号,就是若是跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号之内。

因此你本身利用这个工具类,本身搞一个服务,而后对每一个机房的每一个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是 0。而后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。

利用这个 snowflake 算法,你能够开发本身公司的服务,甚至对于机房 id 和机器 id,反正给你预留了 5 bit + 5 bit,你换成别的有业务含义的东西也能够的。

固然,你也能够用 : 1 个 bit 是不用的  +  用其中的 41 bit 做为毫秒数   +  12 bit 做为序列号  +  用 10 bit 做为工做机器 id,或者颠倒兑换一下顺序,怎么使用根据你本身的业务须要进行组合配用。

这个 snowflake 算法相对来讲仍是比较靠谱的,因此你要真是搞分布式 id 生成,若是是高并发啥的,那么用这个应该性能比较好,通常每秒几万并发的场景,也足够你用了。

本文在米兜公众号连接:
https://mp.weixin.qq.com/s/mt8bVpM57SsI-nvTRKxSKg

 

出处:https://www.cnblogs.com/midoujava/p/11610492.html

============================================================================================

若是仅仅是保证惟一性,本身写一个算法和规则,根据这个规则各个模块本身生成编号;

好比,以前写过一个简单的示例,使用xml文件做为配置生成固定格式的序列号的配置文件,程序根据算法生成指定格式的字符串。相似日期的格式化函数。

可是每每不少时候又须要把多份数据汇总、聚合等,还有排序等状况,这个时候须要根据生成的记录进行排序(也可考虑根据插入时间排序,各有利弊),还要考虑精度:有时、分、秒、厘秒、毫秒、微妙等等,当多台服务器进行时间同步到时候,还有须要靠网络延迟等状况。

全部,要根据本身的业务须要和本身可以承受的容错率来肯定。

============================================================================================

 

本文已经收录自 JavaGuide (60k+ Star【Java学习+面试指南】 一份涵盖大部分Java程序员所须要掌握的核心知识。)

本文受权转载自:https://juejin.im/post/5d6fc8eff265da03ef7a324b ,做者:1点25。

ID是数据的惟一标识,传统的作法是利用UUID和数据库的自增ID,在互联网企业中,大部分公司使用的都是Mysql,而且由于须要事务支持,因此一般会使用Innodb存储引擎,UUID太长以及无序,因此并不适合在Innodb中来做为主键,自增ID比较合适,可是随着公司的业务发展,数据量将愈来愈大,须要对数据进行分表,而分表后,每一个表中的数据都会按本身的节奏进行自增,颇有可能出现ID冲突。这时就须要一个单独的机制来负责生成惟一ID,生成出来的ID也能够叫作分布式ID,或全局ID。下面来分析各个生成分布式ID的机制。

经常使用分布式id方案

这篇文章并不会分析的特别详细,主要是作一些总结,之后再出一些详细某个方案的文章。

数据库自增ID

第一种方案仍然仍是基于数据库的自增ID,须要单独使用一个数据库实例,在这个实例中新建一个单独的表:

表结构以下:

CREATE DATABASE `SEQID`;

CREATE TABLE SEQID.SEQUENCE_ID (
    id bigint(20) unsigned NOT NULL auto_increment, 
    stub char(10) NOT NULL default '',
    PRIMARY KEY (id),
    UNIQUE KEY stub (stub)
) ENGINE=MyISAM;

可使用下面的语句生成并获取到一个自增ID

begin;
replace into SEQUENCE_ID (stub) VALUES ('anyword');
select last_insert_id();
commit;

stub字段在这里并无什么特殊的意义,只是为了方便的去插入数据,只有能插入数据才能产生自增id。而对于插入咱们用的是replace,replace会先看是否存在stub指定值同样的数据,若是存在则先delete再insert,若是不存在则直接insert。

这种生成分布式ID的机制,须要一个单独的Mysql实例,虽然可行,可是基于性能与可靠性来考虑的话都不够,业务系统每次须要一个ID时,都须要请求数据库获取,性能低,而且若是此数据库实例下线了,那么将影响全部的业务系统。

为了解决数据库可靠性问题,咱们可使用第二种分布式ID生成方案。

数据库多主模式

若是咱们两个数据库组成一个主从模式集群,正常状况下能够解决数据库可靠性问题,可是若是主库挂掉后,数据没有及时同步到从库,这个时候会出现ID重复的现象。咱们可使用双主模式集群,也就是两个Mysql实例都能单独的生产自增ID,这样可以提升效率,可是若是不通过其余改造的话,这两个Mysql实例极可能会生成一样的ID。须要单独给每一个Mysql实例配置不一样的起始值和自增步长。

第一台Mysql实例配置:

set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长

第二台Mysql实例配置:

set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长

通过上面的配置后,这两个Mysql实例生成的id序列以下: mysql1,起始值为1,步长为2,ID生成的序列为:1,3,5,7,9,... mysql2,起始值为2,步长为2,ID生成的序列为:2,4,6,8,10,...

对于这种生成分布式ID的方案,须要单独新增一个生成分布式ID应用,好比DistributIdService,该应用提供一个接口供业务应用获取ID,业务应用须要一个ID时,经过rpc的方式请求DistributIdService,DistributIdService随机去上面的两个Mysql实例中去获取ID。

实行这种方案后,就算其中某一台Mysql实例下线了,也不会影响DistributIdService,DistributIdService仍然能够利用另一台Mysql来生成ID。

可是这种方案的扩展性不太好,若是两台Mysql实例不够用,须要新增Mysql实例来提升性能时,这时就会比较麻烦。

如今若是要新增一个实例mysql3,要怎么操做呢? 第一,mysql一、mysql2的步长确定都要修改成3,并且只能是人工去修改,这是须要时间的。 第二,由于mysql1和mysql2是不停在自增的,对于mysql3的起始值咱们可能要定得大一点,以给充分的时间去修改mysql1,mysql2的步长。 第三,在修改步长的时候极可能会出现重复ID,要解决这个问题,可能须要停机才行。

为了解决上面的问题,以及可以进一步提升DistributIdService的性能,若是使用第三种生成分布式ID机制。

号段模式

咱们可使用号段的方式来获取自增ID,号段能够理解成批量获取,好比DistributIdService从数据库获取ID时,若是能批量获取多个ID并缓存在本地的话,那样将大大提供业务应用获取ID的效率。

好比DistributIdService每次从数据库获取ID时,就获取一个号段,好比(1,1000],这个范围表示了1000个ID,业务应用在请求DistributIdService提供ID时,DistributIdService只须要在本地从1开始自增并返回便可,而不须要每次都请求数据库,一直到本地自增到1000时,也就是当前号段已经被用完时,才去数据库从新获取下一号段。

因此,咱们须要对数据库表进行改动,以下:

CREATE TABLE id_generator (
  id int(10) NOT NULL,
  current_max_id bigint(20) NOT NULL COMMENT '当前最大id',
  increment_step int(10) NOT NULL COMMENT '号段的长度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这个数据库表用来记录自增步长以及当前自增ID的最大值(也就是当前已经被申请的号段的最后一个值),由于自增逻辑被移到DistributIdService中去了,因此数据库不须要这部分逻辑了。

这种方案再也不强依赖数据库,就算数据库不可用,那么DistributIdService也能继续支撑一段时间。可是若是DistributIdService重启,会丢失一段ID,致使ID空洞。

为了提升DistributIdService的高可用,须要作一个集群,业务在请求DistributIdService集群获取ID时,会随机的选择某一个DistributIdService节点进行获取,对每个DistributIdService节点来讲,数据库链接的是同一个数据库,那么可能会产生多个DistributIdService节点同时请求数据库获取号段,那么这个时候须要利用乐观锁来进行控制,好比在数据库表中增长一个version字段,在获取号段时使用以下SQL:

update id_generator set current_max_id=#{newMaxId}, version=version+1 where version = #{version}

由于newMaxId是DistributIdService中根据oldMaxId+步长算出来的,只要上面的update更新成功了就表示号段获取成功了。

为了提供数据库层的高可用,须要对数据库使用多主模式进行部署,对于每一个数据库来讲要保证生成的号段不重复,这就须要利用最开始的思路,再在刚刚的数据库表中增长起始值和步长,好比若是如今是两台Mysql,那么 mysql1将生成号段(1,1001],自增的时候序列为1,3,4,5,7.... mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10...

更详细的能够参考滴滴开源的TinyId:github.com/didi/tinyid…

在TinyId中还增长了一步来提升效率,在上面的实现中,ID自增的逻辑是在DistributIdService中实现的,而实际上能够把自增的逻辑转移到业务应用本地,这样对于业务应用来讲只须要获取号段,每次自增时再也不须要请求调用DistributIdService了。

雪花算法

上面的三种方法总的来讲是基于自增思想的,而接下来就介绍比较著名的雪花算法-snowflake。

咱们能够换个角度来对分布式ID进行思考,只要能让负责生成分布式ID的每台机器在每毫秒内生成不同的ID就好了。

snowflake是twitter开源的分布式ID生成算法,是一种算法,因此它和上面的三种生成分布式ID机制不太同样,它不依赖数据库。

核心思想是:分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,原始snowflake算法中对于bit的分配以下图:

雪花算法

  • 第一个bit位是标识部分,在java中因为long的最高位是符号位,正数是0,负数是1,通常生成的ID为正数,因此固定为0。
  • 时间戳部分占41bit,这个是毫秒级的时间,通常实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可使产生的ID从更小值开始;41位的时间戳可使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
  • 工做机器id占10bit,这里比较灵活,好比,可使用前5位做为数据中心机房标识,后5位做为单机房机器标识,能够部署1024个节点。
  • 序列号部分占12bit,支持同一毫秒内同一个节点能够生成4096个ID

根据这个算法的逻辑,只须要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用能够直接使用该工具方法来获取分布式ID,只需保证每一个业务应用有本身的工做机器id便可,而不须要单独去搭建一个获取分布式ID的应用。

snowflake算法实现起来并不难,提供一个github上用java实现的:github.com/beyondfengy…

在大厂里,其实并无直接使用snowflake,而是进行了改造,由于snowflake算法中最难实践的就是工做机器id,原始的snowflake算法须要人工去为每台机器去指定一个机器id,并配置在某个地方从而让snowflake今后处获取机器id。

可是在大厂里,机器是不少的,人力成本太大且容易出错,因此大厂对snowflake进行了改造。

百度(uid-generator)

github地址:uid-generator

uid-generator使用的就是snowflake,只是在生产机器id,也叫作workId时有所不一样。

uid-generator中的workId是由uid-generator自动生成的,而且考虑到了应用部署在docker上的状况,在uid-generator中用户能够本身去定义workId的生成策略,默认提供的策略是:应用启动时由数据库分配。说的简单一点就是:应用在启动时会往数据库表(uid-generator须要新增一个WORKER_NODE表)中去插入一条数据,数据插入成功后返回的该数据对应的自增惟一id就是该机器的workId,而数据由host,port组成。

对于uid-generator中的workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,须要注意的是,和原始的snowflake不太同样,时间的单位是秒,而不是毫秒,workId也不同,同一个应用每重启一次就会消费一个workId。

具体可参考github.com/baidu/uid-g…

美团(Leaf)

github地址:Leaf

美团的Leaf也是一个分布式ID生成框架。它很是全面,即支持号段模式,也支持snowflake模式。号段模式这里就不介绍了,和上面的分析相似。

Leaf中的snowflake模式和原始snowflake算法的不一样点,也主要在workId的生成,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每一个应用在使用Leaf-snowflake时,在启动时都会都在Zookeeper中生成一个顺序Id,至关于一台机器对应一个顺序节点,也就是一个workId。

总结

总得来讲,上面两种都是自动生成workId,以让系统更加稳定以及减小人工成功。

Redis

这里额外再介绍一下使用Redis来生成分布式ID,其实和利用Mysql自增ID相似,能够利用Redis中的incr命令来实现原子性的自增与返回,好比:

127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id      // 增长1,并返回
(integer) 2
127.0.0.1:6379> incr seq_id      // 增长1,并返回
(integer) 3

使用redis的效率是很是高的,可是要考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。

RDB持久化至关于定时打一个快照进行持久化,若是打完快照后,连续自增了几回,还没来得及作下一次快照持久化,这个时候Redis挂掉了,重启Redis后会出现ID重复。

AOF持久化至关于对每条写命令进行持久化,若是Redis挂掉了,不会出现ID重复的现象,可是会因为incr命令过得,致使重启恢复数据时间过长。

 

出处:http://www.javashuo.com/article/p-mqxkfswy-ck.html

相关文章
相关标签/搜索