【问题标题】:Make ULID lexicographic ordering more sensitive to time使 ULID 字典顺序对时间更敏感
【发布时间】:2020-07-21 13:05:11
【问题描述】:

我在一个项目中使用了this ULID example,我不仅需要 ULID 提供的唯一性,还需要它的字典排序能力。

然而,我发现,无论我如何尝试,我都无法让 在循环中生成的 id 进行排序。

例如

class Test{
    public static void main(String[] args) {
            ArrayList<String> ulids = new ArrayList<>();
    
            for (int i = 0; i < 10; i++) {
               
                ulids.add(ULID.generate());
            }
    
            System.out.println("Original:\n..." + ulids);
    
            Collections.shuffle(ulids);
    
            System.out.println("Shuffled:\n..." + ulids);
    
            ulids.sort(new Comparator<String>() {
                @Override
                public int compare(String o1, String o2) {
                    return o1.compareTo(o2);
                }
            });
    
            System.out.println("Sorted:\n..." + ulids);
    
        }
}

Sample output:
Original:
...[01edrp4ng81d3mvkp8s7z19znm, 01edrp4ng872nwfj6b9fsxjkkd, 01edrp4ng86v07r6c9sh62ghr7, 01edrp4ng8bpfw3m2q8bynd5st, 01edrp4ng896t1qhsngrz3h251, 01edrp4ng8jne084nsw5saesfe, 01edrp4ng8w8qz9qtgy3958r1v, 01edrp4ng8fdn30qnr2ktddyz4, 01edrp4ng8ekj0vt393tw12x8j, 01edrp4ng80wacxxskgej5d8mm]
Shuffled:
...[01edrp4ng896t1qhsngrz3h251, 01edrp4ng8w8qz9qtgy3958r1v, 01edrp4ng86v07r6c9sh62ghr7, 01edrp4ng8bpfw3m2q8bynd5st, 01edrp4ng8fdn30qnr2ktddyz4, 01edrp4ng80wacxxskgej5d8mm, 01edrp4ng872nwfj6b9fsxjkkd, 01edrp4ng81d3mvkp8s7z19znm, 01edrp4ng8jne084nsw5saesfe, 01edrp4ng8ekj0vt393tw12x8j]
Sorted:
...[01edrp4ng80wacxxskgej5d8mm, 01edrp4ng81d3mvkp8s7z19znm, 01edrp4ng86v07r6c9sh62ghr7, 01edrp4ng872nwfj6b9fsxjkkd, 01edrp4ng896t1qhsngrz3h251, 01edrp4ng8bpfw3m2q8bynd5st, 01edrp4ng8ekj0vt393tw12x8j, 01edrp4ng8fdn30qnr2ktddyz4, 01edrp4ng8jne084nsw5saesfe, 01edrp4ng8w8qz9qtgy3958r1v]

我检查了实现并发现由于时间是生成 ULID 的核心因素,而且由于所用时间的敏感性是毫秒,即 (System.currentTimeMillis()) ,我可以通过在我的 id 中引入一些延迟来对它们进行排序生成循环。

我引入了大约 5 毫秒的延迟,并且所有的 id 都排序出来了;例如:

class TestWithMsDelay{

   public static void main(String[] args) {
        ArrayList<String> ulids = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(5L);
                ulids.add(ULID.generate());
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        System.out.println("Original:\n..." + ulids);

        Collections.shuffle(ulids);

        System.out.println("Shuffled:\n..." + ulids);

        ulids.sort(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.compareTo(o2);
            }
        });

        System.out.println("Sorted:\n..." + ulids);

    }


}
Sample output:
Original:
...[2rjdme5a5h2ntcd20xq4z487tx, 2rjdme63a23ddsy0km21n6n34a, 2rjdme6pnrenx79zd3jj18est4, 2rjdme70bv45b648p82dbj584n, 2rjdme7d8gx9v9db66ftsxbmqq, 2rjdme7psqdykt24qfymn2e4ba, 2rjdme80as7t1h1rr00m676718, 2rjdme8rztp50bad6ktkhrfhk8, 2rjdme93ngkxkfmf6aegqxer9e, 2rjdme9ea04x22rpx2f3rp5gez]
Shuffled:
...[2rjdme7psqdykt24qfymn2e4ba, 2rjdme6pnrenx79zd3jj18est4, 2rjdme80as7t1h1rr00m676718, 2rjdme63a23ddsy0km21n6n34a, 2rjdme93ngkxkfmf6aegqxer9e, 2rjdme70bv45b648p82dbj584n, 2rjdme9ea04x22rpx2f3rp5gez, 2rjdme8rztp50bad6ktkhrfhk8, 2rjdme7d8gx9v9db66ftsxbmqq, 2rjdme5a5h2ntcd20xq4z487tx]
Sorted:
...[2rjdme5a5h2ntcd20xq4z487tx, 2rjdme63a23ddsy0km21n6n34a, 2rjdme6pnrenx79zd3jj18est4, 2rjdme70bv45b648p82dbj584n, 2rjdme7d8gx9v9db66ftsxbmqq, 2rjdme7psqdykt24qfymn2e4ba, 2rjdme80as7t1h1rr00m676718, 2rjdme8rztp50bad6ktkhrfhk8, 2rjdme93ngkxkfmf6aegqxer9e, 2rjdme9ea04x22rpx2f3rp5gez]

这对我的工作来说还不够好...我不想等待任何长度的时间来生成 ulids(即使是 10us - 100us),人为延迟的概念非常困扰我,lol .

所以,我修改了 ULID.java 并将时间源从 System.currentTimeMillis() 更改为 System.nanoTime()

令我惊讶的是,我不再需要循环中的任何时间延迟来获得可排序的输出 ULID。

但我觉得某处一定有障碍;因为Java规范警告System.nanoTime()不一定比System.currentTimeMillis()更准确

例如在 System.nanoTime() 的 Javadoc 中,它说:

此方法提供纳秒精度,但不一定提供纳秒分辨率(即值更改的频率) - 不保证分辨率至少与 currentTimeMillis() 一样好.

此外,System.nanoTime() 的 Javadoc 似乎表明它与 Epoch 无关(System.currentTimeMillis() 也是如此)

我相信在 ULID.java 中使用 System.nanoTime() 而不是 System.currentTimeMillis()

问题

  1. 我的恐惧是否合理
  2. 如果 (1.) 为真,我该怎么做才能将 ULID 的时间敏感性提高到 1 毫秒以上而不破坏其优势?

【问题讨论】:

    标签: java uuid uid


    【解决方案1】:

    ULID 有两部分:时间分量和随机分量。

    时间分量是自 1970 年以来的毫秒数。

    随机分量更新分两种情况:

    1. 当毫秒变化时,会产生一个新的随机值;
    2. 当毫秒相同时,随机值加一。

    您在此处显示的实现不执行第二步。

    也许你可以包含一些这样的代码(只是一个例子):

    if (timestamp == previousTimestamp) {
        randomComponent++;
    } else {
        randomComponent = RANDOM.nextLong();
    }
    

    我发现的另一个问题是它使用 Math.random(),而不是 java.security.SecureRandom。为了解决这个问题,这是一个建议:

    import java.security.SecureRandom;
    private static final RANDOM = new SecureRandom();
    

    最后,不建议使用System.nanoTime(),因为它返回自任意时间点以来的纳秒数。这不是从主板中的实时时钟 (RTC) 返回的白天时间。此函数用于测量代码中两点之间的经过时间,可能用于基准测试。示例:

    
    long startNanos = System.nanoTime();
    
    // do some expensive tasks here
    
    long endNanos = System.nanoTime();
    
    long elapsedNanos = endNanos - startNanos;
    
    

    如果您愿意,可以查看库 ulid-creator。也许它可以提供帮助。示例:

    // Generate a ULID as UUID
    UUID ulid = UlidCreator.getUlid();
    
    // Or generate a ULID as String (Crockford's base32)
    String ulid = UlidCreator.getUlidString();
    

    项目页面:https://github.com/f4b6a3/ulid-creator

    编辑

    对不起。我没有回答这些问题。

    我的恐惧合理吗

    是的,你的造物主。

    如果 (1.) 为真,我该怎么做才能将 ULID 的时间敏感性提高到 1 毫秒以上而不破坏其优势?

    您可以增加 ULID 分辨率,但它不符合 ULID Spec(顺便说一下,这不是像 RFC-4122 这样的正式标准)。生成的 UUID 类似于由 Jimmy Wilson 创建的 COMB GUID。两者的主要思想是相同的。

    您可以为时间戳组件保留更多位,但它会花费一些位。例如,如果将时间分量从 48 位增加到 64 位,它将在公元 2262 年左右翻转,但随机分量将从 1208925819614629174706176 (2^80) 减少到 18446744073709551616 (2^64)。如果成本影响 ULID 的优势,则取决于您的项目。

    我刚刚实现了一个纳秒分辨率的 ULID 生成器。巧合的是,我几天前正在研究它。使用System.currentTimeMillis() 方法,它实际上具有毫秒精度。在两个后续调用之间使用方法System.nanoTime()模拟纳秒结果。

    如果您仍打算使用纳秒级 ULID,请随时对其进行测试:

    package your.package.name;
    
    import java.security.SecureRandom;
    import java.time.Instant;
    import java.util.UUID;
    
    /**
     * Utility class that creates a COMB GUID with nanoseconds resolution.
     * 
     * It borrows the main idea from ULID and COMB generators: a concatenation of
     * time and random bytes. It is composed of 64 bits for time and 64 for random
     * bits.
     * 
     * A Nano COMB has two components:
     * 
     * 1. Time camponent (64 bits): nanoseconds since 1970
     * 
     * 2. Random component (64 bits): a value generated by a secure random
     * generator.
     * 
     * Maximum time component year is ~2262 A.D. (2^63/10^9/60/60/24/365.25 + 1970)
     * 
     * @author: Fabio Lima 2020
     */
    public final class NanoCombCreator {
    
        private long prevTime = 0;
        private long prevNano = 0;
    
        private static final long ONE_MILLION_NANOSECONDS = 1_000_000L;
    
        private static final SecureRandom SECURE_RANDOM = new SecureRandom();
    
        /**
         * Returns a time component in nanoseconds.
         * 
         * It uses `System.currentTimeMillis()` to get the system time in milliseconds
         * accuracy. The nanoseconds resolution is simulated by calling
         * `System.nanoTime()` between subsequent calls within the same millisecond.
         * It's not precise, but it provides some monotonicity to the values generates.
         * 
         * @return the current time in nanoseconds
         */
        private synchronized long getTimeComponent() {
    
            final long time = System.currentTimeMillis();
            final long nano = System.nanoTime();
            final long elapsed; // nanoseconds since last call
    
            if (time == prevTime) {
                elapsed = (nano - prevNano);
                if (elapsed > ONE_MILLION_NANOSECONDS) {
                    try {
                        // make the clock to catch up
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        System.err.println("something went wrong...");
                    }
                }
            } else {
                prevTime = time;
                prevNano = nano;
                elapsed = 0;
            }
    
            return (time * ONE_MILLION_NANOSECONDS) + elapsed;
        }
    
        /**
         * Returns the random component using a secure random generator.
         * 
         * @return a random value.
         */
        private synchronized long getRandomComponent() {
            return SECURE_RANDOM.nextLong();
        }
    
        /**
         * Returns a Nano COMB.
         * 
         * A Nano COMB is inspired on ULID and COMB generators.
         * 
         * It is composed of 64 bits for time and 64 for random bits.
         * 
         * @return a UUID
         */
        public synchronized UUID create() {
    
            final long timeBits = getTimeComponent();
            final long randomBits = getRandomComponent();
    
            return new UUID(timeBits, randomBits);
        }
    
        /**
         * Test method that generates many Nano COMBs in a loop.
         * 
         * @param args
         */
        public static void main(String[] args) {
    
            NanoCombCreator creator = new NanoCombCreator();
    
            for (int i = 0; i < 100; i++) {
                // Generate a Nano COMB
                UUID uuid = creator.create();
    
                // Extract the milliseconds and nanoseconds
                long milliseconds = uuid.getMostSignificantBits() / ONE_MILLION_NANOSECONDS;
                long nanoseconds = uuid.getMostSignificantBits() & ONE_MILLION_NANOSECONDS;
    
                // Instantiate an instant using the milliseconds and nanoseconds
                Instant time = Instant.ofEpochMilli(milliseconds).plusNanos(nanoseconds);
    
                // Print the UUID and the time it was generated (UTC)
                System.out.println("UUID: '" + uuid + "', time: " + time);
            }
        }
    }
    
    
    OUTPUT:
    
    UUID: '16240ee8-3865-1503-d1fb-b4e85f991c6b', time: 2020-07-22T11:15:58.537327680Z
    UUID: '16240ee8-3865-f90a-ca19-3ec529750ef7', time: 2020-07-22T11:15:58.537344064Z
    UUID: '16240ee8-3866-dd7c-f32f-7acaebcf7766', time: 2020-07-22T11:15:58.537409664Z
    UUID: '16240ee8-3868-0a99-3ead-b114e1d61520', time: 2020-07-22T11:15:58.537524800Z
    UUID: '16240ee8-3868-efc8-937d-599c72de71a6', time: 2020-07-22T11:15:58.537541248Z
    UUID: '16240ee8-386a-3643-6a5e-e3b5e3b03c71', time: 2020-07-22T11:15:58.537655936Z
    UUID: '16240ee8-386b-132f-7016-057ab30a2920', time: 2020-07-22T11:15:58.537721408Z
    UUID: '16240ee8-386b-f929-d5b0-f70b68aea3d9', time: 2020-07-22T11:15:58.537737280Z
    

    【讨论】:

    • 感谢您的回答!我知道 System.nanoTime 的用途,并且您对 SecureRandom 的建议是正确的。这也是我通常使用的规则。问题中提出的问题是,如果可能,我想提高算法的时间敏感性,以便即使在更短的时间间隔生成 ulid 时也能对输出进行排序。
    • 对不起。我没有回答你的问题。我刚刚更新了我的答案。
    • "我前几天正好在做,用这个方法居然有毫秒精度"哇!你是如何处理我几天前需要的东西的。上帝是奇妙的。世界不同地方的两个人在做类似的事情,然后在 stackoverflow 上见面,男人们称之为chance。我会看看它。谢谢你的一切!
    • 生成器试图是单调的,即一个远离增加其值的函数。如果时间戳比实际时间提前太多,它会强制等待一毫秒,直到实际时钟赶上。理论上,它每毫秒可以生成 1,000,000 个值。但是良好的单元测试是保证它的必要条件。
    • 也许您可以查看 ULID 规范页面:github.com/ulid/spec 那里列出了许多语言的许多实现,包括 Java。我认为最好使用许多人已经使用的一种实现。
    猜你喜欢
    • 1970-01-01
    • 2018-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-20
    • 1970-01-01
    • 2010-12-16
    相关资源
    最近更新 更多