分布式ID解决方案

为什么使用分布式ID

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

可用方案

UUID

UUID是指在一台机器上生成的数字,主要由当前日期和时间、时钟序列和全局唯一的IEEE机器识别号组合而成,由一组32位数的16进制数字所构成,是故UUID理论上的总数为16^32=2^128,约等于3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。

优点:简单易用、高效;

缺点:32位的长度太长;使用16进制表示,可读性差;无序,不利于排序。

Twitter-Snowflake

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

优点:有序、高效;

缺点:自主开发。

MySQL

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

CREATE TABLE DISTRIBUTE_ID

(

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

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

PRIMARY KEY (ID),

UNIQUE KEY UK_PURPOSES (PURPOSES)

) ENGINE=InnoDB;;

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

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

SELECT LAST_INSERT_ID();

当然这些都是数据库层面的操作,需要将其封装成服务接口,调用接口即可获取ID。如果需要防止单点故障问题,可以部署两个数据库服务,同时给两个数据库的两个表设置不同的初始值和自增步长。

优点:数据库自增机制,可靠、有序;

缺点:如果多服务器只提供获取ID服务,会产生资源浪费;每次都从数据库获取,不高效。

MySQL+缓存

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

CREATE TABLE DISTRIBUTE_ID

(

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

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

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

MAX_VALUE BIGINT NOT NULL COMMENT '最大值',

UNIQUE KEY UK_PURPOSES (PURPOSES)

) ENGINE=InnoDB;

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

public class DistributeId implements Serializable {

private String purposes;

private long minValue;

private long maxValue;

private AtomicLong currentValue;

public void setMinValue(long minValue) {

this.minValue = minValue;

this.currentValue = new AtomicLong(minValue);

}

//省略其它setter

//获取下一个ID

public long getAndIncrement() {

long nextValue = currentValue.getAndIncrement();

if (nextValue > maxValue) {

return 0;

}

return nextValue;

}

}

public class DistributeIdUtil {

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

private Lock lock = new ReentrantLock();

public long generateId(String purposes) {

DistributeId distributeId = distributeIds.get(purposes);

if(null == distributeId){

queryDB(purposes);

return generateId(purposes);

}

long id = distributeId.getAndIncrement();

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

if(id == 0){

distributeIds.remove(purposes);

queryDB(purposes);

return generateId(purposes);

}

return id;

}

private void queryDB(String purposes){

lock.lock();

try{

DistributeId distributeId = distributeIds.get(purposes);

if(null != distributeId){

return;

}

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

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

distributeId = ...;

distributeIds.put(purposes,distributeId);

}inally {

lock.unlock();

}

}

}

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

相关文章: