MyBatis Plus
中提供了 ASSIGN_ID
这种方式生成主键,使用起来非常方便,只要在PO
上定义一下就可以了,例如:
1public class Order extends Model<Order> {
2
3 @TableId(value = "id", type = IdType.ASSIGN_ID)
4 private Long id;
5
6 //...
7}
这样在保存的时候,MyBatis Plus
会负责生成主键值,自动设置到字段id
中。
一直好奇这个主键是怎么生成的,抽时间查看了一下源码,特意记录一下。
1. MybatisDefaultParameterHandler
通过看MyBatis Plus
的文档,发现这个主键生成过程应该是在ParameterHandler
中进行处理的。在MyBatis Plus
中ParameterHandler
是用来设置参数规则的,当StatementHandler
调用prepare
方法之后,接下来就是调用它来进行设置参数。
对ASSIGN_ID
进行处理的的地方是在MybatisDefaultParameterHandler
中(在3.4.x版本中,该类改为MybatisParameterHandler
),该类在包com.baomidou.mybatisplus.core
中。对主键处理的代码在populateKeys
这个方法中:
1 /**
2 * 填充主键
3 *
4 * @param tableInfo 数据库表反射信息
5 * @param metaObject 元数据对象
6 * @param entity 实体信息
7 */
8 protected static void populateKeys(TableInfo tableInfo, MetaObject metaObject, Object entity) {
9 final IdType idType = tableInfo.getIdType();
10 final String keyProperty = tableInfo.getKeyProperty();
11 if (StringUtils.isNotBlank(keyProperty) && null != idType && idType.getKey() >= 3) {
12 final IdentifierGenerator identifierGenerator = GlobalConfigUtils.getGlobalConfig(tableInfo.getConfiguration()).getIdentifierGenerator();
13 Object idValue = metaObject.getValue(keyProperty);
14 // 此处判断是否idValue是否存在,不存在的话就要根据类型进行计算赋值
15 if (StringUtils.checkValNull(idValue)) {
16 if (idType.getKey() == IdType.ASSIGN_ID.getKey()) { // 此处处理ASSIGN_ID类型的主键生成算法
17 if (Number.class.isAssignableFrom(tableInfo.getKeyType())) {
18 // 此处调用IdentitifierGenerator生成主键
19 metaObject.setValue(keyProperty, identifierGenerator.nextId(entity));
20 } else {
21 metaObject.setValue(keyProperty, identifierGenerator.nextId(entity).toString());
22 }
23 } else if (idType.getKey() == IdType.ASSIGN_UUID.getKey()) { // 此处处理ASSIGN_UUID类型的主键生成算法
24 metaObject.setValue(keyProperty, identifierGenerator.nextUUID(entity));
25 }
26 }
27 }
28 }
2. DefaultIdentifierGenerator
此类在包com.baomidou.mybatisplus.core.incrementer
中。代码如下:
1public class DefaultIdentifierGenerator implements IdentifierGenerator {
2
3 private final Sequence sequence;
4
5 public DefaultIdentifierGenerator() {
6 this.sequence = new Sequence();
7 }
8
9 public DefaultIdentifierGenerator(long workerId, long dataCenterId) {
10 this.sequence = new Sequence(workerId, dataCenterId);
11 }
12
13 public DefaultIdentifierGenerator(Sequence sequence) {
14 this.sequence = sequence;
15 }
16
17 @Override
18 public Long nextId(Object entity) {
19 // 此处调用Sequence的nextId算法生成真正的主键值
20 return sequence.nextId();
21 }
22}
3. Sequence
此类在包com.baomidou.mybatisplus.core.toolkit
中。注意:以下代码进行了缩减。
1public class Sequence {
2 /**
3 * 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
4 */
5 private final long twepoch = 1288834974657L;
6 /**
7 * 机器标识位数
8 */
9 private final long workerIdBits = 5L;
10 private final long datacenterIdBits = 5L;
11 private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
12 private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
13 /**
14 * 毫秒内自增位
15 */
16 private final long sequenceBits = 12L;
17 private final long workerIdShift = sequenceBits;
18 private final long datacenterIdShift = sequenceBits + workerIdBits;
19 /**
20 * 时间戳左移动位
21 */
22 private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
23 private final long sequenceMask = -1L ^ (-1L << sequenceBits);
24
25
26 public Sequence() {
27 this.datacenterId = getDatacenterId(maxDatacenterId);
28 this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
29 }
30
31
32 /**
33 * 数据标识id部分
34 */
35 protected static long getDatacenterId(long maxDatacenterId) {
36 long id = 0L;
37 try {
38 InetAddress ip = InetAddress.getLocalHost();
39 NetworkInterface network = NetworkInterface.getByInetAddress(ip);
40 if (network == null) {
41 id = 1L;
42 } else {
43 byte[] mac = network.getHardwareAddress();
44 if (null != mac) {
45 // mac地址最后两节,例如对Mac地址00:15:5d:2d:0b:01,下面的结果是id=0x0b01
46 id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
47 // 这里是计算除以32后得到的余数
48 id = id % (maxDatacenterId + 1);
49 }
50 }
51 } catch (Exception e) {
52 logger.warn(" getDatacenterId: " + e.getMessage());
53 }
54 return id;
55 }
56
57 /**
58 * 获取 maxWorkerId
59 */
60 protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
61 StringBuilder mpid = new StringBuilder();
62 mpid.append(datacenterId);
63 // 此处返回的是the name representing the running Java virtual machine
64 String name = ManagementFactory.getRuntimeMXBean().getName();
65 if (StringUtils.isNotBlank(name)) {
66 /*
67 * GET jvmPid
68 */
69 mpid.append(name.split(StringPool.AT)[0]);
70 }
71 /*
72 * MAC + PID 的 hashcode 获取16个低位
73 */
74 return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
75 }
76
77 /**
78 * 获取下一个 ID
79 *
80 * @return 下一个 ID
81 */
82 public synchronized long nextId() {
83 long timestamp = timeGen();
84 //闰秒
85 if (timestamp < lastTimestamp) {
86 long offset = lastTimestamp - timestamp;
87 if (offset <= 5) {
88 try {
89 wait(offset << 1);
90 timestamp = timeGen();
91 if (timestamp < lastTimestamp) {
92 throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
93 }
94 } catch (Exception e) {
95 throw new RuntimeException(e);
96 }
97 } else {
98 throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
99 }
100 }
101
102 if (lastTimestamp == timestamp) {
103 // 相同毫秒内,序列号自增
104 sequence = (sequence + 1) & sequenceMask;
105 if (sequence == 0) {
106 // 同一毫秒的序列数已经达到最大
107 timestamp = tilNextMillis(lastTimestamp);
108 }
109 } else {
110 // 不同毫秒内,序列号置为 1 - 3 随机数
111 sequence = ThreadLocalRandom.current().nextLong(1, 3);
112 }
113
114 lastTimestamp = timestamp;
115
116 // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
117 return ((timestamp - twepoch) << timestampLeftShift)
118 | (datacenterId << datacenterIdShift)
119 | (workerId << workerIdShift)
120 | sequence;
121 }
122}
跟踪到这里就可以看到具体的算法了。具体来说,该算法就是Twitter
的雪花算法的变种。最后生成的主键值是一个64位长整数,其具体定义如下:
- 最高1位未用,总为0,这样能够保证生成的主键值永远为整数;
- 然后是41位时间戳,其值为UTC毫秒数减去一个固定值:
1288834974657L
; - 然后是5位datacenterId,从上面的算法来看是从主机的mac地址后2段计算出来的;
- 紧接着是5位的workerId;
- 最后是12位顺序号。也就是一毫秒时间内最多能够生成4096个主键值。
与Twitter
的雪花算法的唯一区别是其41位的时间戳值多减去了一个固定值:1288834974657L
。