根据当前项目推进的情况,我们会发现用户注册时,从来没有考虑主键生成的问题。为什么呢?因为咱们的数据表现在都是通过数据库自增长方式获取主键id的。不过,这个主键自增长的方案好不好呢?我们一起来了解一下程序发展的现状:
-
所有的数据表几乎都是采用自增长主键策略。 用户量大,数据库存储数据量变大。海量数据,int作为主键自增,最大值上限问题!
-
多数据库服务器的部署模式:分库分表,主键自增长策略有问题!多个拆分子表出现相同的主键值。
解决方案:
排除主键自增,获取唯一特征的序列,常见解决方案:
1 UUID
优点:算法写好,使用简单易入门,全局唯一
作为主键缺点:字符串类型,索引有排序的特征 UUID字符长度过长 UUID算法基于MAC地址
比较好的主键,具备哪些特点,才适合我们数据库主键处理呢?
一个『好』ID 的标准应该有哪些:
- 最好是由纯数字组成。
- 越短越好,最好能存进整型变量和数据库的整型字段中。
- 信息安全。另外,『ID 连续』并非好事情。
- 在不连续的情况下,最好是递增的。即便不是严格递增,至少也应该是趋势递增。
Twitter 的雪花算法(SnowFlake)
Snowflake 是 Twitter(美国推特公司)开源的分布式 ID 生成算法。最初 Twitter 把存储系统从 MySQL 迁移到 Cassandra(它是NoSQL数据库),因为Cassandra 没有顺序 ID 生成机制,所以 Twitter 开发了这样一套全局唯一 ID 生成服务。那为什么要叫雪花算法呢?据相关研究表示,一般的雪花大约由10的19次方个水分子组成。在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己独特的形状。雪花算法的意思是表示生成的ID如雪花一般独一无二。
SnowFlake 优点:
整体上按照时间自增排序,并且整个分布式系统内不会产生 ID 碰撞(由数据中心 ID 和机器 ID 作区分),并且效率较高。经测试,SnowFlake 每秒能够产生 26 万 ID 左右。
Snowflake 会生成一个 long 类型的数值,long是8个字节,一共是64位,Snowflake 对于 long 的各个位都有固定的规范:
- 最高位标识(1 位)
由于 long 基本类型在 Java 中是带符号的,最高位是符号位,正数是 0,负数是 1,因为 id 一般是正数,所以最高位是 0 。 - 毫秒级时间戳(41 位)
注意,41 位时间戳不是存储当前时间的时间戳,而是存储时间的差值(当前时间戳 - 开始时间戳) 得到的值,这里的的开始时间,一般是我们的 id 生成器开始使用的时间,由我们程序来指定的(如下面程序 IdGenerator 类的 startTime 属性)。
41 位的时间截,可以使用 69 年。
2的41次方 除以 (1000毫秒 * 60 * 60 * 24 * 365) = 69 - 数据机器位(10 位)
10-bit机器可以分别表示1024台机器,这 10 位的机器位实际上是由 5 位的 互联网数据中心(datacenterId) 和 5 位的工作机器id(workerId) 。这样就可以有32个互联网数据中心(机房)(2的5次方),每个互联网数据中心可以有32台工作机器 。即,总共允许存在 1024 台电脑各自计算 ID 。
每台电脑都由 data-center-id 和 worker-id 标识,逻辑上类似于联合主键的意思。 - 12位的自增序列号,用来记录同毫秒内产生的不同id,就是一毫秒内最多可以产生4096个id
毫秒内的计数,12为的自增序列号 支持每个节点每毫秒(同一机器,同一时间截)产生 4096(2的12次方) 个 ID 序号,这种分配方式可以保证在任何一个互联网数据中心的任何一台工作机器在任意毫秒内生成的ID都是不同的
面试常问:如果是并发量高,同一台机器一毫秒有5000个id,那么id会不会重复,不会,根据源码如果一毫秒内超过4096个id,则会阻塞到下一毫秒再生成
Snowflake 实现源码
public class SnowflakeIdGenerator {// ==============================Fields===========================================// 所占位数、位移、掩码/极大值private static final long sequenceBits = 12; //序列号占用位数private static final long workerIdBits = 5; //工作机器占用位数private static final long dataCenterIdBits = 5; //数据中心占用位数(机房)//~表示非,例如 01 的非 10 负数的二进制 = 该正数的二进制取反+1//为什么不直接写4095呢?(主要计算机运算的时候是二进制,如果写4095的话,还是要转二进制,效率低)private static final long sequenceMask = ~(-1L << sequenceBits); //4095 (0到4095 刚好是4096个) private static final long workerIdShift = sequenceBits; //12 private static final long workerIdMask = ~(-1L << workerIdBits); //31private static final long dataCenterIdShift = sequenceBits + workerIdBits; //17private static final long dataCenterIdMask = ~(-1L << dataCenterIdBits); //31 private static final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;//22//private static final long timestampBits = 41L; //private static final long timestampMask = ~(-1L << timestampBits);//2199023255551/*** 开始时间截 (2015-01-01) 1420070400000L/1000/60/60/24/30/12 = 25+1970 = 2015-01-01*/private static final long twepoch = 1420070400000L;private long sequence = 0; //序列号private long workerId; //工作机器标识private long dataCenterId; //数据中心private long lastTimestamp = -1L; //上次生成 ID 的时间截//==============================Constructors=====================================public SnowflakeIdGenerator() {this(0, 0);}/*** 构造函数** @param workerId 工作ID (0~31)* @param dataCenterId 数据中心 ID (0~31)*/public SnowflakeIdGenerator(long workerId, long dataCenterId) {if (workerId > workerIdMask || workerId < 0) {throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", workerIdMask));} this.workerId = workerId;this.dataCenterId = dataCenterId;}// ============================== Methods ==========================================/*** 获得下一个 ID (该方法是线程安全的,synchronized)*/public synchronized long nextId() {long timestamp = timeGen(); //获取当前服务器时间// 如果当前时间小于上一次 ID 生成的时间戳,说明系统时钟回退过,这个时候应当抛出异常。// 出现这种原因是因为系统的时间被回拨,或出现闰秒现象。// 你也可以不抛出异常,而是调用 tilNextMillis 进行等待if (timestamp < lastTimestamp) {//时间回拨 闰秒//睡3秒Thread.sleep(3000)timestamp = timeGen();if(timestamp < lastTimestamp){throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}// 如果是同一时间生成的,则时并发量高的情况下,同一毫秒内最大支持4096个id,否则阻塞到下一秒生成if (lastTimestamp == timestamp) {// 相同毫秒内,序列号自增 , sequence = 4095时, 0 = (sequence + 1) & sequenceMasksequence = (sequence + 1) & sequenceMask; // 毫秒内序列溢出,即,同一毫秒的序列数已经达到最大if (sequence == 0) { //序号位4096个序号用完了// 阻塞到下一个毫秒,获得新的时间戳timestamp = tilNextMillis(lastTimestamp);}}// 时间戳改变,毫秒内序列重置else {sequence = 0L;}// 将当前生成的时间戳记录为『上次时间戳』。『下次』生成时间戳时要用到。lastTimestamp = timestamp;// 移位并通过或运算拼到一起组成 64 位的 ID = 8个字节return ((timestamp - twepoch) << timestampLeftShift) // 时间毫秒数左移22位| (dataCenterId << dataCenterIdShift) //数据中心节点左移17位| (workerId << workerIdShift) // 机器节点左移12位| sequence;}/*** 阻塞到下一个毫秒,直到获得新的时间戳** @param lastTimestamp 上次生成ID的时间截* @return 当前时间戳*/protected long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();//获取当前时间戳while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 阻塞到下一个毫秒,直到获得新的时间戳** @param timestamp 当前时间错* @param lastTimestamp 上次生成ID的时间截* @return 当前时间戳*/protected long tilNextMillis(long timestamp, long lastTimestamp) {while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 返回以毫秒为单位的当前时间** @return 当前时间(毫秒)*/protected long timeGen() {return System.currentTimeMillis();}//==============================Test=============================================/*** 测试*/public static void main(String[] args) {System.out.println(System.currentTimeMillis());SnowflakeIdGenerator idWorker = new SnowflakeIdGenerator(1, 1);long startTime = System.nanoTime();for (int i = 0; i < 50000; i++) {long id = idWorker.nextId();System.out.println(id);}System.nanoTime(); //获取当前纳秒System.out.println((System.nanoTime() - startTime) / 1000000 + "ms");}
}
解决时间回拨问题
原生的 Snowflake 算法是完全依赖于时间的,如果有时钟回拨的情况发生,会生成重复的 ID,市场上的解决方案也是不少。简单粗暴的办法有:
-
最简单的方案,就是关闭生成唯一 ID 机器的时间同步。这样做,是不是也不合理,服务器时间好像就跟“标准时间”不一样了
-
如果发现有时钟回拨,时间很短比如 5 毫秒,就等待,然后再生成。或者就直接报错,交给业务层去处理。也可以采用 SonyFlake 的方案,精确到 10 毫秒,以 10 毫秒为分配单元。
-
采用直接抛异常方式:上面就是这种方式,虽然可行,但是这种很不友好,太粗暴10秒以内
-
使用阿里云的的时间服务器和自己的服务器进行同步,2017 年 1 月 1 日的闰秒调整,阿里云服务器 NTP 系统 24 小时“消化”闰秒,完美解决了问题。
[root@localhost ~]# ntpdate ntp1.aliyun.com -
如果发现有时钟回拨,时间很短比如 3 毫秒(一般大于3毫秒就不建议等待),就等待(线程睡3秒再来生成id),然后再生成。
public synchronized long nextId() {
long timestamp = timeGen(); //获取当前服务器时间if (timestamp < lastTimestamp) {Thread.sleep(3000)timestamp = timeGen();if(timestamp < lastTimestamp){throw new RuntimeException(String.format("Clock moved backw ....", lastTimestamp - timestamp));}}......
}
3 mybatis plus实现雪花id
mybatis-plus已经内置雪花算法生成分布式唯一id。在mybatis-plus特性中已经明确说明了这点。
mybatis-plus中主键生成策略有以下几种:
案例:注册用户时用户id修改为雪花算法
java程序中如何生成雪花ID呢?操作步骤如下所示:
修改数据表的主键类型和主键生成策略
修改PO对象中主键的生成方式