主頁 > 軟體設計 > 短鏈接服務Octopus的實作與原始碼開放

短鏈接服務Octopus的實作與原始碼開放

2020-12-29 14:10:28 軟體設計

前提

半年前(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_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進制數數即可,說到這里,看起來的方案如下:

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

  • 資料庫自增序列(如自增主鍵)

  • Snowflake演算法

  • 自研的類似UUID演算法生成全域唯一的序列值

考慮到之前筆者鉆研過Snowflake演算法的原理,這里簡單使用Snowflake演算法生成自增序列,使用了下面的流程進行壓縮碼生成和分配:

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

服務實作

短鏈服務的主訪問入口一般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集群,使用鏡像佇列

服務的設計圖如下:

最新的版本考慮把黑白名單的攔截器去掉,「替換成一個基于布隆過濾器現實的攔截器」,服務使用了兩個攔截器(雖然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.WebFilterWebFluxFilter介面),主要有四個實作(順序如下):

  • MappedDiagnosticContextFilter:引入transmittable-thread-local通過MDCTraceId的請求背景關系系結,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.mdlogbacktransmittable-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:請求頭決議

  • UrlTransformFilterURL轉換

  • 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或者UCloudULB,添加或者移除短鏈域名,關鍵在于修改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小時緊急迭代出來的一個微型專案,到現在為止更新了很多次,生產上已經基本穩定,文中描述的版本是公司生產版本的移植版,精簡了大量代碼同時移除了一些業務耦合的設計,這里把原始碼開放出來,讓一些有可能用到短鏈服務的場景提供一個可參考但盡可能不要復制的解決思路,原始碼倉庫:

  • Giteehttps://gitee.com/throwableDoge/octopus

  • Githubhttps://github.com/zjcscut/octopus

代碼都在main分支,

彩蛋

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

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

待后面專案上線一段時間穩定后,應該會抽時間寫一個系列談談怎么不用大資料那套體系,提供用戶畫像的工程化解決方案,

(本文完 c-10-d e-a-20201227 封面來自于動漫《刀劍神域》)

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/241991.html

標籤:其他

上一篇:springboot創建mymes

下一篇:ARFoundation系列講解-05打包iOS應用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more