基本概念对比
UUID (通用唯一识别码)
UUID(通用唯一识别码)的标准形式包含32个十六进制数字,以连字号分为五段。标准的UUID版本有几种,其中最常见的是UUIDv4,它是随机生成的。UUIDv4的重复概率理论上为2^122分之一,即大约5.3e-36,这个概率极低,可以认为几乎不会重复。
// UUID 示例 public static void main(String[] args) throws Exception { UUID uuid = UUID.randomUUID(); String uuidString = uuid.toString(); System.out.println(uuidString); }
返回结果如图
MD5 哈希值
MD5是一种哈希函数,输出128位的哈希值,通常用32个十六进制字符表示。MD5哈希值的重复概率取决于输入空间和哈希函数碰撞的可能性。如果输入的数据集很大,那么根据生日悖论,碰撞的概率会随着输入数量的增加而增加。MD5哈希值的理论碰撞概率是2^64分之一,但是由于MD5存在已知的碰撞漏洞,实际碰撞概率比理论值高。
这里我们使用 DigestUtils.md5Hex(input) 来生成 MD5 字符串,导入 pom.xml 文件
<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.6</version> </dependency>
示例方法
public static void main(String[] args) throws Exception { // MD5 示例 String input = "hello world"; String md5 = DigestUtils.md5Hex(input); System.out.println(md5); }
返回结果如图
理论重复概率分析
UUID v4
UUID 版本 4 (随机 UUID),位数:128位 (16字节),可能取值:2^128 ≈ 3.4 × 10^38 ,下面我们来计算一下重复概率
// 生日悖论公式计算 UUID 碰撞概率 public class UUIDCollisionProbability { /** * 计算在 n 次生成中出现至少一次碰撞的概率 */ public static double calculateCollisionProbability(long n) { double H = Math.pow(2, 128); // UUID 空间大小 return 1 - Math.exp(-Math.pow(n, 2) / (2 * H)); } public static void main(String[] args) { // 不同生成数量下的碰撞概率 long[] attempts = { 1000000L, // 100万 1000000000L, // 10亿 1_000000000000L // 1万亿 }; for (long n : attempts) { double prob = calculateCollisionProbability(n); System.out.printf("生成 %,d 个 UUID,碰撞概率: %.15e%n", n, prob); } } }
执行后得出的结果
MD5
MD5 位数:128位 (与 UUID 相同),可能取值:2^128 ≈ 3.4 × 10^38 ,生日攻击:由于哈希碰撞的特性,实际碰撞概率高于理论值,在
public class MD5CollisionProbability { public static void main(String[] args) throws Exception { // 不同生成数量下的碰撞概率 long[] attempts = { 1000000L, // 100万 1000000000L, // 10亿 1000000000000L // 1万亿 }; for (long n : attempts) { double prob = calculateMD5Collision(n); System.out.printf("生成 %,d 个 UUID,碰撞概率: %.15e%n", n, prob); } for (long n : attempts) { double prob = calculatePracticalMD5Collision(n); System.out.printf("生成 %,d 个 UUID,碰撞概率: %.15e%n", n, prob); } } /** * MD5 生日攻击碰撞概率 */ public static double calculateMD5Collision(long n) { double H = Math.pow(2, 128); return 1 - Math.exp(-Math.pow(n, 2) / (2 * H)); } /** * 实际 MD5 碰撞概率 (考虑已知的密码学弱点) */ public static double calculatePracticalMD5Collision(long n) { // 由于 MD5 的密码学弱点,实际碰撞概率比理论值高 double effectiveBits = 120; // 考虑安全削弱 double effectiveH = Math.pow(2, effectiveBits); return 1 - Math.exp(-Math.pow(n, 2) / (2 * effectiveH)); } }
执行后得出结果
实际应用中的重复概率
输入空间的影响
这里我们同样通过示例的方法来看一下效果
// 场景1: 有限的输入空间 public static void testLimitedInputSpace() { // 如果 MD5 的输入空间有限,碰撞概率会增加 String[] commonInputs = { "user123", "admin", "test", "password", "123456", "hello", "world", "example", "data", "info" }; Set<String> md5Results = new HashSet<>(); Set<String> uuidResults = new HashSet<>(); for (String input : commonInputs) { String md5 = DigestUtils.md5Hex(input); md5Results.add(md5); String uuid = UUID.randomUUID().toString(); uuidResults.add(uuid); } System.out.println("MD5 唯一性: " + md5Results.size() + "/" + commonInputs.length); System.out.println("UUID 唯一性: " + uuidResults.size() + "/" + commonInputs.length); } // 场景2: 大规模生成 public static void testLargeScaleGeneration() { int totalGenerations = 1_000_000; Set<String> uuids = new HashSet<>(); Set<String> md5Hashes = new HashSet<>(); Random random = new Random(); for (int i = 0; i < totalGenerations; i++) { // UUID 生成 uuids.add(UUID.randomUUID().toString()); // MD5 生成 (基于随机输入) String randomInput = "data-" + random.nextInt(1000000) + "-" + System.currentTimeMillis(); md5Hashes.add(DigestUtils.md5Hex(randomInput)); } System.out.println("UUID 碰撞数: " + (totalGenerations - uuids.size())); System.out.println("MD5 碰撞数: " + (totalGenerations - md5Hashes.size())); }
下面我们来执行上述两种场景下的 调用,返回结果如图
性能比较
这里我们通过示例代码来看一下具体的性能对比
public static void comparePerformance() { int iterations = 100000; // UUID 生成性能 long uuidStart = System.nanoTime(); for (int i = 0; i < iterations; i++) { UUID.randomUUID().toString(); } long uuidTime = System.nanoTime() - uuidStart; // MD5 生成性能 long md5Start = System.nanoTime(); for (int i = 0; i < iterations; i++) { DigestUtils.md5Hex("input-data-" + i); } long md5Time = System.nanoTime() - md5Start; System.out.printf("UUID 生成 %d 次耗时: %d ns%n", iterations, uuidTime); System.out.printf("MD5 生成 %d 次耗时: %d ns%n", iterations, md5Time); System.out.printf("MD5 比 UUID 慢 %.2f 倍%n", (double)md5Time / uuidTime); }
下面我们来通过main 函数调用上面的方法观察打印结果
实践场景
下面我们来归纳一下具体的实践场景
// 推荐:对于需要唯一性的场景使用 UUID public static String generateUniqueId() { return UUID.randomUUID().toString().replace("-", "").toLowerCase(); } // 谨慎使用:MD5 用于非安全敏感的场景 public static String generateCacheKey(String data) { return DigestUtils.md5Hex(data); } // 更好的选择:对于哈希需求,考虑更安全的算法 public static String generateSecureHash(String data) { // 使用 SHA-256 代替 MD5 return DigestUtils.sha256Hex(data); } // 结合使用:UUID + 哈希 public static String generateCompositeId(String content) { String uuid = UUID.randomUUID().toString().replace("-", ""); String contentHash = DigestUtils.md5Hex(content).substring(0, 8); return uuid + "-" + contentHash; }
执行结果如图
总结
关于两者的重复概率进行总结。
理论概率相同:两者都是 128 位,理论碰撞概率相同
实际概率差异:
- UUID:在随机生成条件下,重复概率极低,适合需要绝对唯一性的场景
- MD5:重复概率取决于输入空间:
- 输入随机且充足:概率与 UUID 相当
- 输入有限或可预测:碰撞概率显著增加
安全考虑:
- UUID v4 没有已知的密码学弱点
- MD5 存在已知的碰撞攻击,不适用于安全敏感场景
在实际应用中,UUID 的重复概率通常更低且更可预测,因为它的生成不依赖于外部输入数据的分布特性。