关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
短码(Short Code)是指将长 ID、URL 或业务主键压缩为短字符串的编码方案,广泛应用于短链生成、邀请码、订单号、兑换码、分享码等场景。
短码的生成即简单又复杂,简单的是很容易实现,但是要支持高并发场景就不那么容易了。
02 常见的方案
实现的技术方案也不少,我们一起来看看。
2.1 随机生成 + 查重
这种方案是最简单的,但是效率也低。效率低下的原因在于查重。
java
Set<String> codes = new HashSet<>();
@Test
void contextLoads05() {
String enableCode = "";
for (int i = 0; i < 100; i++) {
String code = RandomUtil.randomString(6);
System.out.println(code);
if (!codes.contains(code)) {
codes.add(code);
enableCode = code;
break;
}
}
if (StringUtils.isBlank(enableCode)) {
throw new RuntimeException("Failed to generate enable code");
}
System.out.println(enableCode);
}
Set集合正式使用时可以替换为Redis等其他高性能的中间件。而查询时一个自旋的过程,保证不重复。
测试了一下,如果自选超过10w次,重复的概率就会大幅度提高:

当然这种情况在并发较少的场景下,很容易满足。
2.2 UUID哈希截取
UUID可以做到不重复,但是长度却有点长,不适合作为短码。于是有人提出使用Hash截取的方式。
java
@Test
void contextLoads06() throws NoSuchAlgorithmException {
UUID uuid = UUID.randomUUID();
// 使用SHA-256算法
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 对UUID字符串进行哈希
byte[] hashBytes = digest.digest(uuid.toString().getBytes(StandardCharsets.UTF_8));
// 将哈希结果转为16进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
hexString.append(String.format("%02x", b));
}
// 截取前10位作为短码
String shortCode = hexString.substring(0, 6);
System.out.println("原始UUID: " + uuid);
System.out.println("短码: " + shortCode);
}
Hash的目的是为了让短码分布更均匀。但碰撞风险依然存在。如果使用还是还需要搭配查重才能保险。
2.3 号段模式+Base62
这是一种极佳的设计短码的方案,这里的号段也可以换成其他数字类型,如Snowflake等。
这种设计方案是将"短码生成"问题转化为"自增 ID 分配"问题,再通过确定性编码映射为短码。特征如下:
- 短码不需要随机生成,只需要**单射(一一对应)**地把数字 ID 编码为字符串
- 数字 ID 自增 → Base62 编码 → 6 位字符串
- 同 ID 必同码,不同 ID 必异码(数学上严格成立)
- 配合号段模式,避开每次生成都打数据库的性能瓶颈
我们下面将着重分析一下此方案设计。
03 详细设计
号段模式+Base62分为两部分,号段部分和编码部分。
3.1 号段设计
号段的设计可以通过数据库主键自增的特性,一次性生成多个如1000个。然后加载到缓存,分配给业务即可。
Mysql库表设计
我们可以设计号段表:
sql
CREATE TABLE `code_segment` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`biz_type` VARCHAR(32) NOT NULL COMMENT '业务类型(短链/邀请码/订单号)',
`current_max` BIGINT NOT NULL DEFAULT 0 COMMENT '当前号段已用最大ID',
`step` INT NOT NULL DEFAULT 1000 COMMENT '每次申请的步长',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
`threshold` INT NOT NULL DEFAULT 200 COMMENT '提前加载阈值',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_type` (`biz_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短码号段表';
这里需要注意是的current_max, step这两个字段,分别决定了到什么时候生成,生成的时候生成多少。
Redis设计
Redis的设计就非常简单了,本身就是基于内存的且单线程。直接使用incr自增方法即可。
3.2 编码
Base62编码虽然简单,但是使用的时候会发现几个问题
短码长度问题
java
@Test
void contextLoads03() {
String string = Base62Util.encode(0);
String astring = Base62Util.encode(916_132_831);
System.out.println(string); // 0
System.out.println(astring); // ZZZZZ
}
我们发现数字比较小的时候,编码之后短码的长度不够。在 ID >= 62^5(916_132_832),编码后天然 6 位以上。
而7位对应的ID为56_800_235_584L。为保证6位,我们的ID区间应该控制在916_132_832L~56_800_235_583L之间,可以支持500亿+的6位短码。使用的时候是足够了。
短码顺序问题
java
@Test
void contextLoads03() {
String string = Base62Util.encode(916_132_832);
String astring = Base62Util.encode(916_132_833);
System.out.println(string); // 100000
System.out.println(astring); // 100001
}
因为短码具有顺序性,这样的码不安全,可以猜到临近的短码。
这个问题又怎么解决呢?
哈希混淆
混淆的框架也是有现成的,这里介绍一个:org.hashids.Hashids
xml
<dependency>
<groupId>org.hashids</groupId>
<artifactId>hashids</artifactId>
<version>1.0.3</version>
</dependency>
直接来看案例:
java
@Test
void contextLoads04() {
Hashids hashids = new Hashids("57yW56iL5pyd6Iqx5aSV5ou+",6);
System.out.println(hashids.encode(0)); // pVe5Vb
System.out.println(hashids.encode(1)); // Kjznjw
System.out.println(hashids.encode(100000000)); //eLenzW
}
Hashids是基于Base62编码,然后增加了混淆。防止获取临近的Code。
到这里,6位短码的设计生成已经可以告一段落了。
04 小结
短码虽小但是设计并没有那么简单。高并发情况下,还需要考虑流量的情况。比如短链属于都多写少的场景,使用过程中还要限流、缓存雪崩等场景。