赞
踩
根据当前项目推进的情况,我们会发现用户注册时,从来没有考虑主键生成的问题。为什么呢?因为咱们的数据表现在都是通过数据库自增长方式获取主键id的。不过,这个主键自增长的方案好不好呢?我们一起来了解一下程序发展的现状:
所有的数据表几乎都是采用自增长主键策略。 用户量大,数据库存储数据量变大。海量数据,int作为主键自增,最大值上限问题!
多数据库服务器的部署模式:分库分表,主键自增长策略有问题!多个拆分子表出现相同的主键值。
解决方案:
排除主键自增,获取唯一特征的序列,常见解决方案:
1 UUID
优点:算法写好,使用简单易入门,全局唯一
作为主键缺点:字符串类型,索引有排序的特征 UUID字符长度过长 UUID算法基于MAC地址
比较好的主键,具备哪些特点,才适合我们数据库主键处理呢?
一个『好』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 的各个位都有固定的规范:
面试常问:如果是并发量高,同一台机器一毫秒有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); //31 private static final long dataCenterIdShift = sequenceBits + workerIdBits; //17 private 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) & sequenceMask sequence = (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对象中主键的生成方式
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。