zl程序教程

您现在的位置是:首页 >  前端

当前栏目

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

链接源码服务 实现 开放
2023-09-27 14:25:57 时间
前提


半年前 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_336 147万接近148万N 5 组合数为62 ^ 5 916_132_832 9.16亿左右N 6 组合数为62 ^ 6 56_800_235_584 568亿左右


一般来说 组合数越小破解的难度就越小 组合数越大 要求压缩码长度越大 所以常用的长度就是4、5和6 而且后期可以对失效的长链进行压缩码回收或者禁用 这三个长度对于绝大对数生产短链的应用场景都能满足。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 生产部署使用JDK11MVC框架与容器 spring-boot-starter-webflux或者spring-cloud-gateway 主要是必须使用Netty作为底层通讯容器内部RPC框架 Dubbo服务注册与发现 Nacos可选APM工具 Pinpoint

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

MySQL8.xRedis5.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 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);

复制代码


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



短链接服务Octopus的实现与源码开放(下) 半年前(2020-06)左右,疫情触底反弹,公司的业务量不断提升,运营部门为了方便短信、模板消息推送等渠道的投放,提出了一个把长链接压缩为短链接的功能需求。当时为了快速推广,使用了一些比较知名的第三方短链压缩平台,存在一些问题
如何实现一个短链接服务? 家经常会收到一些莫名的营销短信,里面有一个非常短的链接让你跳转。新浪微博因为限制字数,所以也会经常见到这种看着不像网址的网址。短链的兴起应该就是微博限制字数激起了大家的创造力。