当关系型数据库数据量过大时,通常会采用分库分表降低数据库查表压力。分库分表有多种,有分一个库多张分表额,有分多个库多张表的。一般分库分表使用ShardingSphere分表,建分片键等。但是分库分表之后,主键ID如何处理呢?相同业务表不同分表的主键ID是不可以相同的。所以要考虑一下主键ID如何赋值的问题。
有以下几种我了解或者涉及到过的主键ID的处理方式:
这种方式一般会将主键设置为bitint类型,自增的。但是会存在一个问题,多张分表保证主键不冲突,因为在业务上来说,多张分表的数据组成某个业务,因此主键是不允许冲突的。
当采用自动生成主键ID的方案时,可以设置固定的几张分表,每个分表的起点不一样,每次新增的步长一样,这样就可以保证每张分表的主键不冲突。
举例,如某张表分表有10张,可以设置每张表的起始主键ID从1到10,每张分表主键ID递增步长为10。
表名 | 起始主键ID | 步长 |
---|---|---|
table_1 | 1 | 10 |
table_2 | 2 | 10 |
table_3 | 3 | 10 |
table_4 | 4 | 10 |
table_5 | 5 | 10 |
table_6 | 6 | 10 |
table_7 | 7 | 10 |
table_8 | 8 | 10 |
table_9 | 9 | 10 |
table_10 | 10 | 10 |
根据上面分表主键递增规律,每张表的行数如下递增
表名 | 第一条数据 | 第二条 | 第三条 | 第四条 | 第五条 |
---|---|---|---|---|---|
table_1 | 1 | 11 | 21 | 31 | 41 |
table_2 | 2 | 12 | 22 | 32 | 42 |
table_3 | 3 | 13 | 23 | 33 | 43 |
table_4 | 4 | 14 | 24 | 34 | 44 |
table_5 | 5 | 15 | 25 | 35 | 45 |
table_6 | 6 | 16 | 26 | 36 | 46 |
table_7 | 7 | 17 | 27 | 37 | 47 |
table_8 | 8 | 18 | 28 | 38 | 48 |
table_9 | 9 | 19 | 29 | 39 | 49 |
table_10 | 10 | 20 | 30 | 40 | 50 |
按照主键递增格式有弊端,即新增表时,不好处理主键逻辑。这种主键ID递增的方式适用于分表比较固定的情况。
uuid获取方式:
String id = UUID.randomUUID();
结果:
647be5bd-a477-4eff-8e58-99a573bb14ec
在前几年的时候,uuid作为主键的表遍地都是,因为它数据范围之广,用法方便受很多人青睐。但是uuid长度为36位,即使去掉中间的“-”,长度也有32位,因此比较占用存储空间。
因为uuid是无序的,因此新增到数据库时,数据表如果采用btree索引,那么每次新增一条数据都需要重新排序,比较费时间,因此uuid作为分表主键也是不太推荐的。
采用SnowFlake算法生成唯一id,包含时间戳,工作中心id,数据中心id,序列号组成,结构如下:
(1)一位占位符:默认为0。最高位代表正负,1代表负数,0代表正数,默认为正数。
(2)41位时间戳:毫秒级的时间,可以存69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
(3)5位工作中心id:十进制范围在0-31;5位数据中心id:十进制范围在0-31。两个组合在一起最多可以容纳1024个节点。
(4)序列号:占用12bit,最多可以累加到4095。自增值支持同一毫秒内同一个节点可以生成4096个ID,这个值在同一毫秒同一节点上从0开始不断累加。(最大可以支持单节点差不多四百万的并发量)
java一般使用hutool中的IdUtil类生成雪花算法id。以下是代码解析
(1)IdUtil
public class IdUtil {
public IdUtil() {
}
/** @deprecated 创建Snowflake对象,已废弃 */
@Deprecated
public static Snowflake createSnowflake(long workerId, long datacenterId) {
return new Snowflake(workerId, datacenterId);
}
/**
* 根据工作中心id和数据中心id获取数据
*
* @author zhouxy
* @date 2023/5/12
*/
public static Snowflake getSnowflake(long workerId, long datacenterId) {
return (Snowflake) Singleton.get(Snowflake.class, new Object[]{workerId, datacenterId});
}
/**
* 根据工作中心id获取数据
*
* @author zhouxy
* @date 2023/5/12
*/
public static Snowflake getSnowflake(long workerId) {
return (Snowflake) Singleton.get(Snowflake.class, new Object[]{workerId});
}
/**
* 无参获取Snowflake
*
* @author zhouxy
* @date 2023/5/12
*/
public static Snowflake getSnowflake() {
return (Snowflake) Singleton.get(Snowflake.class, new Object[0]);
}
/**
* 根据数据中心id计算算法id中的数据中心值
*
* @author zhouxy
* @date 2023/5/12
*/
public static long getDataCenterId(long maxDatacenterId) {
Assert.isTrue(maxDatacenterId > 0L, "maxDatacenterId must be > 0", new Object[0]);
//9223372036854775807L转成二进制,由63位的1组成
if (maxDatacenterId == 9223372036854775807L) {
--maxDatacenterId;
}
long id = 1L;
byte[] mac = null;
try {
mac = NetUtil.getLocalHardwareAddress();
} catch (UtilException var6) {
}
if (null != mac) {
id = (255L & (long) mac[mac.length - 2] | 65280L & (long) mac[mac.length - 1] << 8) >> 6;
//取余
id %= maxDatacenterId + 1L;
}
return id;
}
/**
* 获取算法id中的工作中心id
*
* @author zhouxy
* @date 2023/5/12
*/
public static long getWorkerId(long datacenterId, long maxWorkerId) {
StringBuilder mpid = new StringBuilder();
mpid.append(datacenterId);
try {
mpid.append(RuntimeUtil.getPid());
} catch (UtilException var6) {
}
return (long) (mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);
}
/**
* 无参获取算法id
*
* @author zhouxy
* @date 2023/5/12
*/
public static long getSnowflakeNextId() {
return getSnowflake().nextId();
}
/**
* 无参获取字符串类型算法id
*
* @author zhouxy
* @date 2023/5/12
*/
public static String getSnowflakeNextIdStr() {
return getSnowflake().nextIdStr();
}
}
(2) Snowflake类(核心类)
public class Snowflake implements Serializable {
private static final long serialVersionUID = 1L;
public static long DEFAULT_TWEPOCH = 1288834974657L; //时间戳,二进制为41位,对应时间为2010-11-04 09:42:54
public static long DEFAULT_TIME_OFFSET = 2000L; //允许时钟回拨差值,两秒
private static final long WORKER_ID_BITS = 5L; //工作中心位数
private static final long MAX_WORKER_ID = 31L; //最大工作中心id值
private static final long DATA_CENTER_ID_BITS = 5L; //数据中心位数
private static final long MAX_DATA_CENTER_ID = 31L; //最大数据中心id值
private static final long SEQUENCE_BITS = 12L; //序列化位数
private static final long WORKER_ID_SHIFT = 12L; //工作中心移动位数(计算id用)
private static final long DATA_CENTER_ID_SHIFT = 17L; //工作中心移动位数((计算id用)计算id用)
private static final long TIMESTAMP_LEFT_SHIFT = 22L; //时间戳移动位数
private static final long SEQUENCE_MASK = 4095L; //序列化最大值
private final long twepoch;
private final long workerId;
private final long dataCenterId;
private final boolean useSystemClock;
private final long timeOffset;
private final long randomSequenceLimit;
private long sequence;
private long lastTimestamp;
/**
* 无参构造
*
* @author zhouxy
* @date 2023/5/12
*/
public Snowflake() {
this(cn.hutool.core.util.IdUtil.getWorkerId(cn.hutool.core.util.IdUtil.getDataCenterId(31L), 31L));
}
/**
* 根据工作中心id构造
*
* @author zhouxy
* @date 2023/5/12
*/
public Snowflake(long workerId) {
this(workerId, IdUtil.getDataCenterId(31L));
}
/**
* 根据工作中心id和数据中心id构造
*
* @author zhouxy
* @date 2023/5/12
*/
public Snowflake(long workerId, long dataCenterId) {
this(workerId, dataCenterId, false);
}
/**
* 参数:工作中心id、数据中心id、是否采用系统时间
*
* @author zhouxy
* @date 2023/5/12
*/
public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {
this((Date) null, workerId, dataCenterId, isUseSystemClock);
}
/**
* 参数:时间,工作中心id、数据中心id、是否采用系统时间
*
* @author zhouxy
* @date 2023/5/12
*/
public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {
this(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET);
}
/**
* 参数:工作中心id、数据中心id、是否采用系统时间, 允许时针回拨的时长
*
* @author zhouxy
* @date 2023/5/12
*/
public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {
this(epochDate, workerId, dataCenterId, isUseSystemClock, timeOffset, 0L);
}
/**
* 参数:工作中心id、数据中心id、是否采用系统时间, 允许时针回拨的时长,序列化
*
* @author zhouxy
* @date 2023/5/12
*/
public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset, long randomSequenceLimit) {
this.sequence = 0L;
this.lastTimestamp = -1L;
this.twepoch = null != epochDate ? epochDate.getTime() : DEFAULT_TWEPOCH;
this.workerId = Assert.checkBetween(workerId, 0L, 31L);
this.dataCenterId = Assert.checkBetween(dataCenterId, 0L, 31L);
this.useSystemClock = isUseSystemClock;
this.timeOffset = timeOffset;
this.randomSequenceLimit = Assert.checkBetween(randomSequenceLimit, 0L, 4095L);
}
/**
* 获取工作中心id
*
* @author zhouxy
* @date 2023/5/12
*/
public long getWorkerId(long id) {
return id >> 12 & 31L;
}
/**
* 获取数据中心id
*
* @author zhouxy
* @date 2023/5/12
*/
public long getDataCenterId(long id) {
return id >> 17 & 31L;
}
/**
* 获取时间戳
*
* @author zhouxy
* @date 2023/5/12
*/
public long getGenerateDateTime(long id) {
return (id >> 22 & 2199023255551L) + this.twepoch;
}
/**
* 获取id
*
* @author zhouxy
* @date 2023/5/12
*/
public synchronized long nextId() {
//获取当前时间戳,默认取项目时间
long timestamp = this.genTime();
if (timestamp < this.lastTimestamp) {
//校验时间回拨差值是否大于配置的差值,若是,则报错
if (this.lastTimestamp - timestamp >= this.timeOffset) {
throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{this.lastTimestamp - timestamp}));
}
//将上次时间戳赋值给当前时间
timestamp = this.lastTimestamp;
}
//设置序列化号
if (timestamp == this.lastTimestamp) {
//设置序列化号,上次序列化号+1之后与4095进行与计算。
long sequence = this.sequence + 1L & 4095L;
if (sequence == 0L) {
// 毫秒内序列溢出(序列化号已满,说明当前秒的序列化号都已被占用过) 阻塞到下一个毫秒,获得新的时间戳
timestamp = this.tilNextMillis(this.lastTimestamp);
}
//记录当前的序列化号
this.sequence = sequence;
} else if (this.randomSequenceLimit > 1L) {
//当timestamp > this.lastTimestamp且this.randomSequenceLimit > 1L,则随便设置一个不大于等于randomSequenceLimit的值赋予sequence
this.sequence = RandomUtil.randomLong(this.randomSequenceLimit);
} else {
//当timestamp > this.lastTimestamp且this.randomSequenceLimit <= 1时,则默认设置sequence=0
this.sequence = 0L;
}
//将计算完的时间戳赋予全局变量
this.lastTimestamp = timestamp;
//将时间戳(当前时间戳-默认的时间,这样可以时间戳值的范围更大些),数据中心,工作中心,序列化拼接在一起组成id。
return timestamp - this.twepoch << 22 | this.dataCenterId << 17 | this.workerId << 12 | this.sequence;
}
/**
* 获取字符串算法id
*
* @author zhouxy
* @date 2023/5/12
*/
public String nextIdStr() {
return Long.toString(this.nextId());
}
/**
* 计算下一个时间
*
* @author zhouxy
* @date 2023/5/12
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp;
//计算时间,目的是算出当前时间,最大只能等于传进来的参数时间
for (timestamp = this.genTime(); timestamp == lastTimestamp; timestamp = this.genTime()) {
}
if (timestamp < lastTimestamp) {
throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{lastTimestamp - timestamp}));
} else {
return timestamp;
}
}
/**
* 生成时间戳
*
* @author zhouxy
* @date 2023/5/12
*/
private long genTime() {
//SystemClock.now() 获取当前项目计算的时间
//System.currentTimeMillis()获取的是当前系统时间
return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
}
}
上面是hutool中提供的雪花算法,与原始的雪花算法区别在于,hutool中允许一定范围的时间回拨。
hutool中的雪花算法的优缺点:
优点
(1)按照时间排序,则数据库存储时不需要重复排序变动存储位置。
(2)可使用范围长,时间戳的位数41位,可支持69年。
缺点
(1)允许时针回拨,在某些极端情况下会产生重复id
结语:除了上面的几种id生成算法,当然还有其他的主键id生成算法,具体使用哪种需要根据业务的情况来使用。