前言
在系统的数据量足够达到一定程度的时候,需要进行一个分库分表的操作,将一个大的数据表分解成多个小表来增加查询速度。这种情况这几个小表的id要达到一个唯一性,单表的自增id就无法满足需求。需要通过一些复杂的操作,来达到多个表id唯一的需求。
一、UUID
UUID全局唯一标识符,定义为一个字符串主键,采用32位数字组成,编码采用16进制,是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。id示例:
38830405-1057-4282-a3e0-cb4184a9424c
java生成方式
String uid = UUID.randomUUID().toString();
UUID不是绝对的唯一,只是重复的概率很低。如果每秒产生10亿笔UUID,100年后只产生一次重复的机率是50%。如果地球上每个人都各有6亿笔UUID,发生一次重复的机率是50%。在这种很低的情况下,可以认为是唯一的。
优点:
- 生成简单,本机即可生成
- 数据迁移方便,因为每次生成的都是唯一的。
缺点:
- 字符串id不可读,根本看不出是啥意思
- 在数据库使用时,不能和数据库索引友好的结合,导致查询效率低下。
- 每个都是32位的字符串,占用空间较大。
二、mysql自增设置步长
在数据库中经常会使用数据库自增来做主键,分表之后,通过设置自增长的步长来达到两个表主键唯一的效果。
例如:
t_user0表从1开始,步长为2,自增长的id会是1,3,5,7。
t_user1表从2开始,步长为2,自增长的id会是2,4,6,8。
如此实现比较简单,相对与uuid,主键是可读的,而且查询的时候也可以很好的结合数据库的索引。
弊端也很明显,如果想扩容一张表就比较麻烦。所以一般在不需要表扩容的场景使用。个人感觉这个使用较少。
三、Snowflke
snowflake是Twitter开源的分布式ID生成算法。主要解决和优化了mysql自增键在数据分片后主键重复的问题。即:同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。
snowflake算法固定生成一个long类型的数字。
- 第一位是标识位,正数的时候是0,负数的时候是1,所以一般情况下是0。
- 41位来表示时间戳,这个是毫秒级别的。
- 10位表示机器标识,说明同一张表的id,可以在1024个机器上同时生成。实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化,成业务表或者和我们系统相关的业务标识。
- 最后12位是自增id,说明是同一毫秒产生的id数量。12位说明最多每毫秒可以生成4095个id。
优点:
- 不占用宽带
- 本地生成
- 高位是毫秒,低位递增
缺点:
- 强依赖时钟,如果时间回拨,数据递增不安全(美团通过zookeeper来防止时间不一致)
雪花算法代码示例:
public class SnowFlake {
/**
* 起始的时间戳
*/
private final static long START_STMP = 1288834974657L;
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATACENTER_BIT = 5;//数据中心占用的位数
/**
* 每一部分的最大值
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId; //数据中心
private long machineId; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
/**
* 产生下一个ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
private long getNewstmp() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(2, 3);
for (int i = 0; i < (1 << 12); i++) {
System.out.println(snowFlake.nextId());
}
}
}
四、redis原子增
Redis Incr 命令将 key 中储存的数字值增一。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
dev:0>incr aa
“1”dev:0>incr aa
“2”dev:0>incr aa
“3”
这种方式与jvm中的AtomicInteger
类似。
例如:
key:表名+年份+当天当年第多少天+天数+小时
value:年份+当天当年第多少天+天数+小时+incr自增(5位,自动补全)
得到:
key:user2021817
value:202181700021
说明user表在2020年第218天17点的时候,17点到18点这段时间内生成了第21个id。2020只需要后面的20即可。
优点:
- 有序递增,可读性强
- 相对于mysql自增来说,易扩展。
缺点:
- 占用宽带(网络)
- 要保证redis的可用性
- 要考虑redis的持久化方式RDB/AOF,RDB是定时持久化,如果刚刚生成的数据,没即时持久化,数据会丢失。
- 另外incr是5位自动补全,说明每个小时最多生成99999个id,如果没小时超过这个数之后就会出现异常,这时就要根据自己的场景调节位数,避免出现id重复。
总结
如此看来,雪花算法的方式和redis自增都是可以满足订单表这种大数据量表主键的生成,但是在实际场景中也要灵活一点,要考虑到这两种方式的弊端,还要考虑是不是还有其他更好的实现方式。因为主键只要保证唯一即可,并不需要死板的按照上面的那些方式。例如,订单id也可以通过时间戳+用户id
,生成的订单id是不是更加友好。