可实现的全局唯一有序ID生成策略

  • 2019 年 10 月 25 日
  • 笔记

在博客园搜素全局唯一有序ID,罗列出来的文章大致讲述了以下几个问题,常见的生成全局唯一id的常见方法 :使用数据库自动增长序列实现 ; 使用UUID实现;  使用redis实现; 使用Twitter的snowflake算法实现;使用数据库+本地缓存实现。作为一个记录性质的博客,简单总结一下。

在实际的生产场景中,经常会出现如下的情况比方说订单号:D channelNo 流水号 样例PSDK1600000001, PSDK1600000002, PSDK1600000003… 这种具有业务意义的全局唯一id且有序自增。先来看一下使用比较多的Twitter的snowflake算法,snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分),并且效率较高。经测试snowflake每秒能够产生26万个ID。一个简单的实现如下:

/**   * Twitter的分布式自增ID雪花算法snowflake   **/  public class SnowFlake {      /**       * 起始的时间戳       */      private final static long START_STMP = 1480166465631L;      /**       * 每一部分占用的位数       */      private final static long SEQUENCE_BIT = 12; //序列号占用的位数      private final static long MACHINE_BIT = 5;   //机器标识占用的位数      private final static long DATACENTER_BIT = 5;//数据中心占用的位数      /**       * 每一部分的最大值       */      private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);      private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);      private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);      /**       * 每一部分向左的位移       */      private final static long MACHINE_LEFT = SEQUENCE_BIT;      private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;      private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;        private long datacenterId;  //数据中心      private long machineId;     //机器标识      private long sequence = 0L; //序列号      private long lastStmp = -1L;//上一次时间戳        public SnowFlake(long datacenterId, long machineId) {          if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {              throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");          }          if (machineId > MAX_MACHINE_NUM || machineId < 0) {              throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");          }          this.datacenterId = datacenterId;          this.machineId = machineId;      }        /**       * 产生下一个ID       *       * @return       */      public synchronized long nextId() {          long currStmp = getNewstmp();          if (currStmp < lastStmp) {              throw new RuntimeException("Clock moved backwards.  Refusing to generate id");          }            if (currStmp == lastStmp) {              //相同毫秒内,序列号自增              sequence = (sequence + 1) & MAX_SEQUENCE;              //同一毫秒的序列数已经达到最大              if (sequence == 0L) {                  currStmp = getNextMill();              }          } else {              //不同毫秒内,序列号置为0              sequence = 0L;          }            lastStmp = currStmp;            return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分                  | datacenterId << DATACENTER_LEFT       //数据中心部分                  | machineId << MACHINE_LEFT             //机器标识部分                  | sequence;                             //序列号部分      }        private long getNextMill() {          long mill = getNewstmp();          while (mill <= lastStmp) {              mill = getNewstmp();          }          return mill;      }        private long getNewstmp() {          return System.currentTimeMillis();      }        public static void main(String[] args) {          SnowFlake snowFlake = new SnowFlake(1, 1);          long start = System.currentTimeMillis();          for (int i = 0; i < 10; i++) {              System.out.println(snowFlake.nextId());          }          System.out.println(System.currentTimeMillis() - start);      }  }

算法中引入了时间因子,所以可以保证生成的id唯一且有序,但是满足不了业务字段+流水号有序自增的要求。如果在此基础上再配合使用数据库本地缓存自然也是可以实现的,不过复杂化了。上述代码执行两次结果如下: 

385063405393940480  385063405393940481  385063405393940482  385063405393940483  385063405393940484  385063405393940485  385063405393940486  385063405393940487  385063405398134784  385063405398134785    385064572152844288  385064572152844289  385064572152844290  385064572152844291  385064572152844292  385064572152844293  385064572152844294  385064572152844295  385064572152844296  385064572152844297  

 

简单的方法就是我们放弃自己造轮子的思想。mongodb中数据的基本单元称为document,在一个特定集合内部需要唯一的标识文档,因此mongdb中存储的文档都由一个‘_id’键,这个键的值可以是任意类型的ObjectId,要求不同的机器都能用全局唯一的同种方法方便的生成它。因此不能使用自增主键,ObjectId 底层也是借鉴了雪花算法,使用12字节的存储空间  |0|1|2|3|4|5|6 |7|8|9|10|11|  |时间戳  |机器ID|PID|计数器|  前四个字节时间戳是从标准纪元开始的时间戳,单位为秒 。时间戳保证秒级唯一,机器ID保证设计时考虑分布式,避免时钟同步,PID保证同一台服务器运行多个mongod实例时的唯一性,最后的计数器保证同一秒内的唯一性。

 


 


 

mongo在spring boot中的引入和配置,此处不再介绍。

创建model类

package com.slowcity.admin.generate.dbmodel;    import java.io.Serializable;    public class BaseSequence implements Serializable{        private static final long serialVersionUID = 475722757687764546L;      private String id;      private String name;      private Long sequence;        public String getId() {          return id;      }      public void setId(String id) {          this.id = id;      }      public String getName() {          return name;      }      public void setName(String name) {          this.name = name;      }      public Long getSequence() {          return sequence;      }      public void setSequence(Long sequence) {          this.sequence = sequence;      }      @Override      public int hashCode() {          final int prime = 31;          int result = 1;          result = prime * result + ((id == null) ? 0 : id.hashCode());          result = prime * result + ((name == null) ? 0 : name.hashCode());          result = prime * result + ((sequence == null) ? 0 : sequence.hashCode());          return result;      }      @Override      public boolean equals(Object obj) {          if (this == obj)              return true;          if (obj == null)              return false;          if (getClass() != obj.getClass())              return false;          BaseSequence other = (BaseSequence) obj;          if (id == null) {              if (other.id != null)                  return false;          } else if (!id.equals(other.id))              return false;          if (name == null) {              if (other.name != null)                  return false;          } else if (!name.equals(other.name))              return false;          if (sequence == null) {              if (other.sequence != null)                  return false;          } else if (!sequence.equals(other.sequence))              return false;          return true;      }      @Override      public String toString() {          return "BaseSequence [id=" + id + ", name=" + name + ", sequence=" + sequence + "]";      }    }

public class DigitalTaskSequence extends BaseSequence{      private static final long serialVersionUID = -7287622688931253780L;  }

import org.springframework.data.annotation.Id;  import org.springframework.data.mongodb.core.mapping.Document;  import org.springframework.stereotype.Component;      @Component  @Document(collection = "dm_id_task")  public class DigitalTaskSequenceMG extends DigitalTaskSequence {      private static final long serialVersionUID = -425011291271386371L;      @Id      @Override      public String getId() {          return super.getId();      }  }

service

import java.util.List;    import com.slowcity.admin.generate.dbmodel.BaseSequence;    public interface SequenceGenericService {      public String generateId(Class<? extends BaseSequence> clazz);      List<BaseSequence> initAllId();  }

import java.util.ArrayList;  import java.util.List;    import org.slf4j.Logger;  import org.slf4j.LoggerFactory;  import org.springframework.stereotype.Service;  import org.springframework.transaction.annotation.Transactional;    import com.slowcity.admin.generate.dbmodel.BaseSequence;  import com.slowcity.admin.generate.dbmodel.DigitalTaskSequenceMG;  import com.slowcity.admin.generate.repository.SequenceGenericRepository;  import com.slowcity.admin.generate.service.SequenceGenericService;      @Service  @Transactional  public class SequenceGenericServiceImpl implements SequenceGenericService {      private static final Logger log = LoggerFactory.getLogger(SequenceGenericServiceImpl.class);      private SequenceGenericRepository sequenceGenericRepository;        public SequenceGenericServiceImpl(SequenceGenericRepository sequenceGenericRepository) {          this.sequenceGenericRepository = sequenceGenericRepository;      }        @Override      public String generateId(Class<? extends BaseSequence> clazz) {          String id = sequenceGenericRepository.generateId(clazz);          log.info("{} generate {}", clazz.getName(), id);          return id;      }        @Override      public List<BaseSequence> initAllId() {            List<BaseSequence> baseSequenceList = new ArrayList<>(),          baseSequenceResultList = new ArrayList<>();          DigitalTaskSequenceMG digitalTaskSequenceMG = new DigitalTaskSequenceMG();          digitalTaskSequenceMG.setName("sequence");          digitalTaskSequenceMG.setSequence(1210000000000000000L); //1210可以代表业务号 000000000000000代表自增流水号
baseSequenceList.add(digitalTaskSequenceMG);
for (BaseSequence baseSequence:baseSequenceList) { BaseSequence resultSequence = sequenceGenericRepository.initAllId(baseSequence); if(resultSequence != null){ baseSequenceResultList.add(resultSequence); } } return baseSequenceResultList; } }

数据实现层

import com.slowcity.admin.generate.dbmodel.BaseSequence;    public interface SequenceGenericRepository {        public String generateId(Class<? extends BaseSequence> clazz);      BaseSequence initAllId(BaseSequence Sequence);    }

import java.util.List;  import java.util.Map;  import java.util.stream.Collectors;    import org.springframework.data.mongodb.core.MongoTemplate;  import org.springframework.data.mongodb.core.query.Criteria;  import org.springframework.data.mongodb.core.query.Query;  import org.springframework.data.mongodb.core.query.Update;  import org.springframework.stereotype.Component;    import com.slowcity.admin.generate.dbmodel.BaseSequence;  import com.slowcity.admin.generate.repository.SequenceGenericRepository;    @Component  public class SequenceMongoGenericRepository implements SequenceGenericRepository {      private Map<Class,Class<? extends BaseSequence>> baseSequenceMap;      private MongoTemplate mongoTemplate;      public SequenceMongoGenericRepository(List<BaseSequence> baseSequences, MongoTemplate mongoTemplate){          baseSequenceMap = baseSequences.stream()              .collect(Collectors.toMap(baseSequence -> baseSequence.getClass().getSuperclass(),                  BaseSequence::getClass));          this.mongoTemplate = mongoTemplate;      }          @Override      public String generateId(Class<? extends BaseSequence> clazz) {          Class<? extends BaseSequence> childClazz = baseSequenceMap.get(clazz);          if(childClazz != null) {              Query query = new Query(Criteria.where("name").is("sequence"));              Update update = new Update().inc("sequence", 1);              Object dbm = mongoTemplate.findAndModify(query, update, childClazz);              if(dbm != null) {                  BaseSequence bs = (BaseSequence)dbm;                  return String.valueOf(bs.getSequence());              }          }          return null;      }        @Override      public BaseSequence initAllId(BaseSequence Sequence) {            Query query = new Query(Criteria.where("name").is("sequence"));          Class clazz = Sequence.getClass();          List<? extends BaseSequence> list = mongoTemplate.find(query,clazz);          if(list.isEmpty()){              mongoTemplate.save(Sequence);              return Sequence;          }              return null;      }  }

controller

import java.util.List;    import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.web.bind.annotation.RequestMapping;  import org.springframework.web.bind.annotation.RequestMethod;  import org.springframework.web.bind.annotation.RestController;    import com.slowcity.admin.generate.dbmodel.BaseSequence;  import com.slowcity.admin.generate.dbmodel.DigitalTaskSequence;  import com.slowcity.admin.generate.service.SequenceGenericService;    /**   * id生成器   * @author moona   *   */  @RestController  @RequestMapping("/generateId/task")  public class TaskGenerateIdController {        @Autowired      private SequenceGenericService sequenceGenericService;      @RequestMapping(value = "/taskId", method = RequestMethod.GET)      public String generateTaskId() {          return sequenceGenericService.generateId(DigitalTaskSequence.class);      }        @RequestMapping(value = "/init", method = RequestMethod.GET)      public  List<BaseSequence> generateTaskIdinit() {          return sequenceGenericService.initAllId();      }    }

 

执行初始化调用方法

 

 对应数据库

 

 

开始测试生成id

第一次调用:

第2次调用

 

 第10次调用

 

 此时再查看数据库,序列已经到1210000000000000011 下次调用直接取值了。真正做到了了分布式满足业务的自增全局唯一索引。mongo底层是原子性的,所以也不会出现并发的问题。如果将id生成策略部署成单台机器服务,则可以满足不同服务不同业务的需求,真正做到可定制可扩展。尽可放心使用。

 

 

【end】