5位ID生成方案

  最近在某微信技术群,有人问到如何生成5位唯一数字+字母字符串的算法,要保证生成的字符串唯一,且字符串内部也要唯一。

  怎么样,这个需求是不是很简单,也有点特殊呢?简单是指需求简单,特殊是指,字符串长度要求是5位,而不是更长。根据群里的讨论,有以下解决方案:

  • UUID生成随机字符串,并控制字符串长度。估计这个群友是指UUID.randomUUID().toString.substring(0,5) 吧,不过这样是没法达到唯一的。也就是说每次生成的字符串可能重复。
  • apache的StringUtils。我在org.apache.commons.lang3.StringUtils这个类里面没有找到相关的函数。
  • 雪花算法。毫无疑问,雪花算法是可以的,不过没法保证是5位。或许这位兄台的需求有点奇葩了,还必须要求5位。
  • 内存表或数据库表。其实我是比较赞同这种方案的,一共就5位字符和数字,排列组合一下,不考虑内部唯一,一共就pow(36,5).=60466176。6千万数据保存到内存中去,用的时候取一条,用完为止。如果考虑内部唯一,那就是36*35*34*33*32=45239040,4千万的表。不过从表里面去,会有并发的问题。

  其实群友给的方案都是不错的,除了那个StringUtils,但我思考这个问题就比较简单、粗暴了。一共5位长度,要求字符串+数字,也就4千万的事儿,4千万很大妈?说实话,真不大。那怎么搞的?计数器就可以了!

  啥 ?计数器,你确定没说错?嗯,没说错。我刚提出这个方案的时候,就有脑残、喷子开始无脑的怼我了,不等我说完就开始怼我了。那计数器真的就不行吗?

  其实我个人比较倾向于上面的第五种方案,也就是把所有可用的字符串算出来,然后再取就行了。那么这样做有一个不好的地方就是,比较占用内存,大概215MB,说实话吧,这215MB也不算大,如果是我直接就用内存表了。不过还能不能优化一下内存空间?答案是能,那就是用计数器。

  其实我们在生成这4千万的字符串的时候,就是用计数器算的啊,就是从0到36^5,把数字转换成36进制,然后判断是否有重复字符串,这个过程就用了计数器。那能不能用计数器动态计算下一个符合条件的字符串呢?当然可以了啊。

object LimitStringIdGenerator{
  private [idgenerator] val MaxStringLength = 5
  private [idgenerator] val MaxNumberOfString = math.pow(36,5).toInt
  private [idgenerator] def to36String(num:Int):String = BigInt(num).toString(36)
  private [idgenerator] def notHasSameChar(charSequence: String):Boolean = charSequence.toCharArray match {
    case Array(a,b,c,d,e) if ! ( a == b || a == c || a == d || a == e || b ==c || b == d || b ==e || c ==d || c ==e || d == e) => true
    case Array(a,b,c,d) if ! ( a == b || a == c || a == d || b ==c || b == d || c ==d) => true
    case Array(a,b,c) if ! ( a == b || a == c || b == c ) => true
    case Array(a,b) if ! ( a == b ) => true
    case _ => false
  }
}
class LimitStringIdGenerator extends IDGenerator {
  import LimitStringIdGenerator._
  private var start = 0
  override def nextId:Int = {
    var nextId = start + 1
    while(!notHasSameChar(to36String(nextId)) && nextId < MaxNumberOfString){
      nextId = nextId + 1
    }

    nextId
  }

  override def setStart(start: Int): Unit = {
    this.start = start
  }
}

  上面是可执行的代码,非常简单,就是用一个nextId做计数器,依次递增,找到下一个符合条件的字符串。其实上面的代码还是可以优化一下的,为啥呢?因为有时候计数器的步长不一定是1,此话怎讲呢?

  我们来看看当计数器的值是01000时,步长是1会造成很大浪费,因为下一个可用的字符串是01234;当记数字的值是11000时,下一个可用的字符串值是12345,如果步长为1也会造成很大的浪费。

  那有人会问了,并发时候怎么办呢?如果停机再启动怎么办呢?嗯,这两个问题问的非常好,不过就有点***难的意味了,为啥呢?因为楼主只是需要一个算法来获取5位唯一字符串而已,而且数据量不大。但我还是很喜欢这两个问题的。

  我们先来看并发的问题。为啥需要并发呢?有时候是效率的考虑,并发计算会稍微快点;还有时候是多线程导致的;当然还有其他原因。

  其实上面的算法效率已经非常高了,为啥?因为就是对变量自增,然后做个判断而已。经测试,把36^5月6千1百万的数据计算一遍,大概28秒,计算下一个可用的字符串,最长耗时大概400毫秒。那多线程的时候怎么办呢?那就用AtomicInteger来替换呗,应该可以保证线程安全。

  那停机再启动怎么办?其实吧,你停机的时候把nextId保存到数据库、缓存或者其他第三方存储,不就好了?啥,你说程序是异常宕机,没有保存?那就根据生成的最后一个字符串反推一下当前计数器的值喽。啥?你生成的字符串没保存?那你用来干啥呢?

  当然还是可以分布式的,就是有多个节点,每个节点都只从某个值开始计算下一个可用字符串,不过也没啥必要,一共4千万字符串,很快就会耗尽了。

  脑残粉和喷子就不要留言了,当然如果你有更好的方案,还是可以发个博客怼一怼的,如果没有那就闭嘴。

  

posted @ 2018-08-30 15:59  gabry.wu  阅读(1238)  评论(0编辑  收藏  举报