文章目录

  • 1.需求描述
  • 2.需求分析
  • 3.字符集
  • 4.方法一:随机数+唯一性判断(不可逆)
  • 5.方法二:Hash+唯一性判断(不可逆)
  • 6.方法三:进制法(可逆)
  • 7.方法四:进制法+扩散、混淆(可逆)
    • 7.1 扩散和混淆的作用
    • 7.2 进制法+扩散
    • 7.3 进制法+扩散+混淆
  • 8.小结
  • 参考文献

1.需求描述

有一个业务需求,需要根据用户 ID(数值型 >=10000000)生成一个唯一的长 6 个字符的邀请码,用于邀请新用户注册。

2.需求分析

从业务需求和一般产品邀请码的使用体验上来看,邀请码有以下几个特点:

  • 不可重复:不用用户 ID 生成的邀请码是不同的;
  • 唯一确定:一个用户 ID 只能生成一个邀请码;
  • 是否可逆:是否需要通过邀请码反推对应的用户 ID 是什么。

本文将以 Golang 为例,给出根据用户 ID 生成唯一且不重复的邀请码的常见方法与实现示例。

3.字符集

首先需要确定组成邀请码的字符集,一般采用数字和英文大小写字母共计 62 个字符。如果长度时 6 的邀请码,那么空间大小 62^6 = 56,800,235,584,这是一个非常大的空间,足够用户量为亿级别的业务使用。

// AlphanumericSet 字母数字集。
var AlphanumericSet = []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9','a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
}

当然,为什么提升用户体验,可以把 O、o、0、I、1 这五个形似易混淆的字符去掉,本文就不去了。

4.方法一:随机数+唯一性判断(不可逆)

使用用户 ID 作为种子初始化随机数发生器,随机生成字符集下标,取出对应的字符拼接成邀请码。

注意,这里会存在生日问题,随着已生成的邀请码数量的上升,发生碰撞的概率将会大大增加。

如果生成 100W 个邀请码,假设前 100W 一个都不重复,那么下一个重复的概率是((1/62)^6 * 100W)≈1/5.6W,冲突率已经到了万分之一,远大于直觉(1/62)^6

// GetInvCodeByUID 获取指定长度的邀请码。
func GetInvCodeByUID(uid uint64, l int) string {r := rand.New(rand.NewSource(int64(uid)))var code []runefor i := 0; i < l; i++ {idx := r.Intn(len(AlphanumericSet))code = append(code, AlphanumericSet[idx])}return string(code)
}fmt.Println(GetInvCodeByUID(100000000, 6)) // uGkK9K
fmt.Println(GetInvCodeByUID(100000001, 6)) // UPFlHa
fmt.Println(GetInvCodeByUID(100000002, 6)) // keKZ01

缺点:

  • 唯一性判断引入第三方组件,增加依赖,降低了可靠性;
  • 在 >=99.99% 的情况下,唯一性判断都是通过的,浪费存储资源。

5.方法二:Hash+唯一性判断(不可逆)

对用户 ID 做 Hash(如 MD5)运算,获取散列值后取散列值的多个字节映射到字符集,然后组成邀请码。因为可能发生碰撞,所以需要做唯一性判断,比如借助 DB(Redis)来判断。

// GetInvCodeByUID 获取指定长度的邀请码
func GetInvCodeByUID(uid string, l int) string {// 因为 md5 值为 16 字节if l > 16 {return ""}sum := md5.Sum([]byte(uid))var code []runefor i := 0; i < l; i++ {idx := sum[i] % byte(len(AlphanumericSet))code = append(code, AlphanumericSet[idx])}return string(code)
}// 示例
fmt.Println(GetInvCodeByUID("100000000", 6)) // btCcwX
fmt.Println(GetInvCodeByUID("100000001", 6)) // sxQ1mq
fmt.Println(GetInvCodeByUID("100000002", 6)) // swA3pk

缺点:

  • 唯一性判断引入第三方组件,增加依赖,降低了可靠性。
  • 在 >=99.99% 的情况下,唯一性判断都是通过的,浪费存储资源。

我们可以写一个单测来看下长度为 6 的邀请码,在字符空间为 62 ,一千万用户量下的碰撞率。

func TestGetInvCodeByUID(t *testing.T) {sUID, eUID := 10000000, 20000000var md5ConCnt int  // md5 前 6 字节冲突次数var codeConCnt int // 邀请码冲突次数mHash := make(map[uint64]struct{})mCode := make(map[string]struct{})// 统计 1KW 个用户ID生成邀请码发生碰撞的次数t.Run("getConflictNumTestCase", func(t *testing.T) {for uid := sUID; uid < eUID; uid++ {sum := md5.Sum([]byte(strconv.Itoa(uid)))md5Value := uint64(sum[5]) | uint64(sum[4])<<8 | uint64(sum[3])<<16 | uint64(sum[2])<<24 |uint64(sum[1])<<32 | uint64(sum[0])<<40if _, ok := mHash[md5Value]; ok {md5ConCnt++codeConCnt++continue}mHash[md5Value] = struct{}{}code := GetInvCodeByUID(strconv.Itoa(uid), 6)if _, ok := mCode[code]; ok {codeConCnt++continue}mCode[code] = struct{}{}}if md5ConCnt != 0 || codeConCnt != 0 {t.Errorf("md5ConCnt=%v, codeConCnt=%v conRate=%v", md5ConCnt, codeConCnt, float64(codeConCnt)/float64(eUID-sUID))}})
}

执行单测命令go test -run TestGetInvCodeByUID$

--- FAIL: TestGetInvCodeByUID (13.01s)--- FAIL: TestGetInvCodeByUID/getConflictNumTestCase (13.01s)main_test.go:35: md5ConCnt=0, codeConCnt=900 conRate=9e-05
FAIL
exit status 1
FAIL    test    13.331s

可见前 6 个字节的散列值没有发生冲突,但是冲突率还是比较高的,在万分之一的级别。这种方式产生碰撞的原因是:虽然每个字节是不同值,但是对字符集大小取模后可能会相同,所以就有可能出现碰撞。随着用户量的增加,这里的碰撞概率会越来越高。

降低冲突率的办法是增加邀请码的空间,有两个办法:

  • 扩大生成邀请码的字符空间。
  • 增加邀请码的长度。

6.方法三:进制法(可逆)

用户 ID 是唯一的,生成一个唯一的邀请码也是理所当然的。

因为我们的用户ID是一个数值,可以将其看作是一个 62 进制的数,每一位的值范围是 0~61,类似于 10 进制数的每一位的范围是 0~9,取 62 进制数位的每一位作为字符集的下标,高位用 0 补全,这样我们便可以采用除法取整与取模来实现。

// GetInvCodeByUIDUnique 获取指定长度的邀请码
func GetInvCodeByUIDUnique(uid uint64, l int) string {var code []runefor i := 0; i < l; i++ {idx := uid % uint64(len(AlphanumericSet))code = append(code, AlphanumericSet[idx])uid = uid / uint64(len(AlphanumericSet)) // 相当于右移一位(62进制)}return string(code)
}// 示例
fmt.Println(GetInvCodeByUIDUnique(100000000, 6)) // ezAL60
fmt.Println(GetInvCodeByUIDUnique(100000001, 6)) // fzAL60
fmt.Println(GetInvCodeByUIDUnique(100000002, 6)) // gzAL60

缺点:

  • 连续用户 ID 生成的邀请码也是连续的,用户易输错。
  • 连续用户 ID 生成的邀请码也是连续的,规律性强,可以反推用户 ID。

如果业务场景对这两个缺点可以接受的话,那么这个方法就足够用了。

7.方法四:进制法+扩散、混淆(可逆)

7.1 扩散和混淆的作用

扩散 (Diffusion) 和混淆 (Confusion) 是 C. E. Shannon 提出的设计密码体制的两种基本方法,是现代分组密码的设计基础。在分组密码的设计中,充分利用扩散和混淆,可以有效地抵抗坏人从密文的统计特性推测出明文或密钥。

根据香农在《密码学的数学理论》论文中的描述,扩散则主要是用来使用明文和密文的关系变得尽可能的复杂,明文中任何一点小更动都会使得密文有很大的差异。

所谓混淆就是将密文与密钥之间的统计关系变得尽可能复杂,使得对手即使获取了关于密文的一些统计特性,也无法推测出密钥。使用复杂的非线性变换可以达到比较好的混淆效果,而简单的线性变换得到的混淆效果则不理想。

使用扩散和混淆的方式可以对进制法进行改进。

7.2 进制法+扩散

如何扩散呢?

可以将个位和其位作和后取余,即可把个位的变化传导到每一位。为了使结果看起来更随机,可以给每一位分配不同系数。

// GetInvCodeByUID 获取指定长度的邀请码。
func GetInvCodeByUID(uid uint64, l int) string {var code []runeidxs := make([]byte, l)for i := 0; i < l; i++ {idxs[i] = byte(uid % uint64(len(AlphanumericSet)))               // 获取 62 进制的每一位值idx := (idxs[i] + byte(i)*idxs[0]) % byte(len(AlphanumericSet)) // 其他位与个位加和再取余(让个位的变化影响到所有位)code = append(code, AlphanumericSet[idx])uid = uid / uint64(len(AlphanumericSet)) // 相当于右移一位(62进制)}return string(code)
}// 示例
fmt.Println(GetInvCodeByUIDUniqueNew(100000000, 6)) // eN2r08
fmt.Println(GetInvCodeByUIDUniqueNew(100000001, 6)) // fO4u4d
fmt.Println(GetInvCodeByUIDUniqueNew(100000002, 6)) // gP6x8i

从示例结果可以看出,相邻的用户 ID 对应的邀请码虽然不是连续的,但是每一位还是有很强的规律,左起第一、二位间隔一,第三位间隔二,第四位间隔三,以此类推。

如何隐藏这些规律呢?

我们可以对用户 ID 进行变换,比如放大或者加盐

放大可以对用户 ID 乘以一个与 62(字符集大小)互质的数,比如 3。这是根据循环群的性质:若 m 和 p 互质,则 m 可以作为整数同余加法群 [0, p) 的生成元,通过累加取模运算生成整个群,即 ( id * m ) % p 的结果包含 [0, p) 的所有整数,这保证了放大后结果的分布和原数据的分布同样均匀。

举个例子:
如果 p = 3,那么 [0, p) = {0, 1, 2},2 与 3 互质,那么我们可以通过 2 这个生成元生成整个群。

(0*2) % 3 = 0 % 3                   = 0
(1*2) % 3 = 2 % 3                  = 2
(2*2) % 3 = (2 + 2) % 3           = 1
(3*2) % 3 = (2 + 2 + 2) % 3          = 0
(4*2) % 3 = (2 + 2 + 2 + 2) % 3     = 2
(5*2) % 3 = (2 + 2 + 2 + 2 + 2) % 3    = 1
...

右侧的余数便组成了一个 3 阶循环群 {0, 1, 2}。3 阶指元素个数,循环指不管生成元 2 累加多少次,对 3 取余后结果都是在 {0, 1, 2} 中,出现循环的情况。

对 ID 放大后,我们也可以加个盐,可以是一个固定值,也可以是每个用户 ID 对应一个相同的值,我这里取一个固定值 123456789。盐不要太小,太小缺乏隐蔽性;也不能太大,太大会占用过多用户 ID 的取值空间。比如位数可以和最大用户 ID 的位数保持一致。

放大和加盐后的效果:

const (PRIME1 = 3SALT  = 123456789
)
uid = uid*PRIME1 + SALT// 示例
fmt.Println(GetInvCodeByUID(100000000, 6))  // dFchi3
fmt.Println(GetInvCodeByUID(100000001, 6))  // gIiqui
fmt.Println(GetInvCodeByUID(100000002, 6))  // jLozGx

可见,邀请码的规律性肉眼已经看不出来了。

其实做到这一步,已经满足绝大部分业务场景的要求。下面再加上混淆处理,仅供了解。

7.3 进制法+扩散+混淆

然后是混淆,如何混淆呢?

混淆我用了 P-box 的方式,其实就是将数字洗牌。比如把 1234567 洗成 5237641。这样处理之后可以隐藏明文和密文之间的关系。洗牌的方式也很简单,选择一个和 CODE_LENGTH(本文中为 6)互质的数 PRIME2(可以选择 5),和数组角标相乘取余即可(原理同 PRIME1)。

最终的代码如下:

const (PRIME1 = 3 // 与字符集长度 62 互质PRIME2 = 5 // 与邀请码长度 6 互质SALT   = 123456789  // 随意取一个数值
)// GetInvCodeByUIDUniqueNew 获取指定长度的邀请码。
func GetInvCodeByUIDUniqueNew(uid uint64, l int) string {// 放大 + 加盐。uid = uid*PRIME1 + SALTvar code []runeslIdx := make([]byte, l)// 扩散。for i := 0; i < l; i++ {slIdx[i] = byte(uid % uint64(len(AlphanumericSet)))                   // 获取 62 进制的每一位值slIdx[i] = (slIdx[i] + byte(i)*slIdx[0]) % byte(len(AlphanumericSet)) // 其他位与个位加和再取余(让个位的变化影响到所有位)uid = uid / uint64(len(AlphanumericSet))                              // 相当于右移一位(62进制)}// 混淆。for i := 0; i < l; i++ {idx := (byte(i) * PRIME2) % byte(l)code = append(code, AlphanumericSet[slIdx[idx]])}return string(code)
}// 示例
fmt.Println(GetInvCodeByUID(100000000, 6)) // d3ihcF
fmt.Println(GetInvCodeByUID(100000001, 6)) // giuqiI
fmt.Println(GetInvCodeByUID(100000002, 6)) // jxGzoL

8.小结

本文介绍了常见的通过用户 ID 生成唯一邀请码的几种方法,大家可以根据业务场景选择使用。

当然,本文介绍的方法可能并不满组某些业务场景的需求,比如用户 ID 并不是数值型,那么就需要我们根据具体场景用合适的方法解决问题。没有最好的方法,只要能解决问题就是好方法。


参考文献

维基百科.阶 (群论)
简单的密码学生成唯一邀请码
记录使用 Golang math/rand 随机数遇到的坑
维基百科.混淆与扩散
CSDN.以模6加法群(Z6,+)认识循环群及其特点

用户 ID 生成唯一邀请码的几种方法相关推荐

  1. 用户ID生成唯一邀请码的几种方法

    文章目录 1.需求描述 2.需求分析 3.字符集 4.方法一:随机数+唯一性判断(不可逆) 5.方法二:Hash+唯一性判断(不可逆) 6.方法三:进制法(可逆) 7.方法四:进制法+扩散.混淆(可逆 ...

  2. php生成游客id_PHP根据用户ID生成唯一的邀请码 | 剑花烟雨江南

    在开发中,我们可能会遇到需要生成用户唯一的邀请码,来做一些邀请活动,接下来,我们思考如何生成唯一的邀请码,且可以根据邀请码获取到用户id. 首先,我们可以考虑使用用户user_id 来生成邀请码,并生 ...

  3. Java生成唯一标识码的三种方式

    Java生成唯一标识码的三种方式 前言 我们经常会遇到这样的场景,需要生成一个唯一的序列号来表明某一个数据的唯一性,在单节点的应用中我们可以简单地使用一个自增的整型来实现实现,但是在分布式情况下这个方 ...

  4. PHP开发日志 ━━ php、javascript生成二维码的三种方法

    最简单最实例的goolge开源方法 推荐手册:php完全自学手册 1.google开放api 代码如下: $urlToEncode="http://www.helloweba.com&quo ...

  5. Java生成二维码的两种方法

    本文介绍Java生成二维码的两种方法,这两种方法都依赖于google提供的二维码依赖包. 一种是自己写工具类,代码可以网上抄. 另一种是使用hutool第三方工具类的依赖包,不用自己抄代码. 一.自定 ...

  6. PHP生成唯一邀请码

    需求是每个用户都有个邀请码,邀请字母数字组成的10位字符串,首先想到的就是生成随机字符串了,如下 public function createNonceStr($length = 16) {$char ...

  7. 程序猿生成二维码的三种方法(在线接口+在线网站+本地程序)

    一.前言 程序猿经常在web.app.小程序等应用生成二维码,生成二维码方式有多种,如果你第一次接触,网上搜出来的基本是使用本地程序生成的,其实这种最麻烦,有更好的方式,就是使用在线接口的,简单方便快 ...

  8. php生成二维码的三种方法

    最简单最实例的goolge开源方法 1.google开放api 代码如下: $urlToEncode="http://www.helloweba.com"; generateQRf ...

  9. html 生成唯一码,生成唯一邀请码.html

    Document let sourceString = 'ZDOWGVJ5ASB3IRP9QM41EYFCU2TN76XH0KL';//三十五进制字符串长度不足8,用数字8高位补全 function ...

  10. java生成一条唯一的邀请码_根据用户id生成一个唯一邀请码

    需求描述:根据用户id生成与之对应的唯一邀请码,范围为'0-9A-Z'. 这个需求的重点在于加粗的部分,也就是要能够根据邀请码反推出用户ID,这样邀请码就不用入库了,在用户量很大的情况下,性能可以得到 ...

最新文章

  1. java 写日志太快_Log4j2 快速入门 —— 定期/定大小生成日志文件(三)
  2. Linux批量复制文件夹里的文件到另一个文件夹(问题:linux sh: /bin/cp: Argument list too long )
  3. android camera fragment,Android Camera 模块分析(三)
  4. 从缓冲池命中率角度判断自己的MYSQL数据库是否需要扩容内存
  5. [洪流学堂]Hololens开发高级篇2:手势(Gesture)
  6. JDK1.8之Stream
  7. jquery--动态篇
  8. 使用php进行财务统计,基于php的基金财务数据接口调用代码实例
  9. 13. ZooKeeper最佳实践
  10. mysql中kill掉所有锁表的进程
  11. 云计算技术简述及发展分析
  12. 双层PDF—OCR文字识别系统
  13. oracle建表及授权
  14. linux——alsa中多个声卡设备时打开某一指定声卡的PCM设备
  15. JAVA|大小写英文字母表
  16. SAXParseException 前言中不允许有内容
  17. Apm飞控学习笔记之-电机解锁和故障保护-Cxm
  18. python 微信授权 昵称乱码解决
  19. 无损音频压缩——APE和FLAC比较
  20. S7-PLCSIM与WINCC 通讯【工控老鬼】

热门文章

  1. 《信号与系统学习笔记》—z变换(二)
  2. VS2017_Reshaper
  3. 挑战程序设计竞赛学习笔记2
  4. 解决vagrant up出现:An error occurred while downloading the remote file. The errormessage
  5. 小莫微信影视机器人-自定义对接影视教程
  6. chrome Flash Player离线安装包下载
  7. stm32怎么入门?数电模电单片机先学哪个?
  8. STM32F103与MCGS的通讯
  9. 统计学习导论(ISLR)第五章bootstrap和交叉验证课后习题
  10. linux中实现getch函数