在分布式系统中,当数据库数据量达到一定量级的时候,需要进行数据拆分、分库分表操作,传统使用方式的数据库自有的自增特性产生的主键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语法,例如创建如下表:
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服务,会产生资源浪费;每次都从数据库获取,不高效。
4.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,下面以堆缓存为例。
publi c 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。
END