短链接服务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月前
|
搜索推荐 数据挖掘 数据管理
短链接系统精选:打造高效网络分享体验
在互联网时代,短链接系统扮演着重要角色,将长网址转化为简洁、易记的字符串。本文介绍了四款知名服务:行业标准的Bitly,提供详细统计和定制功能;简洁的TinyURL,操作简便;品牌化的Rebrandly,支持自定义域名以增强营销效果;以及DZ_tech/ShortURL,提供轻量级的私有部署方案。选择合适的短链接服务能优化用户体验,助力数据分析和营销。
|
5月前
|
安全 API 开发工具
微信开发:API接口与ipad协议的深度比较及最佳选择
微信开发:API接口与ipad协议的深度比较及最佳选择
|
6月前
|
安全 Java Linux
如何实现无公网IP及服务器实现公网环境企业微信网页应用开发调试
如何实现无公网IP及服务器实现公网环境企业微信网页应用开发调试
109 2
|
6月前
|
NoSQL Redis Docker
揭秘Github火爆的开源IP代理池秘密!
爬虫新利器:揭秘Github火爆的开源IP代理池秘密!
346 0
|
6月前
|
缓存 人工智能 API
【Python+微信】【企业微信开发入坑指北】2. 如何利用企业微信API主动给用户发应用消息
【Python+微信】【企业微信开发入坑指北】2. 如何利用企业微信API主动给用户发应用消息
233 0
|
搜索推荐 数据安全/隐私保护
直播程序源码OAuth协议:开放授权的重要性
在直播程序源码平台,需要OAuth协议这样的协议,OAuth协议保证了用户在使用直播程源码平台结合第三方应用程序的技术功能时的安全性与方便性,也为直播程序源码平台的用户提供了许多互动功能,是让直播程源码平台成为更高质量、更好的平台。
直播程序源码OAuth协议:开放授权的重要性
直播网站源码社区功能部署开发:连接世界的互动形式!
直播网站源码社区功能如何去实现from flask import Flask, request app = Flask(__name__) posts = [] @app.route('/post', methods=['POST'])
直播网站源码社区功能部署开发:连接世界的互动形式!
|
小程序 安全 定位技术
微信小程序学习实录4(开发前准备、认证必备资料、公众号关联小程序、小程序发布、开发配置、服务器域名、业务域名、位置接口设置)
微信小程序学习实录4(开发前准备、认证必备资料、公众号关联小程序、小程序发布、开发配置、服务器域名、业务域名、位置接口设置)
333 0
|
UED
怎么集成短链接生成,短链接的作用
短链接也称为短网址,是指长度较短的URL链接
485 0