1.为什么使用分布式ID
项目初期,数据量与访问量还没有那么大的时候,我们可能使用的单库中单表存着,但是随着业务的快速增长,数据的体量与访问量激增,在单机数据库中我们就可能对大表进行分表,比如说我的订单表按照月分表 ;我们单机数据库扛不住访问量的激增的时候,这时候就要进行分库处理,比如说将订单表进行分片操作,每台数据库上保存着订单的某一个部分,之前单库的时候使用主键id可能是自增的,在分表或者是分表片之后我们就不能使用数据自增的id了,因为我们在分表或者是分片之后,会存在多个订单表,他们的表结构是一样的,如果使用主键自增id,会出现不同分表中的id是一样的,这样我们在按照id做操作的时候就没有办法确定哪个是你需要的那条数据。
2.UUID生成分布式ID
使用UUID 生成分布式ID的方式很简单,java 自带了生成UUID的工具,我们可以直接使用生成。
public static void main(String[] args) {
String uuid = UUID.randomUUID().toString().replace("-","");
System.out.println(uuid);
}
结果:0ef9fb09094e404db05a673ccdfff125
这样就能生成一个uuid了,我们在插入订单数据的时候,每次生成一次就可以了。我们可以看到这个uuid没有规律可循,数据库使用这种主键,效率会降低,要是这种id作为订单号的话肯定不合适的,订单号往往多层含义生成的,而且是纯数字,看起来会舒服点。
3.数据库主键
使用数据库主键的方式其实也挺简单,就是在插入数据之前,需要去一个专门的主键数据库中获取一下主键就可以了。
很简单,首先我们要先有个专门生成主键的数据库,这个库中比如说有一张生成订单id的表order_id,他有两列一列是id,这个id是自增长的,另一列随便搞上列就行,如下图:
id | xxx |
---|---|
1 | 数据 |
然后我们在往业务库插入数据的时候,先去这个order_id 插入一条数据,然后使用数据库 LAST_INSERT_ID()这个函数,获取到最后一次插入的id,我们就可以拿着这个id塞到业务插入那条sql里面了。
4.雪花算法
雪花算法是Twitter推出的一个用于生成分布式ID的算法,雪花算法是一个算法,我们可以基于这个算法生成一个分布式id出来。
像百度的uidgenerator 是基于雪花算法的,然后美团的leaf是基于雪花算法与数据库的方式封装而成的
public class IdWorker{
//下面两个每个5位,加起来就是10位的工作机器id
private long workerId; //工作id
private long datacenterId; //数据id
//12位的序列号
private long sequence;
public IdWorker(long workerId, long datacenterId, long sequence){
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
//初始时间戳
private long twepoch = 1288834974657L;
//长度为5位
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
//最大值
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
//序列号id长度
private long sequenceBits = 12L;
//序列号最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//工作id需要左移的位数,12位
private long workerIdShift = sequenceBits;
//数据id需要左移位数 12+5=17位
private long datacenterIdShift = sequenceBits + workerIdBits;
//时间戳需要左移位数 12+5+5=22位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//上次时间戳,初始值为负数
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId(){
return datacenterId;
}
public long getTimestamp(){
return System.currentTimeMillis();
}
//下一个ID生成算法
public synchronized long nextId() {
long timestamp = timeGen();
//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//获取当前时间戳如果等于上次时间戳
//说明:还处在同一毫秒内,则在序列号加1;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) { // 0 - 4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
//获取时间戳,并与上次时间戳比较
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取系统时间戳
private long timeGen(){
return System.currentTimeMillis();
}
public static void main(String[] args) {
IdWorker worker = new IdWorker(21,10,0);
System.out.println(worker.nextId());
}
}
5.Redis的incr生成
我们可以使用redis incr命令来获取这个分布式id,redis中的incr命令生成的id是自增的,而且能保证唯一
我这里是使用了Redis集群模拟的(这里redis 集群不会使用的可以参考下这个链接:我是个链接),我们可以看下test代码:
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisIncrCommandTest {
@Autowired
private JedisCluster jedisCluster;
private static Executor pool = Executors.newFixedThreadPool(10);
@Test
public void testRedisincr() throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(10);
for (int i=0;i<10;++i){
pool.execute(new Runnable() {
@Override
public void run() {
try {
int await = barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+jedisCluster.incr("gid_key"));
}
});
}
Thread.sleep(10000);
}
}
这里使用了10个线程的线程池模拟并发情况,然后使用CyclicBarrier 栅栏,等着这10个线程都准备好了,才放开,能够更接近并发场景。使用incr(key) 的时候,如果这个key没有他会先给你创建一个0。我们来看下结果:
可以看出我们incr是能够保证自增唯一的。