分布式系统ID的唯一性——雪花算法

1.为什么需要分布式全局唯一ID

2.ID生成规则部分硬性要求

3.ID生成规则的可用性要求

4.现有的ID生成策略

5.雪花算法ID生成策略

1.为什么需要分布式全局唯一ID
在复杂的分布式高并发系统中,往往在一秒之内就会产生海量的数据,而且我们要对这些数据进行唯一性的标识,且还要保证有序性,在我们以往的开发使用中,UUID以及自增ID这种生成策略,可能无法满足一瞬间生成数据的唯一性和有序性,此时一个能够生成全局唯一的ID生成规则是十分重要的

2.ID生成规则部分硬性要求
2.1)全局唯一
不出现重复的ID号,这是对ID最基本的要求。
2.2)趋势递增
在mysql的InnoDB引擎中使用的是聚簇索引,使用BTree结构来保存数据,用递增的ID可以让BTree不会产生巨大的变动来保证写入性能。

当B+Tree插入的主键值为自增的时候:
分布式系统ID的唯一性——雪花算法_第1张图片
当B+Tree插入的主键值为随机值uuid的时候:
分布式系统ID的唯一性——雪花算法_第2张图片

2.1)单调递增
保证下一个ID的值一定要大于上一个ID的值,来符合排序等要求

2.3)信息安全
如果ID是自增ID或者某种规则的连续性ID,恶意的扒取工作就比较容易进行(比如退款接口,随便输入订单的自增ID。或者领取红包的接口,随便输入红包的自增ID,以及观察一天ID的增量来判断系统一天的订单量。)所以在一些应用场景下,需要ID不规则,让这些恶意的扒取工作不好进行。

2.4)含时间戳
这样就能在开发中通过id来了解这条数据的生成时间

3.ID生成规则的可用性要求
3.1)高可用
发送一个获取分布式ID的请求,服务器就要99.99999%的情况下给我们创建一个分布式唯一ID。

3.2)低延时
发送一个获取分布式ID的请求,服务器要响应迅速。

3.3)高qps
假如一秒钟有十万个请求同时发送给服务器,服务器也要同时创建十万个不同的分布式唯一递增有序ID。

4.现有的ID生成策略

4.1)Uuid
优点:
可以保证唯一性

缺点:
4.1.1)无序,不能生成递增的有序数字。
4.1.2)主键过长,不符合主键越短越好的规则,在where id = ''时候会产生比较开销。
4.1.3)索引B+Tree的分裂(上面已经介绍过)。

4.2)自增id
优点:保证单调递增性,有序性。

缺点:
4.2.1)ID很容易被人猜出来,不安全
4.2.2)单机模式无法承载高并发量,无法一秒钟生成几十万个不同的ID,不符合高qps规则。
4.2.3)集群模式,假设第一台数据库id是奇数,第二台id是偶数,这种配置规则非常繁琐,而且如果要扩展到一百多台mysql,不易于扩展。

4.3)基于redis的全局ID策略
优点:因为redis底层天然地保证了原子性,所以可以使用incr来操作,而且单机redis的qps就比较高,可以一定程度保证高qps。

缺点:在集群模式下,redis和mysql一样,都需要设置不同的增长步长,这种配置方式极为繁琐,而且不易于扩展。

5.雪花算法ID生成策略
了解了上面这么多生成策略之后,我们发现上述生成ID规则均不符合生成规则,这个时候,雪花算法出现了!

Twitter的分布式雪花算法snowflake,经测试snowflake每秒能够生成26万个自增可排序的id

5.1)雪花算法的数据结构

我们先来看一下雪花算法的数据结构图:
分布式系统ID的唯一性——雪花算法_第3张图片

雪花算法由一个64bit的long类型构成,它将一个long类型拆分成了四个号段,来分别表示不同的值来保证全局唯一和有序性

5.2)雪花算法的号段解析

1bit 符号位:
不用,二进制中的最高位是符号位,1表示负数,0表示正数
生成的id一般都是正数,所以最高符号位为0.

41bit 时间戳位:
用来记录时间,毫秒数。
41位可以用来表示2^(41)-1个数字,如果只用来表示正数,那么这个值的范围就是0~2^(41)-1,减1是因为数字是从0开始算的,而不是1.

2^(41)-1转化成单位年则是(2^(41)-1)/(1000606024365) = 69年

10bit 工作进程位:
可以用来表示工作机器id,可以部署在2^(10)=1024个节点,也可以用5位workId(工作机器ID)和5位datacenterId(工作线程Id),也可以多用几位来表示工作机器ID,位数不固定。

12bit序列号位:
12位,可表示的最大正整数是 2^12 -1 = 4095,来表示同一机器一时间戳(毫秒)内产生的4095个ID序号。

5.3)雪花算法的优缺点

优点:
毫秒数在高位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性高,符合高qps,高可用。
根据自身业务分配bit位,非常灵活

缺点:
依赖机器时钟如果机器时钟重置,会导致重复ID生成。
在单机上递增,但是由于是分布式环境,每台机器上的时钟不同步,有时候会出现不递增的情况。(不过大致是递增的,而且完全能保证趋势递增。)

5.4)雪花算法的代码实现

我们可以使用糊涂工具生成:


    cn.hutool
    hutool-captcha
    ${hutool.version}

ID 生成器:

package com.example.demo.meeting.util;

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.net.Inet4Address;
import java.net.UnknownHostException;

/**
 * @author sulingfeng
 * @title: IdGenerator
 */
@Component
public class IdGenerator {

    public static Snowflake snowflake = IdUtil.createSnowflake(getWorkId(), getDataCenterId());
    /**
     * workId使用IP生成
     * @return workId
     */
    private static Long getWorkId() {
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for (int b : ints) {
                sums = sums + b;
            }
            //我们可以根据需要自行控制长短
            return (long) (sums % 32);
        }
        catch (UnknownHostException e) {
            // 失败就随机
            return RandomUtils.nextLong(0, 31);
        }
    }


    /**
     * dataCenterId使用hostName生成
     * @return dataCenterId
     */
    private static Long getDataCenterId() {
        try {
            String hostName = SystemUtils.getHostName();
            int[] ints = StringUtils.toCodePoints(hostName);
            int sums = 0;
            for (int i: ints) {
                sums = sums + i;
            }
            return (long) (sums % 32);
        }
        catch (Exception e) {
            // 失败就随机
            return RandomUtils.nextLong(0, 31);
        }
    }

    public synchronized long snowflakeId() {
        return snowflake.nextId();
    }

}

你可能感兴趣的:(雪花算法)