一、用数据库主键自增生成订单ID
数据库主键顺序自增,每天有多少订单量被竞争对手看得一清二楚,商业机密都暴露了。 况且单机MySQL只能支持几百量级的并发,如果系统每天千万订单量,完全hold不住。
二、用数据库集群
数据库集群,自增ID起始值按机器编号,步长等于机器数量。比如有两台机器,第一台机器生成的ID是1、3、5、7,第二台机器生成的ID是2、4、6、8。性能不行就加机器,这并发量一下就上去了。
但实现百万级的并发,大概就需要2000台机器,这还只是用来生成订单ID,公司再有钱也经不起这么造。
三、号段模式
然MySQL的并发量不行,我们是不是可以提前从MySQL获取一批自增ID,加载到本地内存中,然后从内存中并发取。这种叫号段模式。并发量是上去了,但是自增ID还是不能作为订单ID的。
四、用Java自带UUID
import java.util.UUID;
/**
* @author yideng
* @apiNote UUID示例
*/
public class UUIDTest {
public static void main(String[] args) {
String orderId = UUID.randomUUID().toString().replace("-", "");
System.out.println(orderId);
}
}
输出结果:
58e93ecab9c64295b15f7f4661edcbc1
也不行。32位字符串会占用更大的空间,无序的字符串作数据库主键,每次插入数据库的时候,MySQL为了维护B+树结构,需要频繁调整节点顺序,影响性能。况且字符串太长,也没有任何业务含义,pass。
五、生成订单ID要满足的条件
全局唯一:如果订单ID重复了,肯定要完蛋。
高性能:要做到高并发、低延迟。生成订单ID都成为瓶颈了,那还得了。
高可用:至少要做到4个9,别动不动就宕机了。
易用性:如果为了满足上述要求,搞了几百台服务器,复杂且难以维护,也不行。
数值且有序递增:数值占用的空间更小,有序递增能保证插入MySQL的时候更高性能。
嵌入业务含义:如果订单ID里面能嵌入业务含义,就能通过订单ID知道是哪个业务线生成的,便于排查问题。
六、雪花算法生成订单ID
一种流传已久的分布式、高性能、高可用的订单ID生成算法—雪花算法,完全能满足你的上述要求。雪花算法生成ID是Long类型,长度64位。
- 第 1 位: 符号位,暂时不用。
- 第 2~42 位:共41位,时间戳,单位是毫秒,可以支撑大约69年
- 第 43~52 位:共10位,机器ID,最多可容纳1024台机器
- 第 53~64 位:共12位,序列号,是自增值,表示同一毫秒内产生的ID,单台机器每毫秒最多可生成4096个订单ID
代码实现:
package com.ac.member.config.mybatis;
import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
@Slf4j
public class SnowFlake {
/** 初始偏移时间戳 */
private static final long OFFSET = 1546300800L;
/** 机器id (0~15 保留 16~31作为备份机器) */
private static final long WORKER_ID;
/** 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32)*/
private static final long WORKER_ID_BITS = 5L;
/** 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = 65536) */
private static final long SEQUENCE_ID_BITS = 16L;
/** 机器id偏移位数 */
private static final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS;
/** 自增序列偏移位数 */
private static final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS;
/** 机器标识最大值 (2^5 / 2 - 1 = 15) */
private static final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1;
/** 备份机器ID开始位置 (2^5 / 2 = 16) */
private static final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1;
/** 自增序列最大值 (2^16 - 1 = 65535) */
private static final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1;
/** 发生时间回拨时容忍的最大回拨时间 (秒) */
private static final long BACK_TIME_MAX = 1000L;
/** 上次生成ID的时间戳 (秒) */
private static long lastTimestamp = 0L;
/** 当前秒内序列 (2^16)*/
private static long sequence = 0L;
/** 备份机器上次生成ID的时间戳 (秒) */
private static long lastTimestampBak = 0L;
/** 备份机器当前秒内序列 (2^16)*/
private static long sequenceBak = 0L;
static {
// 初始化机器ID
long workerId = getWorkId();
if (workerId < 0 || workerId > WORKER_ID_MAX) {
throw new IllegalArgumentException(String.format("cmallshop.workerId范围: 0 ~ %d 目前: %d", WORKER_ID_MAX, workerId));
}
WORKER_ID = workerId;
}
private static Long getWorkId(){
try {
String hostAddress = Inet4Address.getLocalHost().getHostAddress();
int[] ints = StringUtils.toCodePoints(hostAddress);
int sums = 0;
for(int b : ints){
sums += b;
}
return (long)(sums % WORKER_ID_MAX);
} catch (UnknownHostException e) {
// 如果获取失败,则使用随机数备用
return RandomUtils.nextLong(0,WORKER_ID_MAX-1);
}
}
/** 私有构造函数禁止外部访问 */
private SnowFlake() {}
/**
* 获取自增序列
* @return long
*/
public static long nextId() {
return nextId(SystemClock.now() / 1000);
}
/**
* 主机器自增序列
* @param timestamp 当前Unix时间戳
* @return long
*/
private static synchronized long nextId(long timestamp) {
// 时钟回拨检查
if (timestamp < lastTimestamp) {
// 发生时钟回拨
log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp);
return nextIdBackup(timestamp);
}
// 开始下一秒
if (timestamp != lastTimestamp) {
lastTimestamp = timestamp;
sequence = 0L;
}
if (0L == (++sequence & SEQUENCE_MAX)) {
// 秒内序列用尽
// log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp);
sequence--;
return nextIdBackup(timestamp);
}
return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence;
}
/**
* 备份机器自增序列
* @param timestamp timestamp 当前Unix时间戳
* @return long
*/
private static long nextIdBackup(long timestamp) {
if (timestamp < lastTimestampBak) {
if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) {
timestamp = lastTimestampBak;
} else {
throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak));
}
}
if (timestamp != lastTimestampBak) {
lastTimestampBak = timestamp;
sequenceBak = 0L;
}
if (0L == (++sequenceBak & SEQUENCE_MAX)) {
// 秒内序列用尽
// logger.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp);
return nextIdBackup(timestamp + 1);
}
return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak;
}
/**
* 并发数
*/
private static final int THREAD_NUM = 30000;
private static volatile CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);
public static void main(String[] args) {
ConcurrentHashMap<Long,Long> map = new ConcurrentHashMap<>(THREAD_NUM);
List<Long> list = Collections.synchronizedList(new LinkedList<>());
for (int i = 0; i < THREAD_NUM; i++) {
Thread thread = new Thread(() -> {
// 所有的线程在这里等待
try {
countDownLatch.await();
Long id = SnowFlake.nextId();
list.add(id);
map.put(id,1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
// 启动后,倒计时计数器减一,代表有一个线程准备就绪了
countDownLatch.countDown();
}
try{
Thread.sleep(50000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("listSize:"+list.size());
System.out.println("mapSize:"+map.size());
System.out.println(map.size() == THREAD_NUM);
}
}
与mybatis-plus结合
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
return SnowFlake.nextId();
}
}
接入非常简单,不需要搭建服务集群。代码逻辑非常简单,同一毫秒内,订单ID的序列号自增。同步锁只作用于本机,机器之间互不影响,每毫秒可以4百万的订单ID,非常强悍。
生成规则不是固定的,可以根据自身的业务需求调整。如果你不需要那么大的并发量,可以把机器标识位拆出一部分,当作业务标识位,标识是哪个业务线生成的订单ID。
6.1 雪花算法优化
雪花算法严重依赖系统时钟。如果时钟回拨,就会生成重复ID。
比如美团的Leaf(美团自研一种分布式ID生成系统),为了解决时钟回拨,引入了zookeeper,原理也很简单,就是比较当前系统时间跟生成节点的时间。
有的对并发要求更高的系统,比如双十一秒杀,每毫秒4百万并发还不能满足要求,就可以使用雪花算法和号段模式相结合,比如百度的UidGenerator、滴滴的TinyId。想想也是,号段模式的预先生成ID肯定是高性能分布式订单ID的最终解决方案。