短链接服务Octopus的实现与源码开放(上)
半年前 2020-06 左右 疫情触底反弹 公司的业务量不断提升 运营部门为了方便短信、模板消息推送等渠道的投放 提出了一个把长链接压缩为短链接的功能需求。当时为了快速推广 使用了一些比较知名的第三方短链压缩平台 存在一些问题
收费贵一些情况下 短链域名在部分第三方平台例如微信会被封杀回源数据没有办法定制处理方案 无法打通整个业务链路进行数据分析和跟踪基于此类问题 决定自研一个 长链接压缩为 短链接服务 当时刚好同步进行微服务拆分 内部很多微服务需要重新命名 组内的一个妹子说不如就用Github的吉祥物去命名octopus cat 章鱼猫 去命名 但是考虑到版权问题 去掉了她最喜欢的猫 剩下章鱼 以octopus命名
项目的描述还打错字了 应该是 短链接 因为实现的功能并不复杂 初版于2020-06月底就发布。octopus的实现参考了互联网中几篇关于 短链服务实现 浏览量比较高的文章 下面从实现原理、服务实现和部署架构等方面展开谈谈。
短链服务的核心就是构建短链接和长链接的唯一映射关系 依赖到一个高性能、排列组合数量大而且破解难度大的映射标识生成算法。
上图是笔者收到的京东白条分期还款结果提醒短信 短信内容也包含了一个短链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的登录页 登录后再跳进一个白条攻略页面。这里其实一个长链其实可以压成多个短链 短链可以相同域名 也可以使用不同的域名
访问https://3.cn/j/xxxxxxx短链接具体的交互流程猜测如下
jrmkt.jd.com和3.cn查证都是doge东的域名
构建唯一映射关系其实就是基于一个固定的长链接 映射到一个或者多个可以动态生成的短链接 这个唯一映射关系 要求生成的短链接满足
总的来说 这个唯一映射关系中的映射标识需要像Hash算法生成的Hash码那样具备高唯一性和低碰撞频率 同时具备短小易传输的特点 具体如何去生成映射唯一标识见下一节 压缩码生成算法 。
这里的 压缩码 compression_code 是笔者杜撰出来的名词 在本文中它的含义是短链接URL的路径部分 为了节省长度 除了协议和域名部分 短链的URL只有第一段路径
其中 协议部分基本是固定为https:// 从安全性来看不建议使用http:// 短链域名可以购买尽可能长度短的域名如t.cn 不过有先见之明的资本家一般会把所有优质的短域名买下并且把价格提到很高 所以域名的长度基本也是很难控制的因素 剩下可控的就是压缩码部分。压缩码部分是可控的 但因为它是URL的一部分 只要确保所使用的字符不会被URL编码转义 那么长度是人为可控的。假设我们使用的是26个字母的大小写 加上10个数字 那么对于N位压缩码可以表示的最大组合数量为
一般来说 组合数越小破解的难度就越小 组合数越大 要求压缩码长度越大 所以常用的长度就是4、5和6 而且后期可以对失效的长链进行压缩码回收或者禁用 这三个长度对于绝大对数生产短链的应用场景都能满足。octopus在实现的时候选用的是6位长度的压缩码 无他 因为有现成的成熟的参考方案 62进制数刚好由字符0-9 a-z A-Z组成 生成压缩码的时候 只需要生成一个唯一的10进制数 然后再基于此10进制数转换为62进制数数即可。说到这里 看起来的方案如下
虚线部分一般依赖一种高效而且低冲突的摘要算法 如MurmurHash 而第(1)步的实线部分就是生成一个全局唯一的10进制序列 常用的手法有
考虑到之前笔者钻研过Snowflake算法的原理 这里简单使用Snowflake算法生成自增序列 使用了下面的流程进行压缩码生成和分配
因为运营部门对短链生成的批量不大 而且短链域名只有一个 所以简单起见 一次压缩操作直接消耗掉一个压缩码 不考虑不同短链域名对同一个压缩码进行共享 也不考虑压缩码的回收问题。
短链服务的主访问入口一般QPS极高 因此需要想尽一切办法降低该入口的耗时 考虑可以用Redis做缓存承载入口的流量 基础架构选型如下
中间件依赖 因为之前整个服务集群都上云了 低负载的服务共用了部分中间件
MySQL8.xRedis5.x普通主从或者哨兵集群RabbitMQ3.8.x集群 使用镜像队列服务的设计图如下
最新的版本考虑把黑白名单的拦截器去掉 替换成一个基于布隆过滤器现实的拦截器。服务使用了两个拦截器 虽然Filter翻译是过滤器 但是出于习惯 下文称为拦截器 链 容器提供的拦截器组成的拦截器链主要是负责服务安全、调用链跟踪的功能 而服务内部自定义的拦截器链主要是实现请求参数解析、URL转换、重定向和异步事件记录等功能。
模块划分
- (ROOT) octopus - octopus-contract - octopus-server 复制代码
octopus-contract模块必须脱离父POM的管理 方便单独迭代更新。
一共使用了5个表
具体的初始化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)左右,疫情触底反弹,公司的业务量不断提升,运营部门为了方便短信、模板消息推送等渠道的投放,提出了一个把长链接压缩为短链接的功能需求。当时为了快速推广,使用了一些比较知名的第三方短链压缩平台,存在一些问题
如何实现一个短链接服务? 家经常会收到一些莫名的营销短信,里面有一个非常短的链接让你跳转。新浪微博因为限制字数,所以也会经常见到这种看着不像网址的网址。短链的兴起应该就是微博限制字数激起了大家的创造力。