面试题-分布式


1. Zookeeper如何实现分布式锁?

2. 说说你如何用redis实现分布式锁?

3. 集群高并发情况下如何保证生成分布式全局ID的唯一性?

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

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

美团点评的金融, 支付, 餐饮, 酒店.

猫眼电影等产品的系统中数据日渐增长, 对数据分库分表后需要有一个唯一ID来标识一条数据或消息.

此时能够生成全局唯一ID的实现非常必要.

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

  • 全局唯一性: 不能出现重复的ID号;

  • 趋势递增: 在Mysql的InnoDB引擎中使用的是聚集索引, 由于多数RDBMS使用BTree的数据结构来存储索引数据, 在主键的选择上面我们应该尽量使用有序的主键保证写入性能.

  • 单调递增: 保证下一个ID一定大于上一个ID, 例如事务版本号等.

  • 信息安全: 如果ID是连续的, 恶意用户的爬取工作就非常容易了, 直接按照顺序下载指定URL即可; 如果是订单号就更危险了, 竞争对手可以直接知道我们后台真实的业务量. 所以, 在一些场景下的ID需要是不规则的.

  • 含时间戳: 含有时间信息,方便排查.

3.3 ID号生成系统的可用性要求

  • 高可用: 发一个获取分布式ID的请求, 服务器就要保证99.999%的情况下给我们创建一个唯一的分布式ID.
  • 低延迟: 发一个获取分布式ID的请求, 服务要快.
  • 高QPS: 满足高并发场景的分布式ID生成.

3.4 一般通用方案

3.4.1 UUID

UUID(Universally Unique Identifier)的标准型式包含32个十六进制数字, 以连字号分为5段, 性能非常高, 本地生成, 没有网络消耗.

例如: 52095f3f-6002-40dd-ba35-28998514f582

@Test
public void IdTest() {
    UUID uuid = UUID.randomUUID();
    System.out.println(uuid);
}

但是问题来了, 无序的UUID会导致入库的性能变差. 为什么呢?

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

    首先分布式ID一般都会作为主键, 但MySql官方推荐主键尽量越短越好, UUD每一个都很长, 所以不推荐.

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

    比如做DB主键的场景下, UUID就非常不适用, Mysql官方有明确的建议, 主键尽量越短越好, 36个字符长度的UUID不符合要求.

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

    既然分布式ID是主键, 主键是包含索引的, Mysql的索引是通过B+树来实现的, 每一次新的UUID数据的插入, 为了查询效率的优化, 都会对索引底层的B+树进行修改, 因为UUID是无序的, 所以每一次UUID数据的插入都会对主键底层的B+树进行很大的修改, 插入完全无序, 不但会导致一些中间节点产生分裂, 也会创造出很多不饱和的节点, 大大降低了数据库插入的性能.

3.4.2 数据库自增主键(小公司够用,大厂不行)

  • 单机

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

    replace into含义: 插入一条记录, 如果表中唯一索引的值遇到冲突, 则替换老(旧)数据.

    relace into和insert类似, 不同点在于, replace into首先尝试插入数据, 如果发现表中已经有了此行数据(根据主键或唯一索引判断)则先删除, 再插入; 否则直接插入新数据.

    create table t_test(
        id bigint(20) unsigned not null auto_increment primary key,
       stub char(1) not null default '',
       unique key uk_stub(stub)
    );
    
    select * from t_test;
    replace into t_test(stub) values('b');
    select last_insert_id();
    
  • 集群分布式

    数据库自增ID机制不适合用作分布式ID.

    • 系统水平扩展比较困难, 比如定义好了步长和机器台数还好说, 如果要添加机器怎么办? 假设现在只有一台机器生成ID号是1,2,3,4,5(步长是1), 此时需要扩容一台机器, 可以把第二台机器的初始值设置的比第一台机器超过很多, 貌似可以接收. 如果扩容100台机器呢? 水平扩展复杂度很高, 难以实现.

    • 如果在高并发下, 都去数据库获取ID, 数据库压力很大, 每次获取ID都要读写一次数据库, 非常影响性能, 不符合分布式ID里面的低延迟和高QPS要求.

3.4.3 基于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

Redis方案可以落地实现, 但是为了生成一个全局ID要搞多台Redis集群建设, 浪费资源. 且如果遇到某台机器宕机, 会存在ID不连续的问题.

3.5 终极方案(雪花算法)

Twitter发布的分布式自增ID算法 snowflake.

3.5.1 概述

最初Twitter把存储系统从Mysql迁移到Cassandra(Facebook开发的一套开源分布式NoSql数据库系统), 因为Cassandra没有顺序ID生成机制, 所以自行开发了一套全局唯一ID生成算法.

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

  • Twitter的snowflake生成ID能够按照时间有序生成.
  • snowflake算法生成ID的结果是一个64bit大小的整数, 为一个Long型, 转换成字符串后长度最多19.
  • 由datacenterId和workerId作区分,分布式系统内不会产生ID碰撞, 并且效率高.

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

  • 在分布式的环境下必须全局且唯一.
  • 一般都需要单调递增, 因为一般唯一ID都会存到数据库, 而InnoDB的特性就是讲内容存储在主键索引树上的叶子节点, 而且是从左往右递增的, 所以考虑到数据库性能, 一般生成的ID也最好是单调递增的. 为了防止ID冲突可以使用36位的UUID, 但是UUID有一些缺点, 首先它相对比较长, 另外UUID一般是无序的.
  • 分布式唯一ID还需要是无规则的, 防止竞争对手攻击.

3.5.2 结构

雪花算法的核心组成:

请添加图片描述

在Java中64bit的整数是long类型,所以在snowflake算法生成的ID就是long类型来存储的.

号段解析:

  • 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个)/(365*24*60*60*1000ms)=69年
  • 10bit位, 工作机器ID, 用来记录工作机器ID.
    • 可以并部署在2^{10}=1024个节点, 0,1,2,3,….1023共1024个数字, 包括5位datacenterId和5位workId.
    • 5位(bit)可以表示的最大正整数是2^{5}-1=31, 即可以用0,1,2,3,….31共32个数字, 来表示不同的datacenterId和workId.
  • 12bit位, 序列号, 用来记录同毫秒内产生的不同ID.
    • 12位(bit)可以表示的最大正整数是2^{12}-1=4095, 即可以用0,1,2,3,….,4095共4096个数字, 来表示同一机器同一时间戳(毫秒)内产生的4095个ID序号.

SnowFlake可以保证:

所有生成的ID按时间趋势递增.

整个分布式系统内不会产生重复ID, 因为有datacenterId和workId来做区分.

3.5.2 落地实现

hutool工具包已经提供雪花算法ID生成的工具类.

<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.13</version>
</dependency>
package com.atguigu.springcloud.snowflake;

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;

/**
 * 类描述:雪花算法ID生成器
 */
@Component
@Slf4j
public class IdGeneratorSnowflake {
    /**
     * 0-31
     */
    private long workId = 0;
    /**
     * 0-31
     */
    private long datacenterId = 1;
    private Snowflake snowflake = IdUtil.createSnowflake(workId, datacenterId);

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

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

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

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

3.5.3 优缺点

优点:

毫秒数在高位, 自增序列在低位, 整个ID都是趋势递增的.

不依赖数据库等第三方系统, 以服务的方式部署, 稳定性更高, 生成ID的性能也非常高.

可以根据自身业务特性分配bit位, 非常灵活.

缺点:

依赖机器时钟, 如果机器时钟回拨, 会导致重复ID生成.

在单机上是递增的, 但是由于涉及到分布式环境, 每台机器上的时钟不可能完全同步, 有时候会出现不是全局递增的情况, 此缺点可以认为无所谓, 一般分布式ID只要求趋势递增, 并不会严格要求递增, 90%的需求都只要求趋势递增.

3.5.4 补充

针对雪花算法的时钟回拨问题, 业界也有解决方案.

  • 百度开源的分布式唯一ID生成器UidGenerator
  • 美团点评分布式ID生成系统-Leaf

4. 在你的项目中, 哪些数据是数据库和redis缓存双写的? 如何保证数据的双写一致性?


文章作者: 王子
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王子 !
评论
  目录