背景
短号,即用一个比较短的字符串映射某个比较长的字符串,使得更加易于人们写入。比如一个会议号的唯一ID是一个雪花算法生成的ID,如1077144667185860608,对于用户来说很难手动输入。这时候我们可以生成一个较短的会议ID,比如232 621 942,再把这个短号映射到原来的ID,这样就能够方便用户输入。
在现实中,很多场景并不需要一个永久的短号,也就是说短号是能够复用的。比如上面提到的会议号,会议结束后,即可回收;游戏中的房间号,在一个房间解散后,也可以被回收。因此我们可以通过回收不再使用的短号,从而控制短号的数量。
当然,我们也要求这个短号是随机的,至少是不那么容易被猜到的,否则如果是递增的,那么用户很容易猜到下一个短号,导致一些不愉快的事情发生。
因此,我们需要一种临时的、随机的短号生成算法。
原理
原理很简单,如下步骤:
- 随机生成一个短号字符串
- 判断是否存在,不存在则存储(也就是SetIfNotExists操作,很多数据库都支持)
- 如果第2步成功,则得到一个短号
- 否则回到第1步重试
当然,可能会有一些其他的需求,比如某些短号不能被使用,需要限定重试次数等,这些可以根据具体业务进行完善。
碰撞问题
碰撞也就是随机生成的短号已经存在数据库里,设置到数据库失败。如果碰撞概率太大,那么就会导致重试次数太多,影响性能。
这里的碰撞概率其实是和数据库里面的短号数量有关,随着数据库里面的短号越来越多,数据库里短号占短号总数的比例越来越大,那么碰撞的概率也会越来越大。
由于我们的短号是临时的,因此一个短号在使用完之后我们可以释放它,减少数据库里面的短号数量。
清理的方式一般有两种,一种是手动在使用完成之后马上清理,另外一种是定时的清理过期的(或是不再使用)的短号(可以避免手动清理失败导致短号泄露)。
提高短号的位数也可以减少碰撞的概率,因为这样可以降低数据库里短号占短号总数比例。
由于短号是临时的,因此数据库里面的短号数据并不会很多,碰撞的概率也不会特别大。下面是一个简单的测试。
测试
这里我使用Golang实现了算法,测试了不同短号长度下,数据库里不同短号数量的碰撞概率和性能。
碰撞概率
下面的字符集是0123456789,重试次数都是3。
| 短号长度 | 数据库里短号数量 | 数据库里短号占短号总数的比例% | 碰撞概率% | 失败概率% |
|---|---|---|---|---|
| 8 | 10000 | 0.1 | 0.050000 | 0.000000 |
| 8 | 100000 | 1 | 0.700000 | 0.000000 |
| 9 | 100000 | 0.1 | 0.050000 | 0.000000 |
| 9 | 1000000 | 1 | 0.210000 | 0.000000 |
| 10 | 1000000 | 0.1 | 0.020000 | 0.000000 |
| 10 | 10000000 | 1 | 0.070000 | 0.000000 |
可以看到,随着位数增加,碰撞概率会下降,在9位的时候,100W的短号其实碰撞概率是很低的,而增加到10位之后,基本上就不会发生碰撞了。
因此,数据库里短号占短号总数的比例最好是在1%以下,这样碰撞概率会很低。
当然,也可以设计更加智能的算法,在发现碰撞概率比较高的时候,可以提升短号位数,减少碰撞。
性能
这里我数据库使用了Redis,测试的的结果如下:
| 操作 | QPS |
|---|---|
| 分配短号 | 233612 |
| 释放短号 | 274216 |
这个性能基本上可以满足大部分业务的短号需求。
这个算法的瓶颈基本上在数据库上,因为我们最终需要把短号存储到数据库里。好在一般情况下,我们可以根据短号的值对数据库进行拆分。比如我们的短号是6位数字,那么我们可以这样拆分到10个数据库上,从而提高写能力。
| 数据库编号 | 存储短号范围 |
|---|---|
| 0 | 000000-099999 |
| 1 | 100000-199999 |
| 2 | 200000-299999 |
| 3 | 300000-399999 |
| 4 | 400000-499999 |
| 5 | 500000-599999 |
| 6 | 600000-699999 |
| 7 | 700000-799999 |
| 8 | 800000-899999 |
| 9 | 900000-999999 |