百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

就这?分布式 ID 发号器实战(分布式id生成方案)

cac55 2024-10-20 04:22 13 浏览 0 评论


分布式 ID 需要满足的条件:

  • 全局唯一:这是最基本的要求,必须保证 ID 是全局唯一的。
  • 高性能:低延时,不能因为一个小小的 ID 生成,影响整个业务响应速度。
  • 高可用:无限接近于100%的可用性。
  • 好接入:遵循拿来主义原则,在系统设计和实现上要尽可能简单。
  • 趋势递增:这个要看具体业务场景,最好要趋势递增,一般不严格要求。

让我来先捋一捋常见的分布式 ID 的解决方案有哪些?

1、数据库自增 ID

这是最常见的方式,利用数据库的 auto_increment 自增 ID,当我们需要一个ID的时候,向表中插入一条记录返回主键 ID。简单,代码也方便,但是数据库本身就存在瓶颈,DB 单点无法扛住高并发场景。

针对数据库单点性能问题,可以做高可用优化,设计成主从模式集群,而且要多主,设置起始数和增长步长。

-- MySQL_1 配置:
set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长
-- 自增ID分别为:1、3、5、7、9 ......

-- MySQL_2 配置:
set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长
-- 自增ID分别为:2、4、6、8、10 ....

但是随着业务不断增长,当性能再次达到瓶颈的时候,想要再扩容就太麻烦了,新增实例可能还要停机操作,不利于后续扩容。

2、UUID

UUID 是 Universally Unique Identifier 的缩写,它是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符,UUID 是16字节128位长的数字,通常以36字节的字符串表示,比如:4D2803E0-8F29-17G3-9B1C-250FE82C4309。

生成ID性能非常好,基本不会有性能问题,代码也简单但是长度过长,不可读,也无法保证趋势递增。

3、雪花算法

雪花算法(Snowflake)是 twitter 公司内部分布式项目采用的 ID 生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。

组成结构:正数位(占1 bit)+ 时间戳(占41 bit)+ 机器 ID(占10 bit)+ 自增值(占12 bit),总共64 bit 组成的一个 long 类型。

  • 第一个 bit 位(1 bit):Java 中 long 的最高位是符号位代表正负,正数是0,负数是1,一般生成 ID 都为正数,所以默认为0
  • 时间戳部分(41 bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
  • 工作机器id(10bit):也被叫做 workId,这个可以灵活配置,机房或者机器号组合都可以,通常被分为 机器 ID(占5 bit)+ 数据中心(占5 bit)
  • 序列号部分(12bit):自增值支持同一毫秒内同一个节点可以生成4096个 ID

雪花算法不依赖于数据库,灵活方便,且性能优于数据库,ID 按照时间在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。

雪花算法好像挺不错的样子,靓仔决定采用这个方案试下。

于是一套操作猛如虎,写个 demo 给领导看下。

只能继续思考方案了

4、百度(Uid-Generator)

uid-generator 是基于 Snowflake 算法实现的,与原始的 snowflake 算法不同在于,它支持自定义时间戳、工作机器 ID 和 序列号 等各部分的位数,而且 uid-generator 中采用用户自定义 workId 的生成策略,在应用启动时由数据库分配。

具体不多介绍了,官方地址:https://github.com/baidu/uid-generator

也就是说它依赖于数据库,并且由于是基于 Snowflake 算法,所以也不可读。

5、美团(Leaf)

美团的 Leaf 非常全面,即支持号段模式,也支持 snowflake 模式。

也不多介绍了,官方地址:https://github.com/Meituan-Dianping/Leaf

号段模式是基于数据库的,而 snowflake 模式是依赖于 Zookeeper 的

6、滴滴(TinyID)

TinyID 是基于数据库号段算法实现,还提供了 http 和 sdk 两种方式接入。

文档很全,官方地址:https://github.com/didi/tinyid

7、Redis 模式

其原理就是利用 redis 的 incr 命令实现 ID 的原子性自增,众所周知,redis 的性能是非常好的,而且本身就是单线程的,没有线程安全问题。但是使用 redis 做分布式 id 解决方案,需要考虑持久化问题,不然重启 redis 过后可能会导致 id 重复的问题,建议采用 RDB + AOF 的持久化方式。

分析到这里,我觉得 Redis 的方式非常适用于目前的场景,公司系统原本就用到了 redis,而且也正是采用的 RDB + AOF 的持久化方式,这就非常好接入了,只需少量编码就能实现一个发号器功能。

话不多说,直接开始干吧。

本案例基于 Spring Boot 2.5.3 版本

首先在 pom 中引入 redis 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce客户端连接需要这个依赖 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

application.yml 中配置 redis 连接

spring:
  redis:
    port: 6379
    host: 127.0.0.1
    timeout: 5000
    lettuce:
      pool:
        # 连接池大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池中的大空闲连接
        max-idle: 8
        # 连接池中的小空闲连接
        min-idle: 0
        # 连接池大阻塞等待时间(使用负值表示没有限制)
        max-wait: 1000
        # 关闭超时时间
        shutdown-timeout: 100

将 RedisTemplate 注入 Spring 容器中

@Configuration
public class RedisConfig{

    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerializer来序列化/反序列化redis的value值
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        // value
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 使用StringRedisSerializer来序列化/反序列化redis的key值
        RedisSerializer<?> redisSerializer = new StringRedisSerializer();
        // key
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

使用 redis 依赖中的 RedisAtomicLong 类来实现 redis 自增序列,从类名就可以看出它是原子性的。

看一下 RedisAtomicLong 的部分源码

// RedisAtomicLong 的部分源码
public class RedisAtomicLong extends Number implements Serializable, BoundKeyOperations<String> {
    private static final long serialVersionUID = 1L;
    //redis 中的 key,用 volatile 修饰,获得原子性
    private volatile String key;
    //当前的 key-value 对象,根据传入的 key 获取 value 值
    private ValueOperations<String, Long> operations;
    //传入当前 redisTemplate 对象,为 RedisTemplate 对象的顶级接口
    private RedisOperations<String, Long> generalOps;

    public RedisAtomicLong(String redisCounter, RedisConnectionFactory factory) {
        this(redisCounter, (RedisConnectionFactory)factory, (Long)null);
    }
    private RedisAtomicLong(String redisCounter, RedisConnectionFactory factory, Long initialValue) {
        Assert.hasText(redisCounter, "a valid counter name is required");
        Assert.notNull(factory, "a valid factory is required");
        //初始化一个 RedisTemplate 对象
        RedisTemplate<String, Long> redisTemplate = new RedisTemplate();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericToStringSerializer(Long.class));
        redisTemplate.setExposeConnection(true);
        //设置当前的 redis 连接工厂
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.afterPropertiesSet();
        //设置传入的 key
        this.key = redisCounter;
        //设置当前的 redisTemplate
        this.generalOps = redisTemplate;
        //获取当前的 key-value 集合
        this.operations = this.generalOps.opsForValue();
        //设置默认值,如果传入为 null,则 key 获取 operations 中的 value,如果 value 为空,设置默认值为0
        if (initialValue == null) {
            if (this.operations.get(redisCounter) == null) {
                this.set(0L);
            }
        //不为空则设置为传入的值
        } else {
            this.set(initialValue);
        }
    }
    //将传入 key 的 value + 1并返回
    public long incrementAndGet() {
        return this.operations.increment(this.key, 1L);
    }
} 

看完源码,我们继续自己的编码

使用 RedisAtomicLong 封装一个基础的 redis 自增序列工具类

// 只封装了部分方法,还可以扩展
@Service
public class RedisService {
    @Autowired
    RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取链接工厂
     */
    public RedisConnectionFactory getConnectionFactory() {
        return redisTemplate.getConnectionFactory();
    }

    /**
     * 自增数
     * @param key
     * @return
     */
    public long increment(String key) {
        RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
        return redisAtomicLong.incrementAndGet();
    }

    /**
     * 自增数(带过期时间)
     * @param key
     * @param time
     * @param timeUnit
     * @return
     */
    public long increment(String key, long time, TimeUnit timeUnit) {
        RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
        redisAtomicLong.expire(time, timeUnit);
        return redisAtomicLong.incrementAndGet();
    }

    /**
     * 自增数(带过期时间)
     * @param key
     * @param expireAt
     * @return
     */
    public long increment(String key, Instant expireAt) {
        RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
        redisAtomicLong.expireAt(expireAt);
        return redisAtomicLong.incrementAndGet();
    }

    /**
     * 自增数(带过期时间和步长)
     * @param key
     * @param increment
     * @param time
     * @param timeUnit
     * @return
     */
    public long increment(String key, int increment, long time, TimeUnit timeUnit) {
        RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, getConnectionFactory());
        redisAtomicLong.expire(time, timeUnit);
        return redisAtomicLong.incrementAndGet();
    }
}

根据业务需求编写发号器方法

@Service
public class IdGeneratorService {
    @Autowired
    RedisService redisService;

    /**
     * 生成id(每日重置自增序列)
     * 格式:日期 + 6位自增数
     * 如:20210804000001
     * @param key
     * @param length
     * @return
     */
    public String generateId(String key, Integer length) {
        long num = redisService.increment(key, getEndTime());
        String id = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + String.format("%0" + length + "d", num);
        return id;
    }

    /**
     * 获取当天的结束时间
     */
    public Instant getEndTime() {
        LocalDateTime endTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
        return endTime.toInstant(ZoneOffset.ofHours(8));
    }
}

由于业务需求,需要每天都重置自增序列,所以这里以每天结束时间为过期时间,这样第二天又会从1开始。

测试一下

@SpringBootTest
class IdGeneratorServiceTest {
    @Test
    void generateIdTest() {
        String code = idGeneratorService.generateId("orderId", 6);
        System.out.println(code);
    }
}
// 输出:20210804000001

6位自增序列每天可以生成将近100w个编码,对于大多数公司,已经足够了。

经过本地环境测试,开启10个线程,1秒内每个线程10000个请求,没有丝毫压力。

如果觉得有些场景下连续的编号会泄漏公司的数据,比如订单量,那么可以设置随机增长步长,这样就看不出具体订单量了。但是会影响生成的编码数量,可以根据实际情况调整自增序列的位数。

总结

没有最好的,只有最合适的。在实际工作中往往都是这样,需要根据实际业务需求来选择最合适的方案。

END

相关推荐

正点原子开拓者FPGA开发板资料连载第四十章 SD卡图片显示实验

1)实验平台:正点原子开拓者FPGA开发板2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子3)全套实验源码+手册+视频下载地址:http://www.openedv.c...

东芝存储改名为铠侠了,铠侠microSD卡128GB全网首测

作为一个数码爱好者,平时总爱把玩各种科技数码产品,最近又迷上了口袋云台相机,大疆OsmoPocket、飞宇口袋相机、SnoppaVmate口袋相机什么的,不过这类产品由于设计的机身体积很小(毕竟为...

SD存储卡卡面上奇奇怪怪的图标,你知道几个?

现在对高像素照片、连拍、4K甚至8K的需求越来越多,对存储卡的传输速度、容量等,要求也越来越多了。但是,看到SD存储卡卡面上奇奇怪怪的图标,让人非常迷惑。这篇文章让你简单认识这些图标和奇奇怪怪的数字。...

拍摄4K视频上选!铠侠 EXCERIA PLUS microSD卡

大家好,我是波导终结者。今天跟大家分享的是铠侠的EXCERIAPLUS极至光速microSDXCUHS-1存储卡,名字有点长,但是不用担心,我会帮大家梳理好存储卡的选购建议。有不少刚入门的朋友...

高速稳定,一卡多用:铠侠极至光速microSD存储卡评测

Hello,大家好,我是小胖子。半个月前收到了KIOXIA铠侠寄来的一张256GB的TF卡,用了大半个月,让我们看看这款产品表现如何吧。其实很多人并不太了解铠侠,问我铠侠是什么品牌,好不好。其实,东芝...

读速205MB/s、V30规格,雷克沙SILVER系列存储卡再添新成员

IT之家6月19日消息,雷克沙今日推出3款SILVER系列SD/microSD存储卡新品,支持4K60fps录像。据介绍,该系列存储卡均符合V30标准,其中micr...

相机、无人机拍视频,选择SD存储卡有什么需要知道的?

本文章不涉及产品推荐导购行为,致力于给到小白带来基础知识。相机一般使用SD卡,无人机一般使用microSD卡(也叫TF卡),使用的标准和图标标识是一样的。相机、无人机拍视频,选择SD存储卡有什么需要知...

PNY推出适用Switch 2的microSD Express卡,读取速度高达890MB/s

任天堂Switch2开始预订,其比前代产品变得更加昂贵,各种配件的价格都高于预期,这也包括转向microSDExpress存储。此时,PNY推出了新款microSDExpress闪存卡。新款mi...

SD卡迎来25周年:全球售出120亿张,容量翻50万倍

IT之家5月21日消息,科技媒体betanews今天(5月21日)发布博文,报道称SD卡迎来了25周年的生日。自2000年首款SD存储卡问世以来,已走过25个年头...

微单相机买一款什么样的SD卡才够用?写入速度更为关键

最近,评价君朋友发现自己的卡拍摄视频时候总断流,于是感觉写入速度应该是不够的,打算换卡,评价君正好跟他说道说道。目前的SD存储卡,很多只标注读取速度,比如95MB/s,80MB/s等等,而没有写写入速...

金士顿Canvas Go!Plus 系列存储卡评测

前言2020年,金士顿推出了CanvasGo!Plus系列存储卡,凭借其优秀的读写速度和稳定性获得了广大用户的认可。时隔5年,金士顿推出了其全新升级产品:SDG4/SDCG4,可选容量覆盖64GB...

TF卡速度等级|MK米客方德(tf卡速度等级图)

TF卡(TransFlash卡,又称MicroSD卡)是一种常见的便携式存储媒体,广泛用于智能手机、相机、平板电脑等设备中。TF卡的性能通常由速度等级来衡量,这些等级反映了TF卡的数据传输速度。拓优星...

关于SD卡,看这张表就够了(sd卡的作用)

这里是溢图科技(原“相机笔记”)。这两天有不少存储产品促销,随之而来的就是关于SD卡的一些提问。文章以前已经写过很多了,这里主要给大家看一张表格:上面就是SD卡协会官方制作的“族谱”,明确给出了不同版...

轻量化储存的首选——凯侠极致光速256G microSD存储卡实测

对于摄影师而言,我们经常会接触到相关存储设备,像照片拍摄中给相机安装的SD卡,视频录制中外录高规格画面的SSD等,都属于专业的存储介质,被应用于商业拍摄、电影级别拍摄之中。而针对生活中我们日常用于拍摄...

首发1569元,读取速度可达250MB/s,闪迪推出最新2TB至尊超极速存储卡

近日,闪迪(SanDisk)正式发布了其最新的2TB至尊超极速microSDXCUHS-I存储卡。据悉,这款存储卡的读取速度可达250MB/s,写入速度则达到150MB/s。这意味着用户在处理高分辨...

取消回复欢迎 发表评论: