分布式ID生成方案--雪花算法和UUID对比

分布式ID的业务需求
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。
比如美团外卖:由于系统中数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据,如订单、骑手、优惠券也都需要有唯一ID做标识。因此一个能够生成全局唯一ID的系统是非常必要的。

生成ID的硬性要求

  1. 全局唯一
    不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  2. 趋势递增
    在MySQL的InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用BTree的数据结构来存储索引数据。因此在主键的选择上我们应该尽量使用有序的主键保证写入性能。
  3. 单调递增
    保证下一个ID一定大于上一个ID,例如事务版本号,IM增量消息、排序等特殊需求。
  4. 信息安全
    如果ID是连续的,恶意扒取用户工作就非常容易做了,直接按照顺序下载指定的URL即可;如果是订单号就更危险了,竞争对手可以直接知道我们一天的单量。所以在一些应用场景下,需要ID无规则。
  5. 含时间戳
    这样就能够在开发中快速了解分布式ID的生成时间。

ID生成系统的可用性要求

  1. 高可用
    发一个获取分布式ID的请求,服务器就可以保证99.999%的情况下给我创建一个唯一的分布式ID
  2. 低延迟
    发一个获取分布式ID的请求,服务器响应速度要快
  3. 高QPS
    假如并发10万个创建分布式ID请求,服务器要顶得住并能成功创建10万个唯一的分布式ID

一般通用方案
UUID(Universally Unique Dentifer)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的32个字符,示例:550e8400-e29b-41d4-a716-446655440000
UUID性能非常高:本地生成,没有网络消耗,如果只考虑唯一性UUID是ok的。但是入数据库的性能较差。
为什么无序的UUID会导致数据库性能变差呢?

  1. 无序
    无法预测他的生成顺序,不能生成递增有序的数字。首先分布式id 一般都会作为主键, UUID太长,占用存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
  2. UUID往往是使用字符串存储,查询的效率比较低。传输数据量大,且不可读 。
  3. 索引, B+树索引的分裂
    既然分布式id是主键,主键是包含索引的,然后mysql的索引是通过b+树来实现的, 因为UUID数据是无序的,每一次新的UUID数据的插入,为了查询的优化,都会对索引"底层的B+树进行修改,这一点很不好。插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。

数据库自增主键
在分布式里面,数据库的自增ID机制的主要原理是:
基于数据库自增ID和mysql数据库的replace into实现的。这里的replace into 跟insert功能类似,不同点在于replace into首先尝试把数据插入数据列表中,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入,否则直接插入新数据。

数据库自增ID机制为什么不适合作分布式ID?

  1. 系统水平扩展比较困难
    比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器,id号是1,2,3,4,5步长是1, 这个时候需要扩容一台机器,可以这样做,把第二台机器的初始值设置得比第一台超过很多,貌似还好,现在想象一下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。所以系统水平扩展方案复杂难以实现。
  2. 数据库压力很大,每次获取ID都要读一次数据库,非常影响性能,不符合分布式ID里面的延迟低和要高QPS的规则(在高并发下,如果都去数据库里面获取id,那是非常影响性能的)。

基于redis的id生成策略
因为Redis是单线程的天生保证原子性,可以使用原子操作INCR和INCRBY来实现。
注意:在Redis集群情况下,同样和MySQL一样需要设置不同的增长步长,同时key一定要设置有效期,可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis,可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5
各个Redis生成的ID为:
A: 1,6,11,16,21
B: 2,7,12,17,22
C:3.8.13.18.23
D: 4,9,14,19,24
E: 5,10,15,20,25

雪花算法
Twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra(由Facebook开发一套开源分布式NoSQL数据库系统)因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。
Twitter的分布式雪花算法 ,经测试snowlake每秒能够产生26万个自增可排序的ID。

  1. Twitter的SnowFlake生成ID能够按照时间有序生成
  2. SnowFlake算法生成id的结果是一个64bit大小的整数,为一个Long型(转换成字符串后长度最多19)
  3. 分布式系统内不会产生ID碰撞(由datacenter和workerld作区分)并且效率较高。
    数据结构
    分布式ID生成方案--雪花算法和UUID对比_第1张图片
    各段解析:
    1bit-符号位
    因为二进制中最高位是符号位,1表示负数,0表示正数。
    生成的id一般都是用整数,所以最高位固定为0。
    41bit-时间戳
    用来记录时间戳,毫秒级。
    41位可以表示2^(41)-1个数字,
    如果只用来表示正整数(计算机中正数包含0) ,可以表示的数值范围是: 0至2^(41)-1,
    减1是因为可表示的数值范围是从0开始算的,而不是1。也就是说41位可以表示2^(41)-1个毫秒的值,转化成单位年则是(2"41)-1)/(1000 "60 "60 -24 "365) =69年。
    10bit-工作机器id
    用来记录工作机器id.可以部署在2^(10)= 1024个节点,它包括5位datacenterld和5位。
    workerld
    5bit可以表示的最大正整数是245-1=31,即可以用0,1,2,3…31这32个数字,来表示不同的datecenterld或workerld。
    12bit-序列号
    序列号,用来记录同毫秒内产生的不同id。
    12bit可以表示的最大正整数是2^(12)-1=4095,即可以用0、1.2.3. 4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
    SnowFlake可以保证:所有生成的id按时间趋势递增,整个分布式系统内不会产重复id (因为有datacenterld和workerld来做区分)

优缺点
优点:
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。可以根据自身业务特性分配bit位,非常灵活。
缺点:
依赖机器时钟,如果机器时钟回拨,会导致重复ID生成。
可能在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以忽略, 一般分布式ID只要求趋势递增,并不会严格要求递增, 90%的需求都只要求趋势递增)

测试雪花算法
导入依赖

<!--hutool 测试雪花算法-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-captcha</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Slf4j
@Component
public class IdGeneratorSnowflake {
    private long workerId = 0;
    private long datacenterId = 1;
    private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);

    @PostConstruct
    public void init() {
        try {
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
            log.info("当前机器的workerId:{}", workerId);
        } catch (Exception e) {
            log.info("当前机器的workerId获取失败", e);
            workerId = NetUtil.getLocalhostStr().hashCode();
            log.info("当前机器 workId:{}", workerId);
        }

    }

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

    public synchronized long snowflakeId(long workerId, long datacenterId) {
        snowflake = IdUtil.createSnowflake(workerId, datacenterId);
        return snowflake.nextId();
    }

    public static void main(String[] args) {
        // 1303931069132832768
        System.out.println(new IdGeneratorSnowflake().snowflakeId());
    }
}

输出结果
分布式ID生成方案--雪花算法和UUID对比_第2张图片

你可能感兴趣的:(java,java)