Base62编码生成6位不重复短码的背后设计

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

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 小结

短码虽小但是设计并没有那么简单。高并发情况下,还需要考虑流量的情况。比如短链属于都多写少的场景,使用过程中还要限流、缓存雪崩等场景。

相关推荐
头茬韭菜1 小时前
4.1-4.2 工具注册 — Fail-Closed 类型安全设计与四层工具架构
java·安全·架构
折哥的程序人生 · 物流技术专研1 小时前
Java 23 种设计模式:从踩坑到精通 | 番外:状态 vs 策略 —— if-else 消除的两条路,你走对了吗?
java·spring·状态模式·策略模式·java面试·java设计模式·从踩坑到精通
等什么秋1 小时前
idea2024下载
java·ide·intellij-idea
IT_陈寒1 小时前
JavaScript的隐式类型转换又双叒坑了我一次!
前端·人工智能·后端
她说彩礼65万1 小时前
Asp.net core 返回类型
数据库·后端·asp.net
学计算机的计算基1 小时前
LeetCode 图论四题精讲:BFS、拓扑排序、Trie 树的模板与优化
java·笔记·算法
浩瀚地学1 小时前
【面试算法笔记】0202-链表-基本功能实现
java·经验分享·笔记·算法·面试
3DVisionary2 小时前
如何用单目高速DIC把“看不见“的振动变成“数得清“的数据
java·spring boot·python·数码相机·微小振动·精密件·单目高速dic
tkevinjd2 小时前
416分割等和子集
java·python·算法·leetcode·职场和发展