首先明确的一点是,主键是为了区分不同的行记录,所以先抛开其他的因素,主键必须要保证:唯一性(单表或者分库分表的场景下)
单表
可选的方案有:
- 自增id
- UUID
- 业务字段,如:手机号、身份证号等等
自增id
自增主键是单表中很常用的使用方式。阿里Java开发中规定,表必备三字段: id,gmt_create,gmt_modified。说明:其中id必为主键,类型为unsigned bigint(8个字节)、单表时自增、步长为 1(这里后面还会提到)。【1】
优点:
- 递增,聚集索引的性能更好
- 节省空间(相比于UUID、业务字段)
缺点:
- 不利于迁移,如将数据表迁移到其他服务器中,这种场景下解决方案:【2】
- 目标表的表结构与原表一致,只是主键不设置为递增
- 将原表的数据迁移到目标表中
- 最后将主键设置为递增即可
- ( [1] 数据迁移的过程)
- ( [2] 迁移的过程中有数据的修改该如何处理?)
- 不利于扩展,如:需要做分表时不能使用自增id
总结:单表的情况下没啥毛病
UUID
UUID (Universally Unique Identifier),UUID包含:【3】
- 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同
- 时钟序列
- 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得
java.util中也提供了生成UUID的方法,生成的格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)
UUID的特点:
- 无序
- 绝大部分情况下唯一,高并发的场景下可能也会出现重复的情况
优点:
- 相比于自增id更加安全
缺点:
- 无序,所以在进行insert操作时对于性能会有影响【5】
- 相比于按顺序写入,随机写入时需要写的page可能已经不在buffer pool中了,这样InnoDB需要将目标page再读到内存中去,产生了大量的随机IO
- 由于写入是乱序的,InnoDB可能需要进行频繁的页分裂操作,需要分配新页,移动数据,还有可能向上引发一系列的页分裂
- 由于频繁的页分裂,可能会造成page中的碎片过大
- 相对浪费空间
业务字段
如手机号,不过尽可能避免使用业务字段作为主键
缺点:
- 无序
- 可能有变化,比如有这种需求:手机号需要加密存储,且如果该字段和其他表有依赖,那么需要改很多地方
分库分表
当我们单表的数据量很大时,可能需要分库分表,那么首先要解决的问题是如何生成主键,可选的方案有:
- UUID
- 单机数据库自增ID
- Redis自增ID
- 跳跃式自增ID
- 雪花算法
UUID
前面提到过,并不适合做主键
单机数据库自增ID
即所有的库和表都依赖于一个某个表的自增ID,这样生成的性能瓶颈在于数据库。( [3] AUTO_INCREMENT锁机制 )
Redis自增ID
依赖Redis INCR
跳跃式自增ID
比如分十个表:tb0,tb1,...,tb9,设置初始值为:1,2,...,10,自增步长设置为10即可。
缺点:再次扩展时不好处理,除非实现预估未来多少多少年数据量不会使得10个表成为瓶颈
雪花算法
雪花算法生成的ID一共64位(2进制),最高位为保留位,后41位为时间戳(最多约69年),后10位为机器id(共1024个),最后12位为序列号(即同一个时刻可以生成4096个不同的id)
简单的代码为:
/**
* 格式:x-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx
*/
public class SnowFlake {
private static long lastTimestamp = -1L;
/**
* 初始时间戳,根据业务设置
* <p>
* 如果没有这个初始时间戳的话,可用的时间少于69年
*/
private static long startTimestamp;
/**
* 数据中心id
*/
private static long dataCenterId;
/**
* 机器id
*/
private static long workerId;
/**
* 序列号
*/
private static long sequence;
/**
* 工作id长度为5位
*/
private static long workerIdBits = 5L;
/**
* 数据中心id长度为5位
*/
private static long dataCenterIdBits = 5L;
/**
* 序列号长度
*/
private static long sequenceBits = 12L;
/**
* 工作id最大值
* <p>
* 原理:
* -1L的二进制为:1111....111,共64位
* 左移5位:1111....1100000
* 再亦或:11111
*/
private static long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 数据中心id最大值
*/
private static long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
/**
* 序列号最大值
*/
private static long maxSequence = -1L ^ (-1L << sequenceBits);
/**
* 时间戳需要左移位数 12+5+5=22位
*/
private static long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
/**
* 数据id需要左移位数 12+5=17位
*/
private static long dataCenterIdShift = sequenceBits + workerIdBits;
/**
* 工作id需要左移的位数,12位
*/
private static long workerIdShift = sequenceBits;
public synchronized static Long get() {
long timestamp = System.currentTimeMillis();
if (timestamp == lastTimestamp) {
// 这里需要提前预估好,每一毫秒生成的id会不会超过4096个
sequence = (sequence + 1) & maxSequence;
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - startTimestamp) << timestampLeftShift) |
(dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
public static void main(String[] args) {
System.out.println(SnowFlake.get());
}
}
缺点:雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态
美团Leaf
【7】
上面的问题
- 待整理...
- 待整理...
说明
===================================
仅作为校招时的《个人笔记》,详细内容请看【参考】部分
===================================
参考
- https://pdai.tech/md/dev-spec/code-style/code-style-alibaba.html
- https://blog.csdn.net/dinghua_xuexi/article/details/106075183
- https://www.cnblogs.com/java-class/p/4727698.html
- https://www.cnblogs.com/funnyzpc/p/13541713.html
- 《高性能MySQL》(第3版)
- https://www.cnblogs.com/jajian/p/11101213.html 最后发现一篇总结的很好的博文
- https://tech.meituan.com/2017/04/21/mt-leaf.html