mybatis-plus 中的主键策略
大家一定都使用过下面的注解
value="id", type=IdType.ASSIGN_ID) (privateStringid;
type属性支持多种主键策略,其中IdType.ASSIGN_ID就是使用基于雪花算法的策略生成id
mybatis-plus源码处理
上面的注解设置了值,对应在MybatisDefaultParameterHandler中有判断这个类型的处理,判断tableInfo.getIdType() == IdType.ID_WORKER时,使用IdWorker.getId()创建了一个雪花算法ID
这里查看使用的是 3.1.0版本源码,所以是 ID_WORKER
protectedstaticObjectpopulateKeys(MetaObjectHandlermetaObjectHandler, TableInfotableInfo, MappedStatementms, ObjectparameterObject, booleanisInsert) { if (null==tableInfo) { /* 不处理 */returnparameterObject; } /* 自定义元对象填充控制器 */MetaObjectmetaObject=ms.getConfiguration().newMetaObject(parameterObject); // 填充主键if (isInsert&&!StringUtils.isEmpty(tableInfo.getKeyProperty()) &&null!=tableInfo.getIdType() &&tableInfo.getIdType().getKey() >=3) { ObjectidValue=metaObject.getValue(tableInfo.getKeyProperty()); /* 自定义 ID */if (StringUtils.checkValNull(idValue)) { if (tableInfo.getIdType() ==IdType.ID_WORKER) { metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId()); } elseif (tableInfo.getIdType() ==IdType.ID_WORKER_STR) { metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr()); } elseif (tableInfo.getIdType() ==IdType.UUID) { metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID()); } } } if (metaObjectHandler!=null) { if (isInsert&&metaObjectHandler.openInsertFill()) { // 插入填充metaObjectHandler.insertFill(metaObject); } elseif (!isInsert) { // 更新填充metaObjectHandler.updateFill(metaObject); } } returnmetaObject.getOriginalObject(); }
其中 Sequence 类是雪花算法真正实现类
雪花算法简单介绍
雪花算法是Twitter设计的根据时间戳、机器标识码和序列号生成的唯一长整型数。
使用一个 64 bit 的 long 型的数字作为全局唯一 id。这 64 个 bit 中,其中 1 个 bit 是不用的,然后用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。
- 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
- 41bit-时间戳,用来记录时间戳,毫秒级。
- 10bit-工作机器id,用来记录工作机器id。
- 12bit-序列号,序列号,用来记录同毫秒内产生的不同id。即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
SnowFlake 算法的优点:
- 高性能高可用:生成时不依赖于数据库,完全在内存中生成
- 高吞吐:每秒钟能生成数百万的自增 ID
- ID 自增:存入数据库中,索引效率高
SnowFlake 算法的缺点:
- 依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成 ID 冲突或者重复
- 其中10bit-工作机器id,如果手动设置重复也可能会造成 ID 冲突或者重复
Sequence
针对雪花算法中存在的问题,mybatis-plus做了相应的优化
针对时钟回拨
允许一定的回拨范围
针对工作机器id
提供了一个无参数构造
在没有设置机器id时,会通过当前物理网卡地址和jvm的进程id自动生成。一般在一个集群中,MAC+JVM进程PID一样的几率非常小
protectedstaticlonggetDatacenterId(longmaxDatacenterId) { longid=0L; try { InetAddressip=InetAddress.getLocalHost(); NetworkInterfacenetwork=NetworkInterface.getByInetAddress(ip); if (network==null) { id=1L; } else { byte[] mac=network.getHardwareAddress(); if (null!=mac) { id= ((0x000000FF& (long) mac[mac.length-1]) | (0x0000FF00& (((long) mac[mac.length-2]) <<8))) >>6; id=id% (maxDatacenterId+1); } } } catch (Exceptione) { logger.warn(" getDatacenterId: "+e.getMessage()); } returnid; }
protectedstaticlonggetMaxWorkerId(longdatacenterId, longmaxWorkerId) { StringBuildermpid=newStringBuilder(); mpid.append(datacenterId); Stringname=ManagementFactory.getRuntimeMXBean().getName(); if (StringUtils.isNotEmpty(name)) { /** GET jvmPid*/mpid.append(name.split(StringPool.AT)[0]); } /** MAC + PID 的 hashcode 获取16个低位*/return (mpid.toString().hashCode() &0xffff) % (maxWorkerId+1); }
针对获取时间
高并发场景下System.currentTimeMillis()的性能问题
在Sequence中获取当前时间是通过 SystemClock.now(),SystemClock类中主要是通过使用单个调度线程来按毫秒更新时间戳
publicclassSystemClock { privatefinallongperiod; privatefinalAtomicLongnow; privateSystemClock(longperiod) { this.period=period; this.now=newAtomicLong(System.currentTimeMillis()); scheduleClockUpdating(); } privatestaticSystemClockinstance() { returnInstanceHolder.INSTANCE; } publicstaticlongnow() { returninstance().currentTimeMillis(); } privatevoidscheduleClockUpdating() { ScheduledExecutorServicescheduler=Executors.newSingleThreadScheduledExecutor(runnable-> { Threadthread=newThread(runnable, "System Clock"); thread.setDaemon(true); returnthread; }); scheduler.scheduleAtFixedRate(() ->now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS); } }