短連結服務Octopus的實現與源碼開放

2021-01-08 紙鶴視界

前提

半年前(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東的域名

構建唯一映射關係其實就是基於一個固定的長連結,映射到一個或者多個可以動態生成的短連結,這個唯一映射關係,要求生成的短連結滿足:

不容易被破解(使用數字例如資料庫的自增主鍵作為唯一映射標識容易被人遍歷出來進行惡意調用)不能重複(一個短連結只能對應一個長連結,當然一個長連結可以對應多個短連結)長度儘可能短,這是因為第三方推送的報文內容一般有長度限制,如果短鏈過長,會導致不容易傳輸,還會令到推送內容字數受限(試想運營商簡訊投放內容最大長度為30個字符長度,短鏈已經佔了20個字符長度,剩下只有10個字符長度讓運營同事去發揮,顯然不合理)如果連結過長,生成的二維碼裡面的"碼點"會十分密集,不利於客戶端識別和傳輸,剛好筆者公司運營有使用二維碼的場景,所以必須儘可能縮短連結的長度總的來說,這個唯一映射關係中的映射標識需要像Hash算法生成的Hash碼那樣具備高唯一性和低碰撞頻率,同時具備短小易傳輸的特點,具體如何去生成映射唯一標識見下一節"壓縮碼生成算法"。

壓縮碼生成算法

這裡的"壓縮碼"(compression_code)是筆者杜撰出來的名詞,在本文中它的含義是短連結URL的路徑部分(為了節省長度,除了協議和域名部分,短鏈的URL只有第一段路徑):

其中,協議部分基本是固定為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進位數數即可。說到這裡,看起來的方案如下:

虛線部分一般依賴一種高效而且低衝突的摘要算法,如MurmurHash,而第(1)步的實線部分就是生成一個全局唯一的10進位序列,常用的手法有:

資料庫自增序列(如自增主鍵)Snowflake算法自研的類似UUID算法生成全局唯一的序列值考慮到之前筆者鑽研過Snowflake算法的原理,這裡簡單使用Snowflake算法生成自增序列,使用了下面的流程進行壓縮碼生成和分配:

因為運營部門對短鏈生成的批量不大,而且短鏈域名只有一個,所以簡單起見,一次壓縮操作直接消耗掉一個壓縮碼,不考慮不同短鏈域名對同一個壓縮碼進行共享,也不考慮壓縮碼的回收問題。

服務實現

短鏈服務的主訪問入口一般QPS極高,因此需要想盡一切辦法降低該入口的耗時,考慮可以用Redis做緩存承載入口的流量,基礎架構選型如下:

JDK1.8+:生產部署使用JDK11MVC框架與容器:spring-boot-starter-webflux或者spring-cloud-gateway,主要是必須使用Netty作為底層通訊容器內部RPC框架:Dubbo服務註冊與發現:Nacos可選APM工具:Pinpoint中間件依賴(因為之前整個服務集群都上雲了,低負載的服務共用了部分中間件):

MySQL8.xRedis5.x普通主從或者哨兵集群RabbitMQ3.8.x集群,使用鏡像隊列服務的設計圖如下:

最新的版本考慮把黑白名單的攔截器去掉,替換成一個基於布隆過濾器現實的攔截器。服務使用了兩個攔截器(雖然Filter翻譯是過濾器,但是出於習慣,下文稱為攔截器)鏈,容器提供的攔截器組成的攔截器鏈主要是負責服務安全、調用鏈跟蹤的功能,而服務內部自定義的攔截器鏈主要是實現請求參數解析、URL轉換、重定向和異步事件記錄等功能。

模塊劃分:

- (ROOT) octopus - octopus-contract - octopus-serveroctopus-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 = 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); }}總是批量生成可用的壓縮碼,查詢的時候只需要查出當前未被使用的第一個壓縮碼即可。

容器攔截器鏈實現

容器的攔截器需要實現org.springframework.web.server.WebFilter(WebFlux的Filter接口),主要有四個實現(順序如下):

MappedDiagnosticContextFilter:引入transmittable-thread-local通過MDC做TraceId的請求上下文綁定,WebFlux的線程模型和常見的Servlet容器的線程模型不一樣,這裡不能直接使用ThreadLocal或者Slf4j中原有的MDC實現BlockIpFilter:判斷客戶端請求IP是否命中黑名單AccessDomainFilter:判斷域名是否命中短鏈域名白名單(可選的,因為外部已經通過NGINX做了一次攔截,這個實現是可有可無的)ExcludeUriFilter:判斷當前請求的URI是否命中了URI黑名單這裡簡單展示一下MappedDiagnosticContextFilter的實現:

@Order(value = Integer.MIN_VALUE)@Componentpublic class MappedDiagnosticContextFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { String uuid = UUID.randomUUID().toString(); MDC.put("TRACE_ID", uuid); return chain.filter(exchange).then(Mono.fromRunnable(() -> MDC.remove("TRACE_ID"))); }}上面的TRACE_ID是配合項目的logback.xml中的pattern使用。另外需要參考https://github.com/alibaba/transmittable-thread-local/blob/master/docs/requirement-scenario.md中logback與transmittable-thread-local做集成的場景:

這裡為了方便管理和升級版本,筆者直接把logback-mdc-ttl的源碼實現改造好後放到項目中。

服務內部攔截器鏈實現

服務內部的攔截器鏈主要負責請求參數解析、URL映射轉換、重定向和訪問轉換結果記錄,頂層接口設計如下:

public interface TransformFilter { default int order() { return 1; } default void init(TransformContext context) { } void doFilter(TransformFilterChain chain, TransformContext context);}TransformContext是一個屬性承載類,本質是一個普通的JavaBean,設計如下:

目前內置了4個攔截器實現,包括:

ExtractRequestHeaderTransformFilter:請求頭解析UrlTransformFilter:URL轉換RedirectionTransformFilter:重定向處理TransformEventProcessTransformFilter:轉換事件記錄以UrlTransformFilter為例子,源碼如下:

@Slf4j@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)@Componentpublic class UrlTransformFilter implements TransformFilter { @Autowired private UrlMapCacheManager urlMapCacheManager; @Override public int order() { return 2; } @Override public void init(TransformContext context) { } @Override public void doFilter(TransformFilterChain chain, TransformContext context) { String compressionCode = context.getCompressionCode(); UrlMap urlMap = urlMapCacheManager.loadUrlMapCacheByCompressCode(compressionCode); context.setTransformStatus(TransformStatus.TRANSFORM_FAIL); if (Objects.nonNull(urlMap)) { context.setTransformStatus(TransformStatus.TRANSFORM_SUCCESS); context.setParam(TransformContext.PARAM_LONG_URL_KEY, urlMap.getLongUrl()); context.setParam(TransformContext.PARAM_SHORT_URL_KEY, urlMap.getShortUrl()); chain.doFilter(context); } else { log.warn("壓縮碼[{}]不存在或異常,終止TransformFilterChain執行,並且重定向到404頁面......", compressionCode); throw new RedirectToErrorPageException(String.format("[c:%s]", compressionCode)); } }}所有的服務內攔截器的scope都是prototype,意味著每次初始化攔截器鏈都會重新創建對應的Bean。

主控制器實現

因為octopus只做短鏈訪問的入口,後臺管理的功能交給另外的服務實現,此服務只有一個控制器,控制器裡面只有一個方法:

@RequiredArgsConstructor@RestControllerpublic class OctopusController { private final UrlMapService urlMapService; @GetMapping(path = "/{compressionCode}") @ResponseStatus(HttpStatus.FOUND) public Mono<Void> dispatch(@PathVariable(name = "compressionCode") String compressionCode, ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); TransformContext context = new TransformContext(); context.setCompressionCode(compressionCode); context.setParam(TransformContext.PARAM_SERVER_WEB_EXCHANGE_KEY, exchange); if (Objects.nonNull(request.getRemoteAddress())) { context.setParam(TransformContext.PARAM_REMOTE_HOST_NAME_KEY, request.getRemoteAddress().getHostName()); } HttpHeaders httpHeaders = request.getHeaders(); Set<String> headerNames = httpHeaders.keySet(); if (!CollectionUtils.isEmpty(headerNames)) { headerNames.forEach(headerName -> { String headerValue = httpHeaders.getFirst(headerName); context.setHeader(headerName, headerValue); }); } // 處理轉換 urlMapService.processTransform(context); // 這裡有一個技巧,flush用到的線程和內部邏輯處理的線程不是同一個線程,所以要用到TTL -- 和Servlet容器不一樣,所以目前寫的比較彆扭 return Mono.fromRunnable(context.getRedirectAction()); }}這個主控制的分發壓縮碼方法只負責封裝參數調用服務內部攔截器鏈進行後續的處理。然後添加一個全局的異常處理器,把所有的異常或者非法操作引導到一個自定義的404頁面(甚至可以在上面掛一點廣告):

Dubbo契約實現

octopus-contract是一個完全獨立的模塊,甚至可以說它是一個完全獨立的項目,主要作用是提供契約API,讓其他服務引入,讓octopus-server模塊進行實現。契約接口定義如下:

public interface OctopusApi { Response<CreateUrlMapResponse> createUrlMap(CreateUrlMapRequest request);}基於Dubbo的實現如下:

@DubboService(retries = -1)public class DefaultOctopusApi implements OctopusApi { @Autowired private UrlMapService urlMapService; @Value("${default.octopus.domain}") private String domain; @Override public Response<CreateUrlMapResponse> createUrlMap(CreateUrlMapRequest request) { UrlMap urlMap = new UrlMap(); urlMap.setUrlStatus(UrlMapStatus.AVAILABLE.getValue()); urlMap.setLongUrl(request.getLongUrl()); urlMap.setDescription(request.getDescription()); String shortUrl = urlMapService.createUrlMap(domain, urlMap); return Response.succeed(new CreateUrlMapResponse(request.getRequestId(), shortUrl)); }}生產中契約模塊做了比較多的特性定製,這裡只舉一個簡單實現的例子。

部署架構

octopus服務集群單獨部署,支持無限添加節點,部署架構的關鍵在於網絡架構,內層的負載均衡使用了Nginx,最外層的負載均衡使用了雲負載均衡,如阿里雲的SLB或者UCloud的ULB。添加或者移除短鏈域名,關鍵在於修改Nginx的配置。基本的架構如下:

只要保證負載均衡池指向octopus集群即可,短鏈的域名可能動態增刪,操作完之後只需要nginx -s -reload刷新一下Nginx的配置即可。

使用短鏈服務

先在domain_conf表寫入一條本地域名和埠的數據:

編寫一個集成測試類,創建一個短鏈映射:

@Slf4j@SpringBootTest(classes = OctopusServerApplication.class, properties = "spring.profiles.active=local")@RunWith(SpringRunner.class)public class UrlMapServiceTest { @Autowired private UrlMapService urlMapService; @Test public void createUrlMap() { String domain = "localhost:9099"; UrlMap urlMap = new UrlMap(); urlMap.setUrlStatus(UrlMapStatus.AVAILABLE.getValue()); urlMap.setLongUrl("https://throwx.cn/2020/08/24/canal-ha-cluster-guide"); urlMap.setDescription("測試短鏈"); String url = urlMapService.createUrlMap(domain, urlMap); log.info("生成的短鏈:{}", url); }}// 某次執行的結果如下:生成的短鏈:http://localhost:9099/Myt8qW基於本地配置啟動項目,然後訪問http://localhost:9099/Myt8qW,效果如下:

日誌如下:

[2020-12-27 19:29:22,285] [INFO] cn.throwx.octopus.server.application.consumer.TransformEventConsumer [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] [1c603903-e8d8-4072-aa97-6abf614b9411] - 接收到URL轉換事件,內容:{"clientIp":"192.168.211.113","compressionCode":"Myt8qW","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36","cookieValue":"Webstorm-734c3b68=9b8b3560-41f5-478a-93d0-b02128b1022f; __gads=ID=28121bd829638f67-2286c86e7fc400d3:T=1604132165:RT=1604132165:S=ALNI_MbsMQROv6swaC8kf4ux2suZm_GZXA; Hm_lvt_4df6907aebab752244c3ca1432b4ff57=1605930058,1607228133","timestamp":1609068562262,"shortUrlString":"http://localhost:9099/Myt8qW","longUrlString":"https://throwx.cn/2020/08/24/canal-ha-cluster-guide","transformStatusValue":3}......[2020-12-27 19:29:22,353] [INFO] cn.throwx.octopus.server.application.consumer.TransformEventConsumer [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] [1c603903-e8d8-4072-aa97-6abf614b9411] - 記錄URL轉換事件完成......查看轉換事件記錄表的數據:

後續功能迭代

前期方案有一個安全隱患:沒有做壓縮碼的白名單,容易被基於短鏈域名,偽造壓縮碼拼接短連結的方法進行攻擊。解決方案是在容器的攔截器鏈添加或者替換一個基於布隆過濾器實現的壓縮碼(短連結)白名單攔截器,這樣就能在前期攔截了絕大部分惡意偽造的壓縮碼,讓極少量命中了錯誤率部分的惡意壓縮碼流到後面的處理邏輯中進行判斷。另外,可以引入Caffeine配合Redis做兩級緩存,畢竟本地緩存的速度更快。

小結

octopus初版是一個4小時緊急迭代出來的一個微型項目,到現在為止更新了很多次,生產上已經基本穩定。文中描述的版本是公司生產版本的移植版,精簡了大量代碼同時移除了一些業務耦合的設計,這裡把源碼開放出來,讓一些有可能用到短鏈服務的場景提供一個可參考但儘可能不要複製的解決思路。源碼倉庫:

Gitee:https://gitee.com/throwableDoge/octopusGithub:https://github.com/zjcscut/octopus代碼都在main分支。

彩蛋

最近鴿了很長一段時間,原因是年底比較多業務功能迭代,內部的一個標籤服務重構花了大量時間。筆者一直在摸索著通過"分片"、"異步"等等思想,在時間可控的前提下,對小數據量(百萬和千萬級別)前提下,通過常用的關係型資料庫、緩存、消息隊列等非大數據平臺架構替代實現《用戶畫像方法論與工程化解決方案》裡面提到的解決方案。

標籤服務內部的代號是"千尋",取自於辛棄疾《青玉案元夕》中的"眾裡尋他千百度",項目名來自於宮崎駿的動漫《千與千尋》的女主千尋(千尋羅馬音是chihiro):

待後面項目上線一段時間穩定後,應該會抽時間寫一個系列談談怎麼不用大數據那套體系,提供用戶畫像的工程化解決方案。

個人博客

Throwable's Blog(本文完 c-10-d e-a-20201227)

相關焦點

  • 如何把長連結變成短連結?3個工具幫你實現!
    不管是文章還是視頻的連結,很多時候都是長長的一串,如果是發給別人的話,就會顯得囉裡囉嗦,佔用空間大不說,還非常難看,使用起來不方便。其實長連結也是能夠變成短連結的,下面就帶來三個工具,讓連結變得美觀又簡潔。
  • 百度連結IDM下載小工具,附帶源碼
    你的動手戳一下guan gao廢話不多說開始今天的更新內容吧百度連結IDM下載小工具V1.7 附帶源碼大家知道原IDM軟體是不支持調用的,所以需要這個單獨的調用IDM下載,軟體是易語言編寫的,把源碼也一起打包了,工具是成品的可以直接使用的,具體使用說明按照如下步驟:
  • 雲豹一對一直播app源碼搭建教程
    擁有了雲豹一對一直播app源碼,要搭建完畢才能進行下一步的上架/運營工作。那麼,該如何進行一對一直播app源碼搭建工作呢?一對一直播app源碼申請三方在獲取雲豹一對一直播app源碼後,或者在一對一直播源碼的開發過程中,我們就要開始著手準備申請各種三方帳號了。
  • 長URL連結轉短連結算法
    引言很多大型網站都加入了短連結的功能。之所以要是使用短連結,主要是因為微博只允許發140 字,如果連結地址太長的話,那麼發送的字數將大大減少。短連結的主要職責就是把原始連結很長的地址壓縮成只有6 個字母的短連結地址,當我們點擊這6 個字母的連結後,我們又可以跳轉到原始連結地址。
  • 看直播軟體源碼,如何實現直播系統業務以及技術注意點分析
    ,若在這樣快速的時代下搭建部署屬於自己的直播平臺,首先要做的就是選擇一套完整現成的直播軟體源碼。 1.如何實現終端播放 有了現成的直播軟體源碼可以實現快速的搭建,開發直播系統也會變得比較容易。單就在終端實現直播視頻播放這個關鍵點來說,在蘋果端的開發可以有現成的Video ToolBox框架提供,並且可以對是攝像頭和流媒體數據結構進行處理;在安卓端的開發過程中實現直播視頻播放有ffmpeg、mediaplayer框架。
  • 已有直播源碼,如何搭建直播平臺?
    CDN伺服器節點分布的廣泛性會直接影響視頻數據流的送達時間,距離用戶播放端越近的節點,視頻傳輸速度越短,才能實現視頻的快速播放。三方服務商三方服務不僅可以有效節省開發費用和時間,還能方便用戶的使用。用戶不再需要簡訊驗證的登錄方式,只需要將三方信息授權直播軟體即可登錄,縮短了登錄的時間,保證了用戶的留存率。
  • Google買下g.co短域名為自家產品/服務做短連結
    Google表示這個短連結服務將只用於連接Google旗下產品,也就是指向的必須是Google的網站。Google說「你可以相信它永遠只會帶你到Google的產品和服務」。 2009年Google發布了URL短連結服務goo.gl給普通用戶公開使用,非Google的產品一樣可以使用Goo.gl服務。
  • 多牛軟體:直播平臺開發源碼神秘拋出
    新功用的添加意味著直播源碼開發的技能上需求新的提高,解決新的問題。一套完好的直播體系包含採集、前處理、編碼、傳輸、解碼、渲染這幾個環節,那麼在這幾個環節搭建完成的過程中,有哪些技巧的應用能夠避免程序問題呢。首要我們需求了解一下直播體系源碼完成在線直播的原理:直播源碼開發行業發展到今天,技能相對都比較老練,設備也都支撐硬編碼,直播體系源碼完成相對簡單。
  • 通過ucd-snmp完成SNMP Agent的源碼
    所以我們一再介紹了相關的源碼的內容。那麼接下來我們主要介紹了一下相關協議的開源開發內容,並且討論採用開放源碼的ucd-snmp 4.2.1軟體包開發自己的SNMP Agent,不涉及SNMP協議包的組包、解析等問題。本文從以下部分進行介紹:一:ucd-snmp 4.2.1簡介及SNMP Agent開發步驟二:MIB庫模塊設計及代碼轉換三:SNMP Agent功能擴展方式四:uCLinux
  • 網校系統搭建服務:如何利用教育網站源碼減少機構線上轉型阻力
    作為開發商而言,不僅提供網校系統搭建服務,還要解決行業問題。比如利用教育網站源碼減少機構轉型線上的阻力。 2、產品與服務的優化升級 網校系統搭建服務的優勢,不僅僅在於教育的線上化,更在於教育資源的整合。許多傳統教育平臺在往線上轉型的過程中,只是生搬硬套線下教學內容,包括課程、教學方法等,卻忽略了網際網路數據優勢的深度挖掘。因此,開發商可以從這個角度,來解決教育機構線下轉型的阻力。
  • 短連結工具怎麼生成QQ、微信防弊的短網址?
    短連結一直是社群推廣中非常重要的環節,短連結的打開決定了推廣的效果,所以,短連結工具非常重要。做過社群運營的小夥伴都知道,維持群內活躍可以說是最日常的工作,也是最難的。而增加用戶黏度才是最根本的目的。所有產品的新品上架,打折促銷、還有一些遊戲通知等等活動,多數都是藉助社交平臺微信和QQ,但隨著社交軟體的普及,對於風控方面的管理,長長的連結很容易遭到微信的屏蔽和QQ的封殺。所以,在沒有任何的解決方法之前,運營的工作簡直是難做的很,直到出現了短連結工具。目前網際網路發展的速度之快,市場上已經出現了不少的短連結服務,而選擇一個靠譜的短連結服務就很有必要了。
  • 源碼資本宣布王巍正式加盟任合伙人_詳細解讀_最新資訊_熱點事件...
    編者按:本文來自微信公眾號「源碼資本」(ID:sourcecodecapital),作者:創造持久真實價值,36氪經授權發布。 源碼資本今日宣布,王巍先生正式加盟任合伙人。王巍先生擁有十餘年資本市場從業經驗,曾任360集團副總裁、董事會秘書。
  • 釣魚網站源碼帶木馬,歡迎進入不知道如何被騙的技術詐騙2.0時代
    一方面利用仿冒的二手交易平臺釣魚網站以低價商品吸引「魚兒」上鉤,一方面販賣含後門的釣魚網站源碼,套取他人「詐騙「果實。 近期收錄到一款仿冒二手電商平臺的釣魚網站源碼進行詐騙的案件,復現過程發現了釣魚網站的原理(盜取個人身份信息的同時騙取資金),同時還發現其源碼存在「黑吃黑「的現象。
  • 為IoT設備管理而生,Rancher全新推出開源項目Octopus
    同時,Octopus為用戶提供了Adaptor的自定義實現,用戶可以根據需求添加自己的驅動協議來管理不同類型的IoT設備。Octopus的寓意為八爪魚,它可以通過大腦和觸鬚來連接並操作身邊的物體。如同八爪魚一樣,Octopus由大腦(Brain)和觸鬚(Limb)組成,它們運行在K3s中,通過相互協作來實現管理IoT設備的功能。
  • 藍牙音樂SRC端的安卓實現
    藍牙音樂SRC側的安卓實現隨著電子產品的普及,越來越多的年輕人熱衷於使用藍牙技術來播放歌曲(相當多的手機品牌取消了耳機插孔),本篇文章就和大家聊聊藍牙音樂SRC端在安卓系統中的實現原理。安卓的audio系統播放音頻都是將數據發送到對應的輸出設備進行播放,藍牙自然也是其中一種音頻輸出設備,類似的輸出設備還有如下這些,源碼路徑:/system/media/audio/include/system/audio.h這些音頻輸出設備通過HAL框架以一個個單獨的模塊存在於安卓系統中,音頻系統服務層 audioserver
  • 深入源碼剖析componentWillXXX為什麼UNSAFE
    接下來我們來探討React如何實現異步更新,以及為什麼異步更新情況下鉤子的表現和同步更新不同。同步更新我們可以用代碼版本控制類比更新機制。在沒有代碼版本控制前,我們在代碼中逐步疊加功能。一切看起來井然有序,直到我們遇到了一個緊急線上bug(紅色節點)。為了修復這個bug,我們需要首先將之前的代碼提交。
  • 源碼時代喜迎「十歲」慶典
    成立10年來,源碼時代校區遍布成都、重慶、武漢、西安等城市,源源不斷地向社會輸送培養JAVA、UI設計、H5前端等實戰精英人才10萬餘名。2010年,源碼時代在積極促進落實國家「發展高科技,實現產業化」戰略中應運而生。
  • Reef Octopus 品牌形象設計、品牌畫冊設計及推廣設計 幕恩設計
    Reef octopus舊形象已無法滿足品牌當前發展階段,為配合企業戰略布局,幕恩與octo進行了深入的交流與溝通,最終決定將品牌升級與改造。幕恩對  reef octopus品牌形象的診斷,舊形象造型太過複雜,不夠簡潔國際化,與其專業級產品並不相符,這局限了其全球擴張策略。針對OCTO的情況,幕恩對REEF OCTOPUS進行了全新規劃與設計。
  • 北京卡券源碼優秀服務商-PHP_重慶誇米網絡科技有限公司
    北京卡券源碼優秀服務商-PHP,重慶誇米網絡科技有限公司,我們不斷的提高自己的專業性,嚴謹性專注性,不斷創新思路,保證服務的成功性,效率性,帶給您更好的服務。北京卡券源碼優秀服務商-PHP, 3 提升客單價提升客單價,有效的方式就是讓消費者復購,而不是做一次性買賣。
  • 「連結」與「超連結」知識大全
    文章中添加未發布文章的預覽連結,是不存在的,因為後臺點擊確定後會直接提示「不能輸入公眾號文章的預覽連結」原文連結原文連結是一個全面開放的連結,可以使用內鏈或者外鏈。但目前還算好的就是直接回復連結網址,回復的連結網址是激活狀態,可以直接點擊轉入。自動回復的連結也是一個全面開放的連結,可以使用永久的內鏈或者外鏈。