详解分布式ID解决方案

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;

        /**
          * 返回结果:
          * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
          * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
          * (workerId << workerIdShift) 表示将工作id左移相应位数
          * | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
          * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
        */
        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是能够保证自增唯一的。

猜你喜欢

转载自blog.csdn.net/yuanshangshenghuo/article/details/107101527