在日常的java应用开发中经常会用到ID作为表的主键或唯一标识入库,那么常见的ID的生成方式会用到如下几种:UUID生成的ID、mysql数据库的自增ID、雪花算法生成的ID、redis生成的ID。
当业务表的数据量小于500W或容量小于2G的时候单独一个mysql即可提供服务(redis实现10w级并发),若再大点进行读写分离也可以缓解单库存储的数据量压力和访问的并发压力;但当主从同步(读写分离)也扛不住时就需要分库分表了,那么分库分表就需要一个全局唯一ID来标识一条数据,UUID、自增ID就不能满足需求了,需要雪花算法、redis的方式生成的ID(分布式ID)。
分布式ID的一些特性:
* 全局唯一
* 高性能,高可用低延时,生成要快
* 好接入,接入要简单,拿来即用
* 趋势递增
UUID代码示例:
public static void main(String[] args) {
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid);
}
结果如下:d7e0f5f82e484c5a890d4a657ecfaa1f,字符长度为36个字符,若去掉-则为32个字符,没有任何意义。生成简单快速,但查询性能不好。
mysql自增ID代码示例:
CREATE DATABASE `User_ID`;
CREATE TABLE User_ID.SEQUENCE_ID (
`id` bigint(20) unsigned NOT NULL auto_increment,
`value` char(10) NOT NULL default '',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
) ENGINE=MyISAM;
insert into SEQUENCE_ID(value) VALUES ('values');
当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,无法保证并发请求。
redis生成ID代码示例:
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回递增后的数值
(integer) 2
redis 也同样可以实现,原理就是Redis 操作是单线程的,因此我们可以利用redis的incr
命令实现ID的原子性自增。并且可以解决并发的场景。但注意持久化的问题,持久化方式有rdb(快照,保存整体)和aof(记录,保存操作)两种。
雪花算法生成ID代码示例:
public class SnowFlakeDemo {
/**
* 开始时间截 (2015-01-01)
*/
private final long twepoch = 1420041600000L;
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
private final long dataCenterIdBits = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxWorkerId = ~(-1L << workerIdBits);
/**
* 支持的最大机房标识id,结果是31
*/
private final long maxDataCenterId = ~(-1L << dataCenterIdBits);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 机房标识id向左移17位(12+5)
*/
private final long dataCenterIdShift = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = ~(-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private volatile long workerId;
/**
* 机房中心ID(0~31)
*/
private volatile long dataCenterId;
/**
* 毫秒内序列(0~4095)
*/
private volatile long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private volatile long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param dataCenterId 机房中心ID (0~31)
*/
public SnowFlakeDemo(long workerId, long dataCenterId)
{
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;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* 如果一个线程反复获取Synchronized锁,那么synchronized锁将变成偏向锁。
*
* @return SnowflakeId
*/
public synchronized long nextId() throws RuntimeException
{
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < 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;
//毫秒内序列溢出,一毫秒内超过了4095个
if (sequence == 0)
{
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
else
{
//时间戳改变,毫秒内序列重置
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)
| (dataCenterId << dataCenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private long tilNextMillis(long lastTimestamp)
{
long timestamp = timeGen();
while (timestamp <= lastTimestamp)
{
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
private long timeGen()
{
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlakeDemo snowFlakeDemo = new SnowFlakeDemo(11, 11);
long id = snowFlakeDemo.nextId();
System.out.println("id:" + id);
}
}
SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。众所周知世界上没有一对相同的雪花。雪花算法基本上保持自增的。
优点:
- 高性能高可用:生成时不依赖于数据库,完全在内存中生成。
- 容量大:每秒中能生成数百万的自增ID。
- ID自增:存入数据库中,索引效率高。
缺点:
- 依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。
目前各大互联网公司也推出了自己的ID生成的方法,如百度uid-generator,项目GitHub地址:https://github.com/baidu/uid-generator;美团(Leaf),github地址:https://github.com/Meituan-Dianping/Leaf;滴滴(Tinyid),Github地址:https://github.com/didi/tinyid。