記一次短鏈系統設計:

遇到的問題:

  1. 發號器選擇(最終選擇爲使用樂觀鎖方式實現的數據庫發號)
  2. 數據存儲(mysql)
  3. 爲什麼不使用雪花算法發號
  4. 發號器併發測試tps不高怎麼解決
  5. mysql數據庫字段值默認不區分大小寫,導致短鏈重複

發號器選擇:

1: 雪花算法 ,
2:數據庫樂觀鎖發號(不停的更新數據庫中的一條數據來發號) 3:多個數據庫樂觀鎖發號器(相當於2的擴展)

數據存儲:

1,關係數據庫mysql存儲,數據結構上使用分庫分表和讀寫分離,使用的組件是ShardingJdbc ,分庫是通過業務去區分,不同的業務場景數據存儲到不同的庫中, 每個庫中直接使用100個表來存儲數據
分庫字段爲dbShare,路由規則爲 :database-strategy.inline.algorithm-expression = ds$->{dbShare}

分表字段爲 tableShare, 路由規則爲: table-strategy.inline.algorithm-expression = t_shorturl$->{tableShare % 100}

2,使用Hbase(因爲不熟悉,所以沒使用)

爲什麼不使用雪花算法發號:

首先我們的短鏈是一個62進制的字符串(網上的短鏈生成規則都是這個,原理是低進制轉換爲高進制長度會減短,具體請自行百度),這個字符串是通過發號器分發的一個Long類型數據轉換而來,
雪花算法的正好是生成一個Long類型數據,看起來很合適,但是,雪花算法的發號是隨着時間增長而增長的,即使把起始偏移量設置的很接近當前時間,但是很快會增長到一個很大的數字,這時轉換後的短鏈至少都會有10位長度,但是我們的要求是6爲最多,所以只能拋棄這個方案

發號器併發測試tps不高怎麼解決:

1.首先發號不能一個一個發,這樣單點發號器肯定不能抗住很高的併發,所以我們可以一段一段的發,比如每次發給一個微服務的實例1000個號,預存到內存,這樣就可以減少很多併發,不過這個方案的問題是一旦服務重啓,那麼內存中未消耗的號碼會被浪費,這時就要根據你們自己的業務來權衡這個值需要設置爲多少了.
2.即使分段發,當你有批量生成短鏈的業務時,這樣每一次請求就會消耗更多的號,如 :你每次發1000個號 ,但是批量接口每次也消耗1000個號,那麼這樣就和一次發一個好沒有區別了.
對於2的問題解決方案:
2.1 限制批量接口的生成數量,不能超過發號的個數1/10,但是這樣每次發號需要更多個,那麼浪費的可能也更大
2.2 水平擴展,發號器相當於數據表中一條數據而已(具體實現後面有介紹),那麼我們可以使用100個發號器,每個發號器發不同段的號碼,如:1發號器發出的號碼對100取餘都是1 ,2發號器發出的號碼對100取餘都是2 ,以此類推. 這樣就可以瞬間把併發能力提高100倍.這樣90%的業務場景都能抗住非常高的tps了

mysql數據庫字段值默認不區分大小寫,導致短鏈重複:

因爲mysql數據庫的值不區分大小寫,那麼發號後的短鏈gd,gD,Gd,GD在數據庫中都是同一個號(因爲測試環境我使用的3個表,所以肯定有一個數據會重複插入),但是實際應該是多個,那麼插入數據庫時就會違反了唯一索引原則,導致插入失敗.但是因爲一開始不知道這個問題,一直以爲是發號器問題,導致發號重複了,經過千辛萬苦的排出得出這麼個結論.唉…

發號器: 數據庫中字段

id: 發號器id ,
min_id: 當前發號器已經發出號碼段的最小id
max_id: 當前發號器已經發出號碼段的最大id
step: 每次發號的個數
(number_step: 發號的間隔,實際是你有多少個發號器就是多少,發號開始後就不能改變,此字段我沒有存在數據庫,而是放在發號的業務代碼中,且這個字段是跟具體的發號實現相關的,所以看個人選擇)

原理 : 樂觀鎖 ,每次發號先查數據庫,然後以發號器id 和查到的max_id(min_id)作爲條件來更新表數據,如果成功表示發號成功,失敗則發號失敗,此時你可以再嘗試重新發號即可.

數據表的字段 :

id , shortUrl ,longUrl ,dbShare ,tableShare , 其他業務字段等

分庫分表的配置 :

##所有數據源配置 共有兩個主庫master0,master1,四個從庫master0slave0,master0slave1,master1slave0,master1slave1
spring.shardingsphere.datasource.names = master0,master1,master0slave0,master1slave0
#master0 主庫0 作爲默認庫使用
spring.shardingsphere.datasource.master0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master0.driver-class-name = com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.master0.jdbc-url = jdbc:mysql://xxx:3306/short_url?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.master0.username =
spring.shardingsphere.datasource.master0.password =
#master0slave0 主庫0的從庫0
spring.shardingsphere.datasource.master0slave0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master0slave0.driver-class-name = com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.master0slave0.jdbc-url = jdbc:mysql://xxx:3306/short_url?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.master0slave0.username =
spring.shardingsphere.datasource.master0slave0.password =

#master1 主庫1 要配置分庫規則才能定位到這個庫
spring.shardingsphere.datasource.master1.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master1.driver-class-name = com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.master1.jdbc-url = jdbc:mysql://xxx:3306/short_url?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.master1.username =
spring.shardingsphere.datasource.master1.password =
#master1slave0 主庫1的從庫0
spring.shardingsphere.datasource.master1slave0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.master1slave0.driver-class-name = com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.master1slave0.jdbc-url = jdbc:mysql://xxx:3306/short_url?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.master1slave0.username =
spring.shardingsphere.datasource.master1slave0.password =

#t_shorturl表的分庫分表策略配置
spring.shardingsphere.sharding.tables.t_shorturl.actual-data-nodes = ds>0..1.tshorturl->{0..1}.t_shorturl->{0…99}
spring.shardingsphere.sharding.tables.t_shorturl.database-strategy.inline.sharding-column = dbShare
spring.shardingsphere.sharding.tables.t_shorturl.database-strategy.inline.algorithm-expression = ds>dbSharespring.shardingsphere.sharding.tables.tshorturl.tablestrategy.inline.shardingcolumn=tableSharespring.shardingsphere.sharding.tables.tshorturl.tablestrategy.inline.algorithmexpression=tshorturl->{dbShare} spring.shardingsphere.sharding.tables.t_shorturl.table-strategy.inline.sharding-column = tableShare spring.shardingsphere.sharding.tables.t_shorturl.table-strategy.inline.algorithm-expression = t_shorturl->{tableShare % 100}

#讀寫分離主從配置
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name = master0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names = master0slave0
spring.shardingsphere.sharding.master-slave-rules.ds1.master-data-source-name = master1
spring.shardingsphere.sharding.master-slave-rules.ds1.slave-data-source-names = master1slave0
#日誌打印配置
spring.shardingsphere.props.sql.show = false
#不參與分庫分表的默認庫配置 (即:master0,master0slave0 會作爲默認的庫用來讀寫)
spring.shardingsphere.sharding.default-data-source-name = ds0

發號器代碼:

import com.ec.business.shorturl.domain.NumberDTO;
import com.ec.business.shorturl.infrastructure.GeneratorMapper;
import com.ec.common.config.ShortUrlApolloConfig;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

/**
 * 標誌生成器(發號器)
 *
 * @author 020102
 */
@Repository
public class SignGenerator {

    /**  一些配置屬性  */
    @Autowired
    private ShortUrlApolloConfig shortUrlApolloConfig;

    @Autowired
    public SignGenerator(GeneratorMapper dbGenerator) {
        this.dbGenerator = dbGenerator;
    }

    /**  操作數據庫Mapper */
    private final GeneratorMapper dbGenerator;

    private NumberDTO numberDtoRange = null;

    /**
     * 發號步長,此值一旦確定就不能改變
     */
    private final Long numberStep = 100L;


    /**
     * 發號器
     *
     * @param times 重試次數
     * @return
     */
    public Long getNumber(Integer times) {
        Long number = null;
        while (times > 0 && (number = this.getNumber()) == null) {
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
            }
            times--;
        }
        return number;
    }


    /**
     * 發號器
     *
     * @return 號碼結果
     */
    private Long getNumber() {
        synchronized (SignGenerator.class) {
            if (numberDtoRange == null || numberDtoRange.getNextNumber() > numberDtoRange.getCurrent_max_id()) {
                /* 還沒發過號 或者 已使用完了則重新獲取號段 */
                numberDtoRange = getNumberRange(RandomUtils.nextInt(1, 101));
            }
            if (numberDtoRange == null) {
                /* 發號失敗 */
                return null;
            } else {
                /* 發號成功 */
                Long number = numberDtoRange.getNextNumber();
                numberDtoRange.setNextNumber(number + numberStep);
                return number;
            }
        }
    }


    /**
     * 獲取號段
     * shortUrlApolloConfig.getIncrementStep() 爲每次發號個數
     * @param id 發號器id
     * @return
     */
    private NumberDTO getNumberRange(Integer id) {
        NumberDTO byId = dbGenerator.getByGeneratorId(id);
        Long oldMax = byId.getCurrent_max_id();
        byId.setOld_current_max_id(oldMax);
        byId.setCurrent_min_id(oldMax + numberStep);
        byId.setCurrent_max_id(oldMax + shortUrlApolloConfig.getIncrementStep() * numberStep);
        byId.setNextNumber(byId.getCurrent_min_id());
        byId.setIncrement_step(shortUrlApolloConfig.getIncrementStep());
        int update = dbGenerator.update(byId);
        if (update > 0) {
            return byId;
        } else {
            return null;
        }
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章