分布式ID

UUID/GUID

UUID,适合小规模的分布式环境
•大部分数据库系统都支持uuid
•优势
•可以实现跨表,跨库,甚至跨服务器的唯一标识
•多数据库之间数据汇总简单方便
•可以多服务器,分布式部署
•可以独立于数据库单独产生
•能够实现多种复制方案
•不足
•占用空间大,16byte
•产生的ID,可读性差,无法排序
在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能

优点:搭建比较简单,不需要为主键唯一性的处理。

缺点:占用两倍的存储空间(在云上光存储一块就要多花2倍的钱),后期读写性能下降厉害。

Sharding-JDBC

DefaultKeyGenerator,默认的主键生成器。该生成器采用 Twitter Snowflake 算法实现,生成 64 Bits 的 Long 型编号。国内另外一款数据库中间件 MyCAT 分布式主键也是基于该算法实现。国内很多大型互联网公司发号器服务基于该算法加部分改造实现。所以 DefaultKeyGenerator 必须是根正苗红。

Redis 生成 ID

要知道redis的EVAL,EVALSHA命令:
当使用数据库来生成 ID 性能不够要求的时候,可以尝试使用 Redis 来生成ID。这主要依赖于 Redis 是单线程的,所以也可以用生成全局唯一的 ID。可以用 Redis 原子操作 INCR 和 INCRBY 来实现。可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有 5台 Redis,可以初始化每台 Redis 的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为 :
A : 1,6,11,16,21...
B : 2,7,12,17,22...
C : 3,8,13,18,23...
D : 4,9,14,19,24...
E : 5,10,15,20,25...
随便负载到哪个机确定好,未来很难做修改。但是 3-5 台服务器基本能够满足器上,都可以获得不同的 ID。但是步长和初始值一定要事先确定,使用Redis集群也可以防止单点故障的问题。另外,比较适合使用 Redis 来生成每天从 0 开始的流水号,比如 订单号=日期+当日自增长号。可以每天在 Redis 中生成一个 Key,使用 INCR进行累加
优点 : 1> 不依赖于数据库,灵活方便,且性能优于数据库
          2> 数字ID天然排序,对分页或者需要排序的结果很有帮助
缺点 : 1> 如果系统中没有 Redis,还需要引入新的组件,增加系统复杂度
          2> 需要编码和配置的工作量比较大
来自Flicker的解决方案

自增ID主键+步长,适合中等规模的分布式场景

优点是:实现简单,后期维护简单,对应用透明。

 

缺点是:第一次设置相对较为复杂,因为要针对未来业务的发展而计算好足够的步长;


因为MySQL本身支持auto_increment操作,很自然地,我们会想到借助这个特性来实现这个功能。
Flicker在解决全局ID生成方案里就采用了MySQL自增长ID的机制(auto_increment + replace into + MyISAM)。一个生成64位ID方案具体就是这样的:
先创建单独的数据库(eg:ticket),然后创建一个表:

CREATE TABLE Tickets64 (

id bigint(20) unsigned NOT NULL auto_increment,

stub char(1) NOT NULL default '',

PRIMARY KEY (id),

UNIQUE KEY stub (stub)

) ENGINE=MyISAM

当我们插入记录后,执行SELECT * from Tickets64,查询结果就是这样的:

+-------------------+------+
| id | stub |
+-------------------+------+
| 72157623227190423 | a |
+-------------------+------+
在我们的应用端需要做下面这两个操作,在一个事务会话里提交:

REPLACE INTO Tickets64 (stub) VALUES ('a');

SELECT LAST_INSERT_ID();

这样我们就能拿到不断增长且不重复的ID了。
到上面为止,我们只是在单台数据库上生成ID,从高可用角度考虑,接下来就要解决单点故障问题:Flicker启用了两台数据库服务器来生成ID,通过区分auto_increment的起始值和步长来生成奇偶数的ID。

TicketServer1:

auto-increment-increment = 2

auto-increment-offset = 1

 

TicketServer2:

auto-increment-increment = 2

auto-increment-offset = 2

最后,在客户端只需要通过轮询方式取ID就可以了。

优点:充分借助数据库的自增ID机制,提供高可靠性,生成的ID有序。
缺点:占用两个独立的MySQL实例,有些浪费资源,成本较高。

雪花算法自造全局自增ID,适合大数据环境的分布式场景

由twitter公布的开源的分布式id算法snowflake(Java版本)

IdWorker.java:

import org.slf4j.Logger; 

import org.slf4j.LoggerFactory;

 

public class IdWorker {

   

    protected static final Logger LOG = LoggerFactory.getLogger(IdWorker.class);

    

    private long workerId;

    private long datacenterId;

    private long sequence = 0L;

 

    private long twepoch = 1288834974657L;

 

    private long workerIdBits = 5L;

    private long datacenterIdBits = 5L;

    private long maxWorkerId = -1L ^ (-1L << workerIdBits);

    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 IdWorker(long workerId, long datacenterId) {

        // sanity check for workerId

        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;

        LOG.info(String.format("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId));

    }

 

    public synchronized long nextId() {

        long timestamp = timeGen();

 

        if (timestamp < lastTimestamp) {

            LOG.error(String.format("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) {

            sequence = (sequence + 1) & sequenceMask;

            if (sequence == 0) {

                timestamp = tilNextMillis(lastTimestamp);

            }

        } else {

            sequence = 0L;

        }

 

        lastTimestamp = timestamp;

 

        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;

    }

 

    protected long tilNextMillis(long lastTimestamp) {

        long timestamp = timeGen();

        while (timestamp <= lastTimestamp) {

            timestamp = timeGen();

        }

        return timestamp;

    }

 

    protected long timeGen() {

        return System.currentTimeMillis();

    }

}

测试生成ID的测试类,IdWorkerTest.java:

import java.util.HashSet;

import java.util.Set;

 

public class IdWorkerTest {

          

    static class IdWorkThread implements Runnable {

        private Set<Long> set;

        private IdWorker idWorker;

 

        public IdWorkThread(Set<Long> set, IdWorker idWorker) {

            this.set = set;

            this.idWorker = idWorker;

        }

 

        public void run() {

            while (true) {

                long id = idWorker.nextId();

                System.out.println("            real id:" + id);

                if (!set.add(id)) {

                    System.out.println("duplicate:" + id);

                }

            }

        }

    }

 

    public static void main(String[] args) {

        Set<Long> set = new HashSet<Long>();

        final IdWorker idWorker1 = new IdWorker(0, 0);

        final IdWorker idWorker2 = new IdWorker(1, 0);

        Thread t1 = new Thread(new IdWorkThread(set, idWorker1));

        Thread t2 = new Thread(new IdWorkThread(set, idWorker2));

        t1.setDaemon(true);

        t2.setDaemon(true);

        t1.start();

        t2.start();

        try {

            Thread.sleep(30000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}

总结

(1)单实例或者单节点组:

经过500W、1000W的单机表测试,自增ID相对UUID来说,自增ID主键性能高于UUID,磁盘存储费用比UUID节省一半的钱。所以在单实例上或者单节点组上,使用自增ID作为首选主键。

 

(2)分布式架构场景:

         20个节点组下的小型规模的分布式场景,为了快速实现部署,可以采用多花存储费用、牺牲部分性能而使用UUID主键快速部署;

 

         20到200个节点组的中等规模的分布式场景,可以采用自增ID+步长的较快速方案。

 

         200以上节点组的大数据下的分布式场景,可以借鉴类似twitter雪花算法构造的全局自增ID作为主键。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值