三分钟学习分布式ID方案

235 阅读4分钟
原文链接: mp.weixin.qq.com

为什么使用分布式ID

在分布式系统中,当数据库数据量达到一定量级的时候,需要进行数据拆分、分库分表操作,传统使用方式的数据库自有的自增特性产生的主键ID已不能满足拆分的需求,它只能保证在单个表中唯一,所以需要一个在分布式环境下都能使用的全局唯一ID。

可用方案

1.UUID UUID是指在一台机器上生成的数字,主要由当前日期和时间、时钟序列和全局唯一的IEEE机器识别号组合而成,由一组32位数的16进制数字所构成,是故UUID理论上的总数为16^32=2^128,约等于3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。 优点:简单易用、高效; 缺点:32位的长度太长;使用16进制表示,可读性差;无序,不利于排序。

2.Twitter-Snowflake Snowflake是Twitter公司设计的一套全局唯一ID生成算法。根据算法设计生成的ID共64位,第一位始终为0暂未使用,接着的41位为时间序列(精确到毫秒,41位的长度可以使用69年),紧接着的10位为机器标识(10位的长度最多支持部署1024个节点),最后的12位为计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)。 优点:有序、高效; 缺点:自主开发。

3.MySQL 既然传统使用方式下的数据库自增特性不能满足需求,不如设计单独的库表,单独提供产生全局ID的服务,利用auto_increment特性和replace into语法,例如创建如下表:

  1.   CREATE TABLE DISTRIBUTE_ID

  2. (

  3.  ID BIGINT(25) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '全局ID',

  4.  PURPOSES VARCHAR(30) NOT NULL DEFAULT '' COMMENT '用途',

  5.  PRIMARY KEY (ID),

  6.  UNIQUE KEY UK_PURPOSES (PURPOSES)

  7. ) ENGINE=InnoDB;

当需要产生全局ID时,执行如下SQL:

  1.   REPLACE INTO DISTRIBUTE_ID (PURPOSES) VALUES ('PAYMENT');

  2. SELECT 1310976;

当然这些都是数据库层面的操作,需要将其封装成服务接口,调用接口即可获取ID。如果需要防止单点故障问题,可以部署两个数据库服务,同时给两个数据库的两个表设置不同的初始值和自增步长。 优点:数据库自增机制,可靠、有序; 缺点:如果多服务器只提供获取ID服务,会产生资源浪费;每次都从数据库获取,不高效。

4.MySQL+缓存 使用MySQL实现的方式有两个缺点,一个是产生资源浪费,一个是不高效。其实,按实际来说,能用前来解决的问题就不算问题,所以第一个不需要太关心,那就剩下效率的问题。既然不高效的原因是每次都操作数据库,那么就减少操作数据库,每次取批量的数据,并结合缓存使用。可以创建如下表:

  1.   CREATE TABLE DISTRIBUTE_ID

  2. (

  3.  PURPOSES VARCHAR(30) NOT NULL DEFAULT '' COMMENT '用途',

  4.  INCREMENT INT NOT NULL DEFAULT 1 COMMENT ‘增长步长',

  5.  MIN_VALUE BIGINT NOT NULL DEFAULT 1 COMMENT '最小值',

  6.  MAX_VALUE BIGINT NOT NULL COMMENT '最大值',

  7.  UNIQUE KEY UK_PURPOSES (PURPOSES)

  8. ) ENGINE=InnoDB;

在使用之前初始化数据,设置增长步长、最小值和最大值,编写类用于封装这些数据,以PURPOSES值为key,类实例为value,将key-value存放到缓存中,可以使用堆缓存,也可以使用分布式缓存如Redis,下面以堆缓存为例。

  1.   publi c class DistributeId implements Serializable {

  2.    private String purposes;

  3.    private long minValue;

  4.    private long maxValue;

  5.    private AtomicLong currentValue;

  6.    public void setMinValue(long minValue) {

  7.        this.minValue = minValue;

  8.        this.currentValue = new AtomicLong(minValue);

  9.    }

  10.    //省略其它setter

  11.    //获取下一个ID

  12.    public long getAndIncrement() {

  13.        long nextValue = currentValue.getAndIncrement();

  14.        if (nextValue > maxValue) {

  15.            return 0;

  16.        }

  17.        return nextValue;

  18.    }

  19. }

  20. public class DistributeIdUtil {

  21.    private Map<String, DistributeId> distributeIds = new HashMap<>();

  22.    private Lock lock = new ReentrantLock();

  23.    public long generateId(String purposes) {

  24.        DistributeId distributeId = distributeIds.get(purposes);

  25.        if(null == distributeId){

  26.            queryDB(purposes);

  27.            return generateId(purposes);

  28.        }

  29.        long id = distributeId.getAndIncrement();

  30.        //超过最大值,重新从数据库获取

  31.        if(id == 0){

  32.            distributeIds.remove(purposes);

  33.            queryDB(purposes);

  34.            return generateId(purposes);

  35.        }

  36.        return id;

  37.    }    

  38.    private void queryDB(String purposes){

  39.        lock.lock();

  40.        try{

  41.            DistributeId distributeId = distributeIds.get(purposes);

  42.            if(null != distributeId){

  43.                return;

  44.            }

  45.            // SET MIN_VALUE = MAX_VALUE+1,MAX_VALUE = MAX_VALUE+INCREMENT

  46.            // 此处省略数据库操作,可以使用存储过程完成

  47.            distributeId = ...;

  48.            distributeIds.put(purposes,distributeId);

  49.        }inally {

  50.            lock.unlock();

  51.        }

  52.    }

  53. }

如果需要防止单点故障问题,部署两个需要注意设置不同步长,同时代码中的自增操作需要换成getAndAdd。

END 

如果觉得有收获,记得关注、点赞、转发。