如何生成低碰撞的6位客户用唯一字母数字预订码?
问题解答
一、随机洗牌方案的碰撞与优化
1. 现有代码的致命bug
你当前的随机生成代码存在逻辑错误:每次循环里charset.toCharArray().shuffle().toString()得到的不是字符数组的字符串,而是Java数组的默认标识字符串(比如[C@1234abcd),调用random()是从这个毫无意义的字符串里取字符,完全达不到随机选字符的目的。正确的写法应该是直接从字符集数组里随机选取:
val charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray() return (1..6).map { charset.random() }.joinToString("")
2. 碰撞概率分析
原字符集是36个字符(26字母+10数字),6位短码总共有36^6 = 21亿+种组合。但根据生日悖论:
- 生成约4.6万个短码时,碰撞概率达到1%
- 生成约117万个短码时,碰撞概率飙升至50%
如果你的预订量会超过这个量级,碰撞是大概率事件。
3. 优化与去重必要性
- 多次洗牌完全没用:洗牌后随机取和直接随机取的概率分布完全一致,只会浪费性能,对降低碰撞概率毫无帮助。
- 必须校验去重:只要用纯随机生成,就无法避免碰撞,唯一可靠的方式是生成后查询PostgreSQL数据库,确认短码未被使用,若已存在则重新生成。
二、Feistel密码实现的数组越界问题
你的问题出在encode6char函数的两个错误上:
- 掩码错误:
AllowedAlphabet的长度是32(8个数字+24个字母,去掉了易混淆的0、1、O、I),但你用了0x4f(十进制79),这个值远大于字符集的最大索引31,必然导致数组越界。应该用0x1f(十进制31),刚好匹配0-31的索引范围。 - 移位位数错误:32个字符对应2^5,所以每次应该右移5位,而不是6位——6次5位刚好覆盖30位的数值(5*6=30),和你
encrypt30bit处理的30位数据对应。
修正后的代码:
private val AllowedAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" fun encrypt30bit(value: Int): String { val mask15 = 0x7FFF // 15位无符号整数掩码,避免负数问题 var l1 = (value shr 15) and mask15 var r1 = value and mask15 for (i in 0..2) { val key = ((((1366 * r1 + 150889) % 714025) / 714025.0) * mask15).toInt() val l2 = r1 val r2 = l1 xor key l1 = l2 r1 = r2 } return encode6char((r1 shl 15) or l1) } private fun encode6char(number: Int): String { var num = number val chars = CharArray(6) repeat(6) { i -> chars[i] = AllowedAlphabet[num and 0x1f] // 用0x1f限制索引在0-31 num = num shr 5 // 每次右移5位,对应32进制 } return String(chars) }
另外补充:原代码中0x8FFF作为15位掩码会导致负数(Kotlin的Int是有符号的),换成0x7FFF可以确保处理的是正整数,避免后续计算出现异常。
Feistel方案的优势是无碰撞——每个数据库自增ID对应唯一的短码,完全不需要查询数据库校验,适合大规模场景使用。
内容的提问来源于stack exchange,提问作者BVtp
相关产品推荐
相关产品推荐

