前提
半年前(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-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 = 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)
@Component
public 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)
@Component
public 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
@RestController
public 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):
待后面專案上線一段時間穩定后,應該會抽時間寫一個系列談談怎么不用大資料那套體系,提供用戶畫像的工程化解決方案,
(本文完 c-10-d e-a-20201227 封面來自于動漫《刀劍神域》)
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/241991.html
標籤:其他
