3

前言

当前有一个需求就是生成一个二维码,我们知道二维码的本质就是url,如果url越长生成的二维码就越密集,就越难扫。

这里我拿spring的官网链接进行测试生成二维码,这里发现二维码非常密集,如果在固定长度的位置进行插入二维码就变得非常难扫

https://docs.spring.io/spring-authorization-server/reference/core-model-components.html

image.png

这时候就需要把长链接生成短链接,也就是短码,这样的好处就是方便进行扫描

https://docs.spring.io/XrxeqVqy

这个url是我网站首页经过缩短以后的效果,根据短码进行生成的二维码,你会发现不会很密集,就特别好扫码。

image.png

如何生成短链接

短链接服务的核心功能是为长链接生成一个唯一的“别名”(短码)。

短码一般是由 [a - z, A - Z, 0 - 9] 这62 个字母或数字组成,短码的长度也可以自定义,但一般不超过8位。比较常用的都是6位,6位的短码已经能有568亿种的组合:(26+26+10)^6 = 56800235584,已满足绝大多数的使用场景。

0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

常见生成的算法自增 ID、62位自增、摘要算法与随机数

下面我只讲解前2个案例

自增 ID

该方法是一种无碰撞的方法,原理是,每新增一个短码,就在上次添加的短码id基础上加1,然后将这个10进制的id值,转化成一个62进制的字符串。

一般利用数据表中的自增id来完成:每次先查询数据表中的自增id最大值max,那么需要插入的长网址对应自增id值就是 max+1,将max+1转成62进制即可得到短码。

以下网址生成短码

https://docs.spring.io/spring-authorization-server/reference/core-model-components.html

查询数据库中当前自增 ID 最大值,当前数据库中最大自增 ID 为 1000

生成新自增id

id=1001

将 10 进制自增 ID 转为 62 进制

短码 = 62进制(1001) = 'g9'

生成的短码为

https://docs.spring.io/g9

代码实现

public class ShortCodeGenerator {
    private long currentId;
    private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    // 构造函数,支持自定义起始 ID
    public ShortCodeGenerator(long startId) {
        this.currentId = startId;
    }

    // 生成短码的方法
    public synchronized String generateShortCode() {
        currentId++; // 自增 ID
        return encodeBase62(currentId); // 转换为 62 进制短码
    }

    // 62 进制编码
    private String encodeBase62(long num) {
        StringBuilder shortCode = new StringBuilder();
        while (num > 0) {
            shortCode.append(BASE62.charAt((int) (num % 62))); // 获取当前余数对应字符
            num /= 62; // 除以 62
        }
        return shortCode.reverse().toString(); // 翻转结果
    }
}

输出案例

初始化id为1000

第一次调用生成短码
    当前 ID:1001

62 进制编码:
    1001 % 62 = 9 -> BASE62[9] = '9'
    1001 / 62 = 16
    16 % 62 = 16 -> BASE62[16] = 'G'
    短码为 G9。

更多案例

自增 ID62 进制编码
1001G9
1002GA
1003GB
1004GC
1005GD

image.png

获取 62 进制字符的下一个字符进行自增生成短码

短码生成的核心在于,将每个短码看作一个基于 62 进制的“数字”。例如,0 是最小值,Z 是单字符的最大值。

实现流程

短码的递增,就像十进制数字的加法。
每个字符对应一个“位”,如果当前位需要进位,则调整该位为起始字符 0,同时对前一位递增。

  1. 初始化短码:
    初始值为 0,即最短长度的短码。
    短码的长度会随着递增逐渐增加。
  2. 第一个字符递增:
    如果当前短码未达到最大字符(Z),仅递增第一个字符即可。
    示例:0 → 1 → 2 → ... → 9 → a → b → ... → z → A → ... → Z。
  3. 进入下一轮:
    当第一个字符递增到 Z 后,新增一位,短码长度增加 1。
    示例:Z → 00 → 01 → 02 → ...。

Base62 字符集

private static final String BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

获取下一个字符

如果当前的短码是 a,那么下一个字符应该是 b。如果当前字符是 9,则下一个字符应该是 a

   /**
     * 获取字符在62进制中的下一个字符
     * @param c 被查询的字符
     * @return 下一个字符
     */
    public static char getNextCharInBase62(char c) {
        int index = BASE62CHARS.indexOf(c);
        if (index == -1) {
            throw new IllegalArgumentException("Character is not a valid Base62 character.");
        }
        return BASE62CHARS.charAt((index + 1) % BASE62CHARS.length());
    }

判断是否需要进位

在十进制系统中,当数字达到 9 时,进位并将下一位加一。同样地,在 Base62 系统中,当字符达到 Z 时需要进位

private static boolean isCarry(char c) {
    return c == BASE62_CHARS.charAt(BASE62_CHARS.length() - 1);
}

生成下一个短码

如果需要进位,方法会计算当前字符的下一个字符。
如果不需要进位,直接添加当前字符。

   /**
     * 获取字符串 +1 之后的值
     * @param str 原始字符串
     * @return +1 后的字符串
     */
    public static String next(String str) {
        StringBuilder builder = new StringBuilder();
        // 初始化进位符
        boolean carryBit = true;
        // 遍历
        for (int i = str.length() - 1; i >= 0; i--) {
            if (carryBit) {
                // 如果有进位就继续计算
                builder.append(getNextCharInBase62(str.charAt(i)));
                carryBit = isCarryBit(str.charAt(i));
            } else {
                // 否则直接拼接字符
                builder.append(str.charAt(i));
            }
        }
        // 如果算完了还有进位, 最前面再加个 0
        if (carryBit) {
            builder.append(getNextCharInBase62());
        }
        // 反转,返回
        return builder.reverse().toString();
    }

举例

 Assertions.assertEquals(Base62Util.next("0"), "1");
 Assertions.assertEquals(Base62Util.next("9"), "a");
 Assertions.assertEquals(Base62Util.next("z"), "A");
 Assertions.assertEquals(Base62Util.next("Z"), "00");
 Assertions.assertEquals(Base62Util.next("0Z"), "10");
 Assertions.assertEquals(Base62Util.next("ZZ"), "000");
 Assertions.assertEquals(Base62Util.next("1ZZ"), "200");

生成短码代码实现

    /**
     * 生成短链接
     *
     * @param saveRequest 保存的url
     * @return 返回短码
     */
    @Override
    public synchronized String generateShortCode(ShortCodeDto.SaveRequest saveRequest) {
        return this.shortCodeRepository.findByUrlEquals(saveRequest.getUrl())
                .map(shortCode -> this.domain + shortCode.getShortKey())
                .orElseGet(() -> {
                    ShortCode newShortCode = new ShortCode();
                    newShortCode.setUrl(saveRequest.getUrl());

                    String nextShortKey = (this.shortCodeRepository.count() == 0)
                            ? Base62Util.next()
                            : Base62Util.next(
                            this.shortCodeRepository.findTopByOrderByIdDesc()
                                    .orElseThrow(() -> new IllegalStateException("Failed to fetch latest short code"))
                                    .getShortKey()
                    );

                    newShortCode.setShortKey(nextShortKey);
                    ShortCode savedShortCode = this.shortCodeRepository.save(newShortCode);
                    return this.domain +  savedShortCode.getShortKey();
                });
    }

image.png

使用流程可以描述为:

  • 用户在浏览器地址栏中输入短网址,或者点击某个短网址链接。
  • 这实际上触发了对该短网址的 GET 请求(例如,请求 http://docs.spring.io/0)。
  • 服务器接收到请求后,解析短码 0,并从数据库中查找与该短码对应的长链接。
  • 找到匹配的长链接后,服务器通过 HTTP 重定向 将用户引导到对应的长链接地址。
  • 最终,用户的浏览器自动跳转到该长链接页面,完成访问。

kexb
544 声望22 粉丝