每日生成9位不重复的唯一标识符

3

有一个令人困惑的需求。

基本上,我需要按照以下标准创建唯一的ID:

  • 9位数字,每天唯一(意味着如果该数字在第二天再次出现,则可以接受)
  • 实时生成;仅使用Java(意味着不从数据库生成序列号——实际上根本不访问数据库)
  • 生成该编号以填充请求ID,并且每天将生成约1,000,000个ID。
  • 不能使用UUID或UID(超过9位数字)

这是我的考虑:

  • 使用序列号听起来不错,但如果JVM重新启动,则可能会重新生成请求ID。
  • 使用时间HHmmssSSS(小时分钟秒毫秒)有两个问题:

a.系统小时可能会被服务器管理员更改。
b.如果在同一毫秒内发出2个请求,则可能会出现问题。

有什么想法吗?


基于墙上时间仍然似乎是最好的选择。即使管理员调整时间,这也是逐渐发生的,不会干扰您的功能。 - Marko Topolnik
1
为什么只有9位数字?会有多少台服务器处理请求? - Ray Toal
@RayToal忘了提到。负载均衡上有2个服务器(!!!)我不太清楚为什么会有9位数字。 - Rudy
4个回答

5

不生成数据库序列号

我讨厌这种愚蠢的要求。我建议您使用嵌入式数据库,如H2HSQLDB,并通过序列生成标识符。

编辑:让我详细解释一下为什么我提出这个“欺骗”:我理解“无数据库”要求是指不能安装任何数据库软件来处理此要求或者现有的数据库模式不能更改。使用嵌入式数据库与将新的jar文件添加到项目中相同。为什么不这样做?当关系型数据库已经为您解决了这个问题时,为什么要自己实现呢?


必须在 JVM 关闭/启动时存活,显然。 - T.J. Crowder
嵌入式数据库不一定要在内存中,它们也可以保存在持久性存储中。 - mavroprovato
好的。他说他不想这样做。(要求相当武断。)你如何解决多服务器ID冲突? - T.J. Crowder
1
抱歉,我重新读了两遍问题,但我没有看到他说过不应该使用持久存储。我也不知道这个多服务器的要求是什么。我认为只有一个服务器会为多个客户端生成ID。 - mavroprovato
1
这个问题非常模糊,应该被关闭,因为它似乎已经被放弃了(没有对有用的后续问题的回答),但他说“不从数据库生成序列号”,这对我来说基本上意味着“不从数据库生成序列号”。 - T.J. Crowder
不要使用H2或HSQLDB,因为它必须通过客户安全审查。但是我同意这是某种愚蠢的要求。 - Rudy

3
九位数可以处理100万个ID,剩下的三位数字用于服务器ID,每个服务器都有一个三位数的ID。在每个服务器内部分配唯一的ID值而不必担心它们之间的重叠。我们可以使用一个在内存中不断增加的值,但为了在JVM重新启动时保留该值,我们需要将最近分配的值写入磁盘中(任何你想存储的地方)- 本地磁盘、内存缓存等等。
为确保不会在每个请求上发生文件/其他I/O的开销,我们以块形式分配ID,将块的终点回显到存储器中。
具体步骤如下:
1. 给每个服务器分配一个ID。 2. 在服务器上有一个存储器,用于存储当天分配的最后一个ID值(例如,一个文件)。 3. 使用块(10个或100个ID等)方式进行ID分配。 4. 分配一个块: - 读取文件并回写一个增加了块大小的数字。 5. 使用块中的ID。 6. ID格式为:例如,服务器#12分配的第28个ID将是12000000027。 7. 当天更改(例如,午夜)时,丢弃当前块,并为新的一天分配一个新的块。
伪代码如下:
class IDAllocator {
    Storage storage;
    int     nextId;
    int     idCount;
    int     blockSize;
    long    lastIdTime;

    /**
     * Creates an IDAllocator with the given server ID, backing storage,
     * and block size.
     *
     * @param   serverId        the ID of the server (e.g., 12)
     * @param   s               the backing storage to use
     * @param   size            the block size to use
     * @throws  SomeException   if something goes wrong
     */
    IDAllocator(int serverId, Storage s, int size)
    throws SomeException {

        // Remember our info
        this.serverId = serverId * 1000000; // Add a million to make life easy
        this.storage = s;
        this.nextId = 0;
        this.idCount = 0;
        this.blockSize = bs;
        this.lastIdTime = this.getDayMilliseconds();

        // Get the first block. If you like and depending on
        // what container this code is running in, you could
        // spin this out to a separate thread.
        this.getBlock();
    }

    public synchronized int getNextId()
    throws SomeException {
        int id;

        // If we're out of IDs, or if the day has changed, get a new block
        if (idCount == 0 || this.lastIdTime < this.getDayMilliseconds()) {
            this.getBlock();
        }

        // Alloc from the block    
        id = this.nextId;
        --this.idCount;
        ++this.nextId;

        // If you wanted (and depending on what container this
        // code is running in), you could proactively retrieve
        // the next block here if you were getting low...

        // Return the ID
        return id + this.serverId;
    }

    protected long getDayMilliseconds() {
        return System.currentTimeMillis() % 86400000;
    }

    protected void getBlock()
    throws SomeException {
        int id;

        synchronized (this) {
            synchronized (this.storage.syncRoot()) {
                id = this.storage.readIntFromStorage();
                this.storage.writeIntToStroage(id + blocksize);
            }

            this.nextId = id;
            this.idCount = blocksize;
        }
    }
}

...但需要注意的是,这只是伪代码,你可能需要加入一些主动性的内容,以便在需要ID时不会因为I/O等待而阻塞。

上述内容假设你已经有了某种全局应用程序单例,并且IDAllocator实例只是该单例中的数据成员。如果没有,你可以很容易地将其改为单例模式,通过给它经典的getInstance方法,并从环境中读取配置而不是将其作为构造函数的参数来实现。


2
我是否有什么误解?每天1,000,000个ID只需要6位数字(假设000000是有效的ID)。因此,一些数字可用于在服务器之间“分区”ID空间。 - Michael Burr
@MichaelBurr:在 Stack Overflow 上做任何事情之前,我真的需要先喝上第一杯咖啡。谢谢! - T.J. Crowder
这个类不应该是单例模式吗?这样可以避免两个客户端创建该类的实例并生成相同的ID。或者你可以在IDAllocator.class上同步getNextId方法。 - mavroprovato
@mavroprovato:嘿,这是伪代码。 :-) 但我编写它的时候是以为你会在启动时创建类的实例,而不是每个客户端都创建。将其设为单例肯定是一个选择,但前提是如果OP已经有另一个应用级别的对象(就像许多人一样),该对象是这些东西的容器。如果它是单例,当然我们会希望它从某个地方读取其配置,而不是将其作为参数接收。 - T.J. Crowder

1

对于服务器1,从1数到999,999,999怎么样? 对于服务器2,从-999,999,999数到-1怎么样?

我猜由于负载均衡,平衡比例应该是50:50。所以每个服务器都有相同的ID范围。此外,您可以将最后生成的ID存储在文件系统中。由于性能问题,只需存储每1000个值(或10000个值,这并不重要)。重新启动应用程序后,读取最后生成的值并加上1000。我想那应该可以工作。


这不应该是个问题。只需将数字范围从1到999.999.999分为两组即可。第一组:1-499.999.999,第二组:500.000.000-999.999.999。只需格式化数字,例如1就是... - zip

0

时间戳部分+随机数,只要他不使用时间戳作为随机种子就可以了。当然无法在数学上证明它不会冲突,但如果要求在多个线程中生成这些ID,并且这些线程彼此不知道,则无法保证ID不会冲突。一个解决方案是运行一个专门用于生成ID的服务,所有服务器的所有线程都会请求该ID,该服务可以是数据库或比完整数据库更简单的东西。一旦解决了这个问题,确保ID唯一就变得微不足道了。 - Lassi Kinnunen
@npinti:在做任何关于SO的事情之前,我真的需要先喝上第一杯咖啡。对此很抱歉。 - T.J. Crowder
@T.J.Crowder:没关系。我以为我错过了一些显而易见的东西。 - npinti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接