mybatis-plus 中的主键策略
大家一定都使用过下面的注解
value = "id", type = IdType.ASSIGN_ID) ( private String id;
type属性支持多种主键策略,其中IdType.ASSIGN_ID就是使用基于雪花算法的策略生成id
mybatis-plus源码处理
上面的注解设置了值,对应在MybatisDefaultParameterHandler中有判断这个类型的处理,判断tableInfo.getIdType() == IdType.ID_WORKER时,使用IdWorker.getId()创建了一个雪花算法ID
这里查看使用的是 3.1.0版本源码,所以是 ID_WORKER
protected static Object populateKeys(MetaObjectHandler metaObjectHandler, TableInfo tableInfo, MappedStatement ms, Object parameterObject, boolean isInsert) { if (null == tableInfo) { /* 不处理 */ return parameterObject; } /* 自定义元对象填充控制器 */ MetaObject metaObject = ms.getConfiguration().newMetaObject(parameterObject); // 填充主键 if (isInsert && !StringUtils.isEmpty(tableInfo.getKeyProperty()) && null != tableInfo.getIdType() && tableInfo.getIdType().getKey() >= 3) { Object idValue = metaObject.getValue(tableInfo.getKeyProperty()); /* 自定义 ID */ if (StringUtils.checkValNull(idValue)) { if (tableInfo.getIdType() == IdType.ID_WORKER) { metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId()); } else if (tableInfo.getIdType() == IdType.ID_WORKER_STR) { metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr()); } else if (tableInfo.getIdType() == IdType.UUID) { metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID()); } } } if (metaObjectHandler != null) { if (isInsert && metaObjectHandler.openInsertFill()) { // 插入填充 metaObjectHandler.insertFill(metaObject); } else if (!isInsert) { // 更新填充 metaObjectHandler.updateFill(metaObject); } } return metaObject.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一样的几率非常小
protected static long getDatacenterId(long maxDatacenterId) { long id = 0L; try { InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = 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 (Exception e) { logger.warn(" getDatacenterId: " + e.getMessage()); } return id; }
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) { StringBuilder mpid = new StringBuilder(); mpid.append(datacenterId); String name = 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类中主要是通过使用单个调度线程来按毫秒更新时间戳
public class SystemClock { private final long period; private final AtomicLong now; private SystemClock(long period) { this.period = period; this.now = new AtomicLong(System.currentTimeMillis()); scheduleClockUpdating(); } private static SystemClock instance() { return InstanceHolder.INSTANCE; } public static long now() { return instance().currentTimeMillis(); } private void scheduleClockUpdating() { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { Thread thread = new Thread(runnable, "System Clock"); thread.setDaemon(true); return thread; }); scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS); } }