likes
comments
collection
share

面向面试编程:分布式ID生成策略(一)

作者站长头像
站长
· 阅读数 42
面试官:说说你了解的分布式ID生成策略。

UUID

UUID (Universally Unique Identifier),通用唯一识别码。UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。

UUID由以下几部分的组合:

  • 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
  • 时钟序列。
  • 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

UUID 是由一组32位数的16进制数字所构成,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。例如:

aefbbd3a-9cc5-4655-8363-a2a43e6e6c80
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

如果需求是只保证唯一性,那么UUID也是可以使用的,但是按照分布式id的要求, UUID其实是不能做成分布式id的,原因如下:

  • 首先分布式id一般都会作为主键,但是安装mysql官方推荐主键要尽量越短越好,UUID每一个都很长,所以不是很推荐
  • 既然分布式id是主键,然后主键是包含索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键生成的b+树进行很大的修改,这一点很不好
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

自增ID

针对表结构的主键,我们常规的操作是在创建表结构的时候给对应的ID设置 auto_increment 也就是勾选自增选项。

但是这种方式我们清楚在单个数据库的场景中我们是可以这样做的,但如果是在分库分表的环境下。直接利用单个数据库的自增肯定会出现问题。因为ID要唯一,但是分表分库后只能保证一个表中的ID的唯一,而不能保证整体的ID唯一。

上面的情况我们可以通过单独创建主键维护表来处理。举个例子来看看:

创建一个表结构

CREATE TABLE `test_order_id`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `title` char(1) NOT NULL,
  PRIMARY KEY (`id`),
	UNIQUE KEY `title` (`title`)
) ENGINE = InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET =utf8;

然后我们通过更新ID操作来获取ID信息

BEGIN;

REPLACE INTO test_order_id (title) values ('p') ;
SELECT LAST_INSERT_ID();

COMMIT;

数据库多主模式

单点数据库方式存在明显的性能问题,可以对数据库进行高可以优化,担心一个主节点挂掉没法使用,可以选择做双主模式集群,也就是两个MySQL实例都能单独生产自增的ID。

查看主键自增的属性。

show variables like '%increment%'

我们可以设置主键自增的步长从2开始。但是这种在并发量比较高的情况下,如何保证扩展性其实会是一个问题。在高并发情况下无能为力。

号段模式

号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:

CREATE TABLE id_generator (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '当前最大id',
  step int(20) NOT NULL COMMENT '号段的布长',
  biz_type    int(20) NOT NULL COMMENT '业务类型',
  version int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`)
)

字段说明:

  • biz_type :代表不同业务类型
  • max_id :当前最大的可用id
  • step :代表号段的长度
  • version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。但同样也会存在一些缺点比如:服务器重启,单点故障会造成ID不连续。

Redis

基于全局唯一ID的特性,我们可以通过Redis的INCR命令来生成全局唯一ID。

同样使用Redis也有对应的缺点:

  • ID 生成的持久化问题,如果Redis宕机了怎么进行恢复
  • 当个节点宕机问题

当然针对故障问题我们可以通过Redis集群来处理,比如我们有三个Redis的Master节点。可以初始化每台Redis的值分别是1,2,3,然后分别把分布式ID的KEY用Hash Tags固定每一个master节点,步长就是master节点的个数。各个Redis生成的ID为:

  • A:1,4,7
  • B:2,5,8
  • C:3,6,9

优点

  • 不依赖于数据库,灵活方便,且性能优于数据库
  • 数字ID有序,对分页处理和排序都很友好
  • 防止了Redis的单机故障

缺点

  • 如果没有Redis数据库,需要安装配置,增加复杂度
  • 集群节点确定是3个后,后面调整不是很友好

Redis分布式ID的简单案例:

/**
 *  Redis 分布式ID生成器
 */
@Component
public class RedisDistributedId {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final long BEGIN_TIMESTAMP = 1659312000l;

    /**
     * 生成分布式ID
     * 符号位    时间戳[31位]  自增序号【32位】
     * @param item
     * @return
     */
    public long nextId(String item){
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        // 格林威治时间差
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        // 我们需要获取的 时间戳 信息
        long timestamp = nowSecond - BEGIN_TIMESTAMP;
        // 2.生成序号 --》 从Redis中获取
        // 当前当前的日期
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 获取对应的自增的序号
        Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date);
        return timestamp << 32 | increment;
    }

}
转载自:https://juejin.cn/post/7160132343523442701
评论
请登录