暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

面试官:集群高并发情况下如何实现分布式唯一全局id生成?

CodeJie 2021-09-06
805

脱离业务谈技术,都是在耍流氓。

问题:

为什么需要分布式全局唯一ID以及分布式ID的业务需求?

在复杂的分布式系统中,往往需要对大量的数据和消息进行唯一标识。

如在美团点评的金融、支付、餐饮、酒店;
猫眼电影等产品的系统中数据的日益增长,对数据分库分表后需要一个唯一ID来标识一条数据或消息;

特别一点的如订单、骑手、优惠劵也需要一个唯一的ID做标识。
此时一个能够生成全局唯一ID的系统是非常重要的

ID生成规则部分硬性要求:

  1. 全局唯一:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。

  2. 趋势递增:在MySQLInnDB引擎中使用的是聚集索引,由于多数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

UUID是什么?

UUID的标准形式包含32个16进制的数字,一连字号分五段,形式为8-4-4-4-12的36个字符。如:
842d4f28-8b37-48da-8151-259bcf3e23f0

System.out.println(UUID.randomUUID().toString());

好处性能非常高,本地生成,没有网络消耗

但是UUID生成的标识入数据库的性能差。因为生成的36个字符是无序的

为什么无序的UUID会导致入库性能变差呢?

  • 无序——无法预测它的生成顺序,不能生成递增有序的数字

  • 主键——ID作为主键时在特定的环境会存在一些问题

  • 索引——B+树索引的分裂

数据库自增主键

数据库自增主键分为单机和集群分布式
单机

在分布式里面,数据库的自增ID机制主要原理是:数据库自增IDmysql数据库的replace into实现的。

这里的replace into 跟 insert 功能类似
不同点在于:replace into首先尝试插入数据列表中,如果发现表中已经有此行数据,则先删除,再插入,否则直接插入新数据。
replace into :插入一条数据,如果表中唯一索引的值遇到冲突,则替换老数据

CREATE TABLE t_test{
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
   stub CHAR(1) NOT NULL DEFAULT '',
UNIQUE KEY stub (stub)
}

SELECT * FROM t_test;

REPLACE INTO t_test (stub) VALUES('b');
SELECT LAST_INSERT_ID();


集群分布式
那数据库自增ID机制适合作为分布式ID吗? 答案是不太适合

1、系统水平扩展比较困难,比如定义好了步长和机器台数,如果要添加机器该怎么做?假设现在只有一台机器发号是1,2,3,4,5(步长是1),这个时候需要在扩容机器一台,可以这样做:把第二台机器的初始值设置的比第一台超过很多,貌似还好。现在想象一下如果我们找上100台机器,这个时候要扩容活该怎么做呢?简直是噩梦,所以系统水平扩展方案复杂难以实现。

2、数据库压力太大,每次获取ID都得读写一次数据库,非常影响性能,不符合分布式ID里面的延迟低和要高QPS的规则(在高并发下,如果都去获取数据库里面获取id,是非常影响性能的)

基于Redis生成全局id策略

因为Redis是单线的天生保证原子性,可以使用原子操作,INCRINCRBY来实现

集群分布式
可以使用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

配置麻烦,维护啰嗦,若其中有一个Redis出故障了,就会出现序号不连续了。

Twitter的分布式自增ID算法snowflake

概述

Twitter的分布式雪花算法SnowFlake。经测试snowflake每秒能产生26万个自增可排序的ID

  1. twitterSnowFlake生成的ID能够按照时间有序生成

  2. SnowFlake算法生成id的结果是一个64bit大小的整数,为一个Long型

  3. 分布式系统内不会产品ID碰撞,并且效率较高

分布式系统中,有一些需要使用全局唯一ID的场景,生成ID基本要求

  1. 在分布式的环境下必须全局且唯一

  2. 一般都需要单调递增,以为一般唯一ID都会存到数据库,而Innodb的特性就是将内容存储在主键索引树上的叶子节点,而且是从左往右递增的,所以考虑到数据库性能,一般生成的id也最好是单调递增,为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,UUID较长,也是无序的。

  3. 可能还会需要无规则,因为如果使用唯一ID作为订单号这种,为了不让别人知道一天的订单量是多少,就需要这个规则。

结构

SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:

号段解析:
1bit:

  • 不用。因为二进制中最高位是符号位,1表示负数,0表示正数

  • 生成的id一般都是整数,所以最高位固定为0

41bit-时间戳,用来记录时间戳,毫秒级:

  • 41位可以表示2^(41)-1个数字。

  • 如果只用来表示正整数,可以表示的数值是:0至2^(41)-1,减一是因为可表示的数值范围是从0开始算的,而不是1。

  • 也就是说41位可以表示2^ (41)-1个毫秒的值,转化成单位年则是2^(41)-1/(1000* 60* 60* 24* 365)=69年

10bit-工作机器id,用来记录工作机器id。

  • 可以部署在2^(10)=1024个节点,包括datacenterIdworkerId

  • 5位(bit)可以表示的最大正整数是2^5-1=31,即可以用0、1、2、3、…、31这32个数字来表示不同的datacenterIdworkerId

12bit-序列号。用来记录毫秒内产生的不同id。

  • 12位(bit)来表示最大的正整数是2^12-1=4095,即可以用0、1、2、3、…、4094这4095个数字,来表示同一机器同一时间戳(毫秒)内产生的4095个ID序号。

即SnowFalke可以保证所有生成的id按时间趋势递增,整个分布式系统内不会产生重复id。

源码

package com.example.demo.snowfalke;

public class SnowflakeIdWorker {

   // ==============================Fields===========================================
   /** 开始时间截 (201-01-01) */
   private final long twepoch = 1514736000000L;

   /** 机器id所占的位数 */
   private final long workerIdBits = 5L;

   /** 数据标识id所占的位数 */
   private final long datacenterIdBits = 5L;

   /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
   private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

   /** 支持的最大数据标识id,结果是31 */
   private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

   /** 序列在id中占的位数 */
   private final long sequenceBits = 12L;

   /** 机器ID向左移12位 */
   private final long workerIdShift = sequenceBits;

   /** 数据标识id向左移17位(12+5) */
   private final long datacenterIdShift = sequenceBits + workerIdBits;

   /** 时间截向左移22位(5+5+12) */
   private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

   /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
   private final long sequenceMask = -1L ^ (-1L << sequenceBits);

   /** 工作机器ID(0~31) */
   private long workerId;

   /** 数据中心ID(0~31) */
   private long datacenterId;

   /** 毫秒内序列(0~4095) */
   private long sequence = 0L;

   /** 上次生成ID的时间截 */
   private long lastTimestamp = -1L;

   //==============================Constructors=====================================
   /**
    * 构造函数
    * @param workerId 工作ID (0~31)
    * @param datacenterId 数据中心ID (0~31)
    */

   public SnowflakeIdWorker(long workerId, long datacenterId) {
       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));
       }
       this.workerId = workerId;
       this.datacenterId = datacenterId;
   }

   // ==============================Methods==========================================
   /**
    * 获得下一个ID (该方法是线程安全的)
    * @return SnowflakeId
    */

   public synchronized long nextId() {
       long timestamp = timeGen();

       //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
       if (timestamp < lastTimestamp) {
           throw new RuntimeException(
                   String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
       }

       //如果是同一时间生成的,则进行毫秒内序列
       if (lastTimestamp == timestamp) {
           sequence = (sequence + 1) & sequenceMask;
           //毫秒内序列溢出
           if (sequence == 0) {
               //阻塞到下一个毫秒,获得新的时间戳
               timestamp = tilNextMillis(lastTimestamp);
           }
       }
       //时间戳改变,毫秒内序列重置
       else {
           sequence = 0L;
       }

       //上次生成ID的时间截
       lastTimestamp = timestamp;

       //移位并通过或运算拼到一起组成64位的ID
       return ((timestamp - twepoch) << timestampLeftShift) //
               | (datacenterId << datacenterIdShift) //
               | (workerId << workerIdShift) //
               | sequence;
   }

   /**
    * 阻塞到下一个毫秒,直到获得新的时间戳
    * @param lastTimestamp 上次生成ID的时间截
    * @return 当前时间戳
    */

   protected long tilNextMillis(long lastTimestamp) {
       long timestamp = timeGen();
       while (timestamp <= lastTimestamp) {
           timestamp = timeGen();
       }
       return timestamp;
   }

   /**
    * 返回以毫秒为单位的当前时间
    * @return 当前时间(毫秒)
    */

   protected long timeGen() {
       return System.currentTimeMillis();
   }

   //==============================Test=============================================
   /** 测试 */
   public static void main(String[] args) {
       SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
       for (int i = 0; i < 10; i++) {
           long id = idWorker.nextId();
           System.out.println(id+"\t"+String.valueOf(id).length());
           System.out.println("------------------------");
       }
   }
}


工程落地经验

糊涂工具包
github地址:https://github.com/looly/hutool/

springboot整合雪花算法

1、maven中导入依赖

		<dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>cn.hutool</groupId>
           <artifactId>hutool-captcha</artifactId>
           <version>5.0.1</version>
       </dependency>

2、Controller层

package com.example.demo.controller;

import com.example.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
   @Autowired
   private OrderService orderService;

   @RequestMapping(value = "/snowflake")
   public String index(){
       return orderService.getIDBySnowFlake();
   }
}


util工具类

package com.example.demo.util;

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;

@Component
@Slf4j
public class IdGeneratorSnowflake {

   private long workerId = 0;
   private long datacenterId = 1;
   private Snowflake snowFlake = IdUtil.createSnowflake(workerId,datacenterId);

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

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

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

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


service层

package com.example.demo.service;

import com.example.demo.util.IdGeneratorSnowflake;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
public class OrderService {

   @Autowired
   private IdGeneratorSnowflake idGenerator;

   public String getIDBySnowFlake() {
       ExecutorService threadpool = Executors.newFixedThreadPool(5);

       for (int i = 1; i <= 20 ; i++) {
           threadpool.submit(() -> {
               System.out.println(idGenerator.snowflakeId());
           });
       }
       threadpool.shutdown();
       return "hello SnowFlake";
   }
}


那么运行结果我也截个图展示一下:

在浏览器输入会返回自己输入的字符串

同时后台也会打印出我们5个多线程打印的20个id标识

雪花算法的优缺点:

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

补充:若想解决时钟回拨问题可参考下面2个

  • 百度开源的分布式唯一ID生成器UidGenerator

  • Leaf----美团点评分布式ID生成系统


终于写完了,如果对你们有帮助的话,希望老铁们来个三连击。


戳“阅读原文”我们一起进步



文章转载自CodeJie,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论