`
lujintao
  • 浏览: 3745 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

高并发环境下的高性能ID生成器-无锁化设计

阅读更多

   当业务数据量很大而且增长快,在设计数据库时,我们往往不会采用数据库自增ID,这样不便于未来的数据库扩容。我们往往会自己想办法生成ID,业界用得较多的是SnowFlake算法,但该算法有一个缺点,因为ID获取方法上同步锁的存在,在高并发下会存在性能瓶颈。以下是SnowFlake算法获取ID的部分代码:
    /**
     * 获得下一个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;
    }

 

以下是我实现的算法的代码:

package com.tuling.util;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 默认的ID生成器,生成的ID格式为:0 + 当前2020年的年数(8位) + 自增序号(40) + 启动序号(7)
  + 服务器标识(8)
 * 启动序号是为了解决服务器重启后ID生成重复的问题,每次重启,序号都会自增
 * @author lujintao
 * @date 2020-04-30
 */
public class DefaultIdGenerator implements IdGenerator<Long>{

   private static final Logger LOGGER = LoggerFactory.getLogger(DefaultIdGenerator.class);

   private static final ScheduledExecutorService  executorService = new 
ScheduledThreadPoolExecutor(1);

   private static final int DAYS_OF_YEAR = 365;

   private static final StartIndex START_INDEX = new StartIndex(0);

   private static final int BASE_YEAR = 2020;
   private AtomicLong counter = new AtomicLong();

   //年份(以2020年为起点,距离2020年的年份)占用的位数
   private int yearLength;
   //记录当前年份与基准年份(2020)年相隔的年数
   private int yearDistance;
   //表示要与年份进行与计算的另一数
   private long yearAnother;
   // 年份需要向左移动的位数
   private int yearMove;
   //启动序号占用的位数
   private int startIndexLength;
   //表示要与启动序号进行与计算的另一数
   private long startIndexAnother;
   // 启动序号需要向左移动的位数
   private int startIndexMove;
   //服务器ID
   private long serverId;
   //服务器标识占用的位数
   private int serverIdLength;
   //计数器序列号占用的标识
   private int sequenceLength;
   //表示要与序列号进行与计算的另一数
   private long sequenceAnother;
   // 序列号需要向左移动的位数
   private int sequenceMove;

   static {
      //初始化并更新启动序号,在原有基础上加1,以此保证重启后生成的ID不会和之前的重复
      START_INDEX.initAndUpdate();
   }

   /**
    *
    * @param yearLength 年份占用的位数
    * @param startIndexLength 启动序号占用的位数
    * @param serverIdLength  服务器标识占用的位数
    * @param serverId  服务器标识
    */
   public DefaultIdGenerator(int yearLength, int startIndexLength, int serverIdLength
long serverId){
      this.yearLength = yearLength;
      this.yearDistance = getYearDistance();
      this.startIndexLength = startIndexLength;
      this.serverIdLength = serverIdLength;
      this.sequenceLength = 63 - yearLength - startIndexLength - serverIdLength;
      this.yearAnother = getAnotherForAnd(yearLength);
      this.startIndexAnother = getAnotherForAnd(startIndexLength);
      this.sequenceAnother = getAnotherForAnd(sequenceLength);
      this.yearMove = 63 - yearLength;
      this.sequenceMove = startIndexLength + serverIdLength;
      this.startIndexMove = serverIdLength;
      this.serverId = serverId;
     //每到新的一年开始时,就重新计算yeatDistan
      executorService.scheduleAtFixedRate(() -> {
         this.yearDistance = getYearDistance();
      },DAYS_OF_YEAR - Calendar.getInstance().get(Calendar.DAY_OF_YEAR),DAYS_OF_YEAR,
      TimeUnit.DAYS);
   }



   /**
    * 获得一个指定长度的ID
    * 自增序号的获取采用了从counter.getAndIncrement()的值中截取特定长度位的方式获取,是鉴于
    * 以下理由:在一个整数不断自增的过程中,其尾部特定长度的数字是跟着周期性变化的,例如我们
    * 观察一个整数从1000增加到3000,会发现当数字从1000增加到2000时,其尾部的数字从000变到999
    * ,从2000增加到3000时,尾部数字也从000变到999对于long型整数来说,当整数自增时,其尾部
    * 特定长度的二进制位的变化也符合这个规律。无论是普通情况下的自增,还是Long.MAX_VALUE + 1
    * ,或者 -1 + 1。只要我们保证序列号能表达的最大的数一定比业务系统一年需要的ID数多,那生成
    * 的ID就不可能出现重复    
    * @return  Id
    */
    public Long getId() {
      long result = 0L;
      result = (((long)yearDistance & yearAnother) << yearMove) |
            ((counter.getAndIncrement() & sequenceAnother) << sequenceMove) |
            ((START_INDEX.getIndex() & startIndexAnother) << startIndexMove) |
            serverId;
      return result;
   }


   private int getYearDistance(){
      return Calendar.getInstance().get(Calendar.YEAR) - BASE_YEAR;
   }

   /**
    * 当一个整数需要截取低位length个位时,为了完成这一操作,需要与之进行与操作的另一个数
   * 譬如为了截取a = 01010000 10000000 00010010 00110000 11000000 11000000 
   * 11000000 01010001的最后7位,可以这么做,让它与b = 00000000 00000000 0000
   * 0000 00000000 00000000 00000000 00000000 01111111进行与计算即可即 a & b
   * @param length 
   * @return  
   */  
   private long getAnotherForAnd(int length){
      return ((long)-1) >>> (64 - length);
   }
 }
 该生成器在生成ID时,无需加锁,也不存在并发问题,经我本人实测,在我的电脑(windows7系统,
 AMD 双核3.0GHZ)上在单线程下生成100万个ID,
 SnowFlake算法最短耗时1674ms,而此算法最短耗时44ms。

 项目源码地址: https://github.com/lujintao2000/Id_Generator
 

 

0
1
分享到:
评论

相关推荐

    通用、灵活、高性能的分布式 ID 生成器

    分布式ID生成器是现代互联网系统中的重要组成部分,它在大数据量和高并发的场景下扮演着关键角色。本文将深入探讨“通用、灵活、高性能”的分布式ID生成器的设计原理、实现方式以及它在服务器应用和分布式服务/框架...

    百度开源的分布式 ID 生成器,太强大了!(csdn)————程序.pdf

    总结,百度开源的UidGenerator是一款高效、稳定的分布式ID生成器,通过解决时钟回调问题和采用无锁的RingBuffer设计,保证了ID的唯一性和高性能。将其整合到SpringBoot和MyBatis项目中,可以方便地为系统提供全局...

    开源项目-rs-xid.zip

    在实际应用中,rs-xid可能被用作数据库主键生成器、消息队列的消息ID生成器,或者其他任何需要全局唯一标识的场景。通过其提供的API,开发者可以方便地在Java、Python、Go等语言中集成和使用rs-xid。 在`xid-master...

    idgen:ID生成器

    2. **性能优化**:高性能的 ID 生成器通常需要处理大量的并发请求。因此,算法的设计必须高效,避免锁竞争,可能使用无锁数据结构或原子操作来实现。 3. **可扩展性**:随着系统的扩展,ID 生成器应该能够容易地...

    uidGenerator.rar

    分布式ID生成器是现代互联网系统中不可或缺的一部分,尤其是在大数据量、高并发的场景下,确保全局唯一ID的生成显得尤为重要。百度开源的`uidGenerator`就是这样一个工具,它旨在为服务提供高效、稳定的分布式ID生成...

    CosId-main.zip

    CosId,作为一个通用、灵活且高性能的分布式ID生成器,为解决这一问题提供了强大的解决方案。 首先,我们来深入了解CosId的核心特性。CosId的主要目标是提供全局唯一且具有时间序列性的ID,这对于数据的存储和查询...

    网络游戏-网络ID的自动指配.zip

    这种方法可能会导致一定的重复率,特别是在高并发环境下。 3. **哈希算法**:使用哈希函数将用户名或其他唯一信息转换为固定长度的ID,以确保唯一性。这种方法可以避免ID冲突,但可能需要额外的冲突解决机制。 4. ...

    Java开发面试必备知识技能总结视频合集

    - **ID生成方案**:介绍在分布式环境中保证ID全局唯一性的几种常见方案,如UUID、Snowflake算法等。 - **具体实现**:结合分库分表的场景,探讨如何在实际项目中实现全局唯一ID的生成与分配。 #### 6. RPC底层通讯...

    PublicIdService:用于创建供公众使用的唯一ID的服务

    综上所述,`PublicIdService`是一个关键的基础设施组件,它使用Java语言,结合了多种设计原则和技术手段,确保了在高并发、高可用环境下生成的公众唯一ID的质量和效率。理解并掌握这些知识点对于开发和维护这样的...

    java程序猿 问题总结.txt

    Redis具有高性能的特性,能够快速存取数据,非常适合用来存储Session数据。例如,在Spring框架中可以通过配置将Session存储到Redis中,从而实现Session的共享和持久化。 #### 七、MyBatis与对象映射 **MyBatis对象...

    java乐观锁

    例如,在分布式系统中,可以利用分布式锁的乐观锁实现,如Redis的`watch`命令,或者通过分布式ID生成器(如Snowflake)来实现版本号的唯一性和全局性。 总之,Java乐观锁是并发控制的一种高效策略,它通过减少锁的...

    java程序员面试大纲错过了金三银四你还要错过2018吗.docx

    7. **Netty的高性能表现**:采用异步非阻塞I/O模型,高效的数据缓冲和处理机制,以及灵活的编解码器支持等,使其成为高性能网络应用程序的理想选择。 #### 六、分布式系统 1. **Dubbo的基本原理**:Dubbo是一个高...

    logPotato

    10. **性能优化**:Kotlin的编译器能够生成高效的字节码,配合精心设计的数据结构和算法,logPotato能够在不影响性能的情况下提供强大的日志服务。 综上所述,logPotato作为一个Kotlin实现的日志处理框架,结合了...

    1剑盛二面准备试题.txt1剑盛二面准备试题.txt

    70. **SpringMVC与Struts的区别**:SpringMVC是一个更轻量级的Web框架,与Spring框架的整合度更高,提供更好的性能。Struts是一个基于MVC模式的重量级框架。 71. **避免SQL注入的方法**:使用预处理语句...

Global site tag (gtag.js) - Google Analytics