短链接服务Octopus的实现与源码开放(上)

本文涉及的产品
.cn 域名,1个 12个月
简介: 半年前(2020-06)左右,疫情触底反弹,公司的业务量不断提升,运营部门为了方便短信、模板消息推送等渠道的投放,提出了一个把长链接压缩为短链接的功能需求。当时为了快速推广,使用了一些比较知名的第三方短链压缩平台,存在一些问题

前提



半年前(2020-06)左右,疫情触底反弹,公司的业务量不断提升,运营部门为了方便短信、模板消息推送等渠道的投放,提出了一个把长链接压缩为短链接的功能需求。当时为了快速推广,使用了一些比较知名的第三方短链压缩平台,存在一些问题:

  • 收费贵
  • 一些情况下,短链域名在部分第三方平台例如微信会被封杀
  • 回源数据没有办法定制处理方案,无法打通整个业务链路进行数据分析和跟踪


基于此类问题,决定自研一个(长链接压缩为)短链接服务,当时刚好同步进行微服务拆分,内部很多微服务需要重新命名,组内的一个妹子说不如就用Github的吉祥物去命名octopus cat(章鱼猫)去命名,但是考虑到版权问题,去掉了她最喜欢的猫,剩下章鱼,以octopus命名:


微信截图_20220513181754.png


(项目的描述还打错字了,应该是"短链接")因为实现的功能并不复杂,初版于2020-06月底就发布。octopus的实现参考了互联网中几篇关于"短链服务实现"浏览量比较高的文章,下面从实现原理、服务实现和部署架构等方面展开谈谈。


基本原理



短链服务的核心就是构建短链接和长链接的唯一映射关系,依赖到一个高性能、排列组合数量大而且破解难度大的映射标识生成算法。


构建唯一映射关系


微信截图_20220513181802.png


上图是笔者收到的京东白条分期还款结果提醒短信,短信内容也包含了一个短链https://3.cn/j/xxxxxxx,把它拷贝到浏览器中打开,发现客户端会重定向到长链https://jrmkt.jd.com/ptp/wl/vouchers.html?activityId=${activityId}&uep_p=${uep_p}&uep_template_id=${uep_template_id}&uep_timestamp=${uep_timestamp},然后跳入一个H5的登录页,登录后再跳进一个白条攻略页面。这里其实一个长链其实可以压成多个短链,短链可以相同域名,也可以使用不同的域名:


微信截图_20220513181811.png


访问https://3.cn/j/xxxxxxx短链接具体的交互流程猜测如下:


微信截图_20220513181817.png


jrmkt.jd.com和3.cn查证都是doge东的域名


构建唯一映射关系其实就是基于一个固定的长链接,映射到一个或者多个可以动态生成的短链接,这个唯一映射关系,要求生成的短链接满足:


  • 不容易被破解(使用数字例如数据库的自增主键作为唯一映射标识容易被人遍历出来进行恶意调用)
  • 不能重复(一个短链接只能对应一个长链接,当然一个长链接可以对应多个短链接)
  • 长度尽可能短,这是因为第三方推送的报文内容一般有长度限制,如果短链过长,会导致不容易传输,还会令到推送内容字数受限(试想运营商短信投放内容最大长度为30个字符长度,短链已经占了20个字符长度,剩下只有10个字符长度让运营同事去发挥,显然不合理)
  • 如果链接过长,生成的二维码里面的"码点"会十分密集,不利于客户端识别和传输,刚好笔者公司运营有使用二维码的场景,所以必须尽可能缩短链接的长度


总的来说,这个唯一映射关系中的映射标识需要像Hash算法生成的Hash码那样具备高唯一性和低碰撞频率,同时具备短小易传输的特点,具体如何去生成映射唯一标识见下一节"压缩码生成算法"。


压缩码生成算法


这里的"压缩码"(compression_code)是笔者杜撰出来的名词,在本文中它的含义是短链接URL的路径部分(为了节省长度,除了协议和域名部分,短链的URL只有第一段路径):


微信截图_20220513181825.png


其中,协议部分基本是固定为https://(从安全性来看不建议使用http://),短链域名可以购买尽可能长度短的域名如t.cn,不过有先见之明的资本家一般会把所有优质的短域名买下并且把价格提到很高,所以域名的长度基本也是很难控制的因素,剩下可控的就是压缩码部分。压缩码部分是可控的,但因为它是URL的一部分,只要确保所使用的字符不会被URL编码转义,那么长度是人为可控的。假设我们使用的是26个字母的大小写,加上10个数字,那么对于N位压缩码可以表示的最大组合数量为:


  • N = 4,组合数为62 ^ 4 = 14_776_336147万接近148
  • N = 5,组合数为62 ^ 5 = 916_132_8329.16亿左右
  • N = 6,组合数为62 ^ 6 = 56_800_235_584568亿左右


一般来说,组合数越小破解的难度就越小,组合数越大,要求压缩码长度越大,所以常用的长度就是456,而且后期可以对失效的长链进行压缩码回收或者禁用,这三个长度对于绝大对数生产短链的应用场景都能满足。octopus在实现的时候选用的是6位长度的压缩码,无他,因为有现成的成熟的参考方案:62进制数刚好由字符0-9 a-z A-Z组成,生成压缩码的时候,只需要生成一个唯一的10进制数,然后再基于此10进制数转换为62进制数数即可。说到这里,看起来的方案如下:


微信截图_20220513181832.png


虚线部分一般依赖一种高效而且低冲突的摘要算法,如MurmurHash,而第(1)步的实线部分就是生成一个全局唯一的10进制序列,常用的手法有:


  • 数据库自增序列(如自增主键)
  • Snowflake算法
  • 自研的类似UUID算法生成全局唯一的序列值


考虑到之前笔者钻研过Snowflake算法的原理,这里简单使用Snowflake算法生成自增序列,使用了下面的流程进行压缩码生成和分配:


微信截图_20220513181840.png


因为运营部门对短链生成的批量不大,而且短链域名只有一个,所以简单起见,一次压缩操作直接消耗掉一个压缩码,不考虑不同短链域名对同一个压缩码进行共享,也不考虑压缩码的回收问题


服务实现



短链服务的主访问入口一般QPS极高,因此需要想尽一切办法降低该入口的耗时,考虑可以用Redis做缓存承载入口的流量,基础架构选型如下:


  • JDK1.8+:生产部署使用JDK11
  • MVC框架与容器:spring-boot-starter-webflux或者spring-cloud-gateway,主要是必须使用Netty作为底层通讯容器
  • 内部RPC框架:Dubbo
  • 服务注册与发现:Nacos
  • 可选APM工具:Pinpoint

中间件依赖(因为之前整个服务集群都上云了,低负载的服务共用了部分中间件):

  • MySQL8.x
  • Redis5.x普通主从或者哨兵集群
  • RabbitMQ3.8.x集群,使用镜像队列


服务的设计图如下:


微信截图_20220513181848.png


最新的版本考虑把黑白名单的拦截器去掉,替换成一个基于布隆过滤器现实的拦截器。服务使用了两个拦截器(虽然Filter翻译是过滤器,但是出于习惯,下文称为拦截器)链,容器提供的拦截器组成的拦截器链主要是负责服务安全、调用链跟踪的功能,而服务内部自定义的拦截器链主要是实现请求参数解析、URL转换、重定向和异步事件记录等功能。


模块划分:


- (ROOT) octopus
  - octopus-contract
  - octopus-server
复制代码


octopus-contract模块必须脱离父POM的管理,方便单独迭代更新。


数据库设计


一共使用了5个表:


微信截图_20220513181856.png


具体的初始化DDL如下:


CREATE DATABASE `db_octopus` CHARSET 'utf8mb4' COLLATE 'utf8mb4_unicode_520_ci';
USE `db_octopus`;
CREATE TABLE `url_map`
(
    `id`               BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    `short_url`        VARCHAR(32)     NOT NULL COMMENT '短链URL',
    `long_url`         VARCHAR(768)    NOT NULL COMMENT '长链URL',
    `short_url_digest` VARCHAR(128)    NOT NULL COMMENT '短链摘要',
    `long_url_digest`  VARCHAR(128)    NOT NULL COMMENT '长链摘要',
    `compression_code` VARCHAR(16)     NOT NULL COMMENT '压缩码',
    `description`      VARCHAR(256) COMMENT '描述',
    `url_status`       TINYINT         NOT NULL DEFAULT 1 COMMENT 'URL状态,1:正常,2:已失效',
    `create_time`      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `edit_time`        DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `creator`          VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '创建者',
    `editor`           VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '更新者',
    `deleted`          TINYINT         NOT NULL DEFAULT 0 COMMENT '软删除标识',
    `version`          BIGINT          NOT NULL DEFAULT 1 COMMENT '版本号',
    UNIQUE uniq_compression_code (`compression_code`),
    INDEX idx_short_url (`short_url`),
    INDEX idx_short_url_digest (`short_url_digest`),
    INDEX idx_long_url_digest (`long_url_digest`)
) COMMENT 'URL映射表';
CREATE TABLE `domain_conf`
(
    `id`            BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    `domain_value`  VARCHAR(16)     NOT NULL COMMENT '域名',
    `protocol`      VARCHAR(8)      NOT NULL DEFAULT 'https' COMMENT '协议,https或者http',
    `domain_status` TINYINT         NOT NULL DEFAULT 1 COMMENT '域名状态,1:正常,2:已失效',
    `create_time`   DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `edit_time`     DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `creator`       VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '创建者',
    `editor`        VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '更新者',
    `deleted`       TINYINT         NOT NULL DEFAULT 0 COMMENT '软删除标识',
    `version`       BIGINT          NOT NULL DEFAULT 1 COMMENT '版本号',
    UNIQUE uniq_domain (`domain_value`)
) COMMENT '域名配置';
CREATE TABLE `compression_code`
(
    `id`               BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    `compression_code` VARCHAR(16)     NOT NULL COMMENT '压缩码',
    `code_status`      TINYINT         NOT NULL DEFAULT 1 COMMENT '压缩码状态,1:未使用,2:已使用,3:已失效',
    `sequence_value`   VARCHAR(64)     NOT NULL COMMENT '序列(盐)',
    `strategy`         VARCHAR(8)      NOT NULL DEFAULT 'sequence' COMMENT '策略,sequence或者hash',
    `create_time`      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `edit_time`        DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `creator`          VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '创建者',
    `editor`           VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '更新者',
    `deleted`          TINYINT         NOT NULL DEFAULT 0 COMMENT '软删除标识',
    `version`          BIGINT          NOT NULL DEFAULT 1 COMMENT '版本号',
    UNIQUE uniq_compression_code (`compression_code`)
) COMMENT '压缩码';
CREATE TABLE `visit_statistics`
(
    `id`                            BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    `create_time`                   DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `edit_time`                     DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `creator`                       VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '创建者',
    `editor`                        VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '更新者',
    `deleted`                       TINYINT         NOT NULL DEFAULT 0 COMMENT '软删除标识',
    `version`                       BIGINT          NOT NULL DEFAULT 1 COMMENT '版本号',
    `statistics_date`               DATE            NOT NULL DEFAULT '1970-01-01' COMMENT '统计日期',
    `pv_count`                      BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '页面流量数',
    `uv_count`                      BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '独立访客数',
    `ip_count`                      BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '独立IP数',
    `effective_redirection_count`   BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '有效跳转数',
    `ineffective_redirection_count` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '无效跳转数',
    `compression_code`              VARCHAR(16)     NOT NULL COMMENT '压缩码',
    `short_url_digest`              VARCHAR(128)    NOT NULL COMMENT '短链摘要',
    `long_url_digest`               VARCHAR(128)    NOT NULL COMMENT '长链摘要',
    UNIQUE uniq_date_code_digest (`statistics_date`, `compression_code`)
) COMMENT '访问数据统计';
CREATE TABLE `transform_event_record`
(
    `id`               BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    `create_time`      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `edit_time`        DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `creator`          VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '创建者',
    `editor`           VARCHAR(32)     NOT NULL DEFAULT 'admin' COMMENT '更新者',
    `deleted`          TINYINT         NOT NULL DEFAULT 0 COMMENT '软删除标识',
    `version`          BIGINT          NOT NULL DEFAULT 1 COMMENT '版本号',
    `unique_identity`  VARCHAR(128)    NOT NULL COMMENT '唯一身份标识,SHA-1(客户端IP-UA)',
    `client_ip`        VARCHAR(64)     NOT NULL COMMENT '客户端IP',
    `short_url`        VARCHAR(32)     NOT NULL COMMENT '短链URL',
    `long_url`         VARCHAR(768)    NOT NULL COMMENT '长链URL',
    `short_url_digest` VARCHAR(128)    NOT NULL COMMENT '短链摘要',
    `long_url_digest`  VARCHAR(128)    NOT NULL COMMENT '长链摘要',
    `compression_code` VARCHAR(16)     NOT NULL COMMENT '压缩码',
    `record_time`      DATETIME        NOT NULL COMMENT '记录时间戳',
    `user_agent`       VARCHAR(2048) COMMENT 'UA',
    `cookie_value`     VARCHAR(2048) COMMENT 'cookie',
    `query_param`      VARCHAR(2048) COMMENT 'URL参数',
    `province`         VARCHAR(32) COMMENT '省份',
    `city`             VARCHAR(32) COMMENT '城市',
    `phone_type`       VARCHAR(64) COMMENT '手机型号',
    `browser_type`     VARCHAR(64) COMMENT '浏览器类型',
    `browser_version`  VARCHAR(128) COMMENT '浏览器版本号',
    `os_type`          VARCHAR(32) COMMENT '操作系统型号',
    `device_type`      VARCHAR(32) COMMENT '设备型号',
    `os_version`       VARCHAR(32) COMMENT '操作系统版本号',
    `transform_status` TINYINT         NOT NULL DEFAULT 0 COMMENT '转换状态,1:转换成功,2:转换失败,3:重定向成功,4:重定向失败',
    INDEX idx_record_time (`record_time`),
    INDEX idx_compression_code (`compression_code`),
    INDEX idx_short_url_digest (`short_url_digest`),
    INDEX idx_long_url_digest (`long_url_digest`),
    INDEX idx_unique_identity (`unique_identity`)
) COMMENT '转换事件记录';
复制代码


压缩码生成模块实现


压缩码生成的方法比较简单:


private final SequenceGenerator sequenceGenerator;    # <------------- 雪花算法序列生成器
@Value("${compress.code.batch:100}")
private Integer compressCodeBatch;
......
private void generateBatchCompressionCodes() {
    for (int i = 0; i < compressCodeBatch; i++) {
        long sequence = sequenceGenerator.generate();
        CompressionCode compressionCode = new CompressionCode();
        compressionCode.setSequenceValue(String.valueOf(sequence));
        String code = ConversionUtils.X.encode62(sequence);    # <-------------- 10进制转62进制
        code = code.substring(code.length() - 6);
        compressionCode.setCompressionCode(code);
        compressionCodeDao.insertSelective(compressionCode);
    }
}
复制代码


总是批量生成可用的压缩码,查询的时候只需要查出当前未被使用的第一个压缩码即可。


相关文章
|
6月前
|
安全 API 开发工具
微信开发:API接口与ipad协议的深度比较及最佳选择
微信开发:API接口与ipad协议的深度比较及最佳选择
|
搜索推荐 数据安全/隐私保护
直播程序源码OAuth协议:开放授权的重要性
在直播程序源码平台,需要OAuth协议这样的协议,OAuth协议保证了用户在使用直播程源码平台结合第三方应用程序的技术功能时的安全性与方便性,也为直播程序源码平台的用户提供了许多互动功能,是让直播程源码平台成为更高质量、更好的平台。
直播程序源码OAuth协议:开放授权的重要性
|
安全 网络协议 网络安全
百度搜索:蓝易云【网络通信协议-FTP协议详解!】
总之,FTP 是一种用于在计算机网络上进行文件传输的协议。它提供了文件传输、目录操作和身份验证等功能。FTP 使用控制连接和数据连接来传输数据,并支持不同的工作模式和数据传输模式。然而,由于其
218 3
网络直播源码UDP协议搭建:为平台注入一份力量
在实时网络直播源码平台中,UDP协议的实时、高速的传输速度尤为重要,UDP协议的特性使其成为低延迟、高质量流媒体传输技术的理想选择,也让网络直播源码平台成为一个优质、更为用户提供更好体验的平台
网络直播源码UDP协议搭建:为平台注入一份力量
|
消息中间件 存储 小程序
直播小程序源码有用的协议知识:MQTT协议
MQTT协议能够帮助直播小程序源码平台进行可靠高效的消息传输、实时数据统计分析、实时推送订阅消息与辅助弹幕和实时评论,让直播小程序源码平台向着高质量平台方向迈进,是重要的协议之一。
直播小程序源码有用的协议知识:MQTT协议
|
UED
怎么集成短链接生成,短链接的作用
短链接也称为短网址,是指长度较短的URL链接
495 0