分庫分表 - 4:自定義主鍵、分布式主鍵



目錄:分庫分表 -Sharding-JDBC

組件 鏈接地址
准備一: 在window安裝虛擬機集群 分布式 虛擬機 linux 環境制作 GO
而且:在虛擬機上需要安裝 mysql centos mysql 筆記(內含vagrant mysql 鏡像)GO
分庫分表 -Sharding-JDBC- 從入門到精通 1 Sharding-JDBC 分庫、分表(入門實戰) GO
分庫分表 -Sharding-JDBC- 從入門到精通 2 Sharding-JDBC 基礎知識 GO
分庫分表 -Sharding-JDBC- 從入門到精通 3 MYSQL集群主從復制,原理與實戰 GO
分庫分表 Sharding-JDBC 從入門到精通之4 自定義主鍵、分布式主鍵,原理與實戰 GO
分庫分表 Sharding-JDBC 從入門到精通之5 讀寫分離,原理與實戰GO
分庫分表 Sharding-JDBC 從入門到精通之6 Sharding-JDBC執行原理 GO
分庫分表 Sharding-JDBC 從入門到精通之源碼 git

1、概述:sharding-jdbc 三種主鍵生成策略

傳統數據庫軟件開發中,主鍵自動生成技術是基本需求。而各大數據庫對於該需求也提供了相應的支持,比如MySQL的自增鍵。 對於MySQL而言,分庫分表之后,不同表生成全局唯一的Id是非常棘手的問題。因為同一個邏輯表內的不同實際表之間的自增鍵是無法互相感知的, 這樣會造成重復Id的生成。我們當然可以通過約束表生成鍵的規則來達到數據的不重復,但是這需要引入額外的運維力量來解決重復性問題,並使框架缺乏擴展性。

sharding-jdbc提供的分布式主鍵主要接口為ShardingKeyGenerator, 分布式主鍵的接口主要用於規定如何生成全局性的自增、類型獲取、屬性設置等。

sharding-jdbc提供了兩種主鍵生成策略UUID、SNOWFLAKE ,默認使用SNOWFLAKE,其對應實現類為UUIDShardingKeyGenerator和SnowflakeShardingKeyGenerator。

除了以上兩種內置的策略類,也可以基於ShardingKeyGenerator,定制主鍵生成器。

2、自定義的自增主鍵生成器

shardingJdbc 抽離出分布式主鍵生成器的接口 ShardingKeyGenerator,方便用戶自行實現自定義的自增主鍵生成器。

2.1自定義的主鍵生成器的參考代碼

package com.crazymaker.springcloud.sharding.jdbc.demo.strategy;

import lombok.Data;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;

import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;

// 單機版 AtomicLong 類型的ID生成器
@Data
public class AtomicLongShardingKeyGenerator implements ShardingKeyGenerator
{

    private AtomicLong atomicLong = new AtomicLong(0);
    private Properties properties = new Properties();

    @Override
    public Comparable<?> generateKey() {
        return atomicLong.incrementAndGet();
    }

    @Override
    public String getType() {
       
    	//聲明類型
        return "AtomicLong";
    }
}

2.2SPI接口配置

在Apache ShardingSphere中,很多功能實現類的加載方式是通過SPI注入的方式完成的。 Service Provider Interface (SPI)是一種為了被第三方實現或擴展的API,它可以用於實現框架擴展或組件替換。

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的接口,它可以用來啟用框架擴展和替換組件。 SPI 的作用就是為這些被擴展的API尋找服務實現。

SPI 實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制。

Spring中大量使用了SPI,比如:對servlet3.0規范對ServletContainerInitializer的實現、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等

Apache ShardingSphere之所以采用SPI方式進行擴展,是出於整體架構最優設計考慮。 為了讓高級用戶通過實現Apache ShardingSphere提供的相應接口,動態將用戶自定義的實現類加載其中,從而在保持Apache ShardingSphere架構完整性與功能穩定性的情況下,滿足用戶不同場景的實際需求。

添加如下文件:META-INF/services/org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator,

文件內容為:com.crazymaker.springcloud.sharding.jdbc.demo.strategy.AtomicLongShardingKeyGenerator.

#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#配置自己的 AtomicLongShardingKeyGenerator
com.crazymaker.springcloud.sharding.jdbc.demo.strategy.AtomicLongShardingKeyGenerator


#org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator
#org.apache.shardingsphere.core.strategy.keygen.UUIDShardingKeyGenerator

以上文件的原始文件,是從 sharding-core-common-4.1.0.jar 的META-INF/services 復制出來的spi配置文件。

在這里插入圖片描述

2.3使用自定義的 ID 生成器

在配置分片策略是,可以配置自定義的 ID 生成器,使用 生成器的的 type類型即可,具體的配置如下:

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        filters: com.alibaba.druid.filter.stat.StatFilter,com.alibaba.druid.wall.WallFilter,com.alibaba.druid.filter.logging.Log4j2Filter
        url: jdbc:mysql://cdh1:3306/store?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC
        password: 123456
        username: root
        maxActive: 20
        initialSize: 1
        maxWait: 60000
        minIdle: 1
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: select 'x'
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxOpenPreparedStatements: 20
        connection-properties: druid.stat.merggSql=ture;druid.stat.slowSqlMillis=5000
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        filters: com.alibaba.druid.filter.stat.StatFilter,com.alibaba.druid.wall.WallFilter,com.alibaba.druid.filter.logging.Log4j2Filter
        url: jdbc:mysql://cdh2:3306/store?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC
        password: 123456
        username: root
        maxActive: 20
        initialSize: 1
        maxWait: 60000
        minIdle: 1
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: select 'x'
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxOpenPreparedStatements: 20
        connection-properties: druid.stat.merggSql=ture;druid.stat.slowSqlMillis=5000
    sharding:
      tables:
        #邏輯表的配置很重要,直接關系到路由是否能成功
        #shardingsphere會根據sql語言類型使用對應的路由印象進行路由,而logicTable是路由的關鍵字段
        # 配置 t_order 表規則
        t_order:
          #真實數據節點,由數據源名 + 表名組成,以小數點分隔。多個表以逗號分隔,支持inline表達式
          actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
          key-generate-strategy:
            column: order_id
            key-generator-name: snowflake
          table-strategy:
            inline:
              sharding-column: order_id
              algorithm-expression: t_order_$->{order_id % 2}
          database-strategy:
            inline:
              sharding-column: user_id
              algorithm-expression: ds$->{user_id % 2}
          key-generator:
            column: order_id
            type: AtomicLong
            props:
              worker.id: 123

2.4自定義主鍵的測試

啟動應用,訪問其swagger ui界面,連接如下:

http://localhost:7700/sharding-jdbc-provider/swagger-ui.html#/sharding%20jdbc%20%E6%BC%94%E7%A4%BA/listAllUsingPOST

增加一條訂單,訂單的 user id=4,其orderid不填,讓后台自動生成,如下圖:

在這里插入圖片描述

提交訂單后,再通過swagger ui上的查詢接口, 查看全部的訂單,如下圖:

在這里插入圖片描述

在這里插入圖片描述

通過上圖可以看到, 新的訂單id為1, 不再是之前的雪花算法生成的id。

另外,通過控制台打印的日志,也可以看出所生成的id為 1, 插入訂單的日志如下

[http-nio-7700-exec-8] INFO  ShardingSphere-SQL - Actual SQL: ds0 ::: insert into t_order_1 (status, user_id, order_id) values (?, ?, ?) ::: [INSERT_TEST, 4, 1]

反復插入訂單,訂單的id會通過 AtomicLongShardingKeyGenerator 生成,從 1/2/3/4/5/6/....開始一直向后累加

3.UUID生成器

ShardingJdbc內置ID生成器實現類有UUIDShardingKeyGenerator和SnowflakeShardingKeyGenerator。依靠UUID算法自生成不重復的主鍵鍵,UUIDShardingKeyGenerator的實現很簡單,其源碼如下:

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.shardingsphere.core.strategy.keygen;

import lombok.Getter;
import lombok.Setter;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;

import java.util.Properties;
import java.util.UUID;

/**
 * UUID key generator.
 */
@Getter
@Setter
public final class UUIDShardingKeyGenerator implements ShardingKeyGenerator {
    
    private Properties properties = new Properties();
    
    @Override
    public String getType() {
        return "UUID";
    }
    
    @Override
    public synchronized Comparable<?> generateKey() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
}

由於InnoDB采用的B+Tree索引特性,UUID生成的主鍵插入性能較差, UUID常常不推薦作為主鍵。

4雪花算法

4.1雪花算法簡介

分布式id生成算法的有很多種,Twitter的SnowFlake就是其中經典的一種。

有這么一種說法,自然界中並不存在兩片完全一樣的雪花的。每一片雪花都擁有自己漂亮獨特的形狀、獨一無二。雪花算法也表示生成的ID如雪花般獨一無二。

分布式ID生成--雪花算法

1. 雪花算法概述

雪花算法生成的ID是純數字且具有時間順序的。其原始版本是scala版,后面出現了許多其他語言的版本如Java、C++等。

2. 組成結構

分布式ID生成--雪花算法

大致由:首位無效符、時間戳差值,機器(進程)編碼,序列號四部分組成。

基於Twitter Snowflake算法實現,長度為64bit;64bit組成如下:

  • 1bit sign bit.

  • 41bits timestamp offset from 2016.11.01(Sharding-JDBC distributed primary key published data) to now.

  • 10bits worker process id.

  • 12bits auto increment offset in one mills.

Bits 名字 說明
1 符號位 0,通常不使用
41 時間戳 精確到毫秒數,支持 2 ^41 /365/24/60/60/1000=69.7年
10 工作進程編號 支持 1024 個進程
12 序列號 每毫秒從 0 開始自增,支持 4096 個編號

snowflake生成的ID整體上按照時間自增排序,一共加起來剛好64位,為一個Long型(轉換成字符串后長度最多19)。並且整個分布式系統內不會產生ID碰撞(由datacenter和workerId作區分),工作效率較高,經測試snowflake每秒能夠產生26萬個ID。

3. 特點(自增、有序、適合分布式場景)

  • 時間位:可以根據時間進行排序,有助於提高查詢速度。
  • 機器id位:適用於分布式環境下對多節點的各個節點進行標識,可以具體根據節點數和部署情況設計划分機器位10位長度,如划分5位表示進程位等。
  • 序列號位:是一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號

snowflake算法可以根據項目情況以及自身需要進行一定的修改。

分布式ID生成--雪花算法

三、雪花算法的缺點

  • 強依賴時間,
  • 如果時鍾回撥,就會生成重復的ID

sharding-jdbc的分布式ID采用twitter開源的snowflake算法,不需要依賴任何第三方組件,這樣其擴展性和維護性得到最大的簡化;

但是snowflake算法的缺陷(強依賴時間,如果時鍾回撥,就會生成重復的ID),sharding-jdbc沒有給出解決方案,如果用戶想要強化,需要自行擴展;

4.2SnowflakeShardingKeyGenerator 源碼

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.shardingsphere.core.strategy.keygen;

import com.google.common.base.Preconditions;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;

import java.util.Calendar;
import java.util.Properties;

/**
 * Snowflake distributed primary key generator.
 * 
 * <p>
 * Use snowflake algorithm. Length is 64 bit.
 * </p>
 * 
 * <pre>
 * 1bit sign bit.
 * 41bits timestamp offset from 2016.11.01(ShardingSphere distributed primary key published data) to now.
 * 10bits worker process id.
 * 12bits auto increment offset in one mills
 * </pre>
 * 
 * <p>
 * Call @{@code SnowflakeShardingKeyGenerator.setWorkerId} to set worker id, default value is 0.
 * </p>
 * 
 * <p>
 * Call @{@code SnowflakeShardingKeyGenerator.setMaxTolerateTimeDifferenceMilliseconds} to set max tolerate time difference milliseconds, default value is 0.
 * </p>
 */
public final class SnowflakeShardingKeyGenerator implements ShardingKeyGenerator {
    
    public static final long EPOCH;
    
    private static final long SEQUENCE_BITS = 12L;
    
    private static final long WORKER_ID_BITS = 10L;
    
    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
    
    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
    
    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
    
    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;
    
    private static final long WORKER_ID = 0;
    
    private static final int DEFAULT_VIBRATION_VALUE = 1;
    
    private static final int MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS = 10;
    
    @Setter
    private static TimeService timeService = new TimeService();
    
    @Getter
    @Setter
    private Properties properties = new Properties();
    
    private int sequenceOffset = -1;
    
    private long sequence;
    
    private long lastMilliseconds;
    
    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2016, Calendar.NOVEMBER, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        EPOCH = calendar.getTimeInMillis();
    }
    
    @Override
    public String getType() {
        return "SNOWFLAKE";
    }
    
    @Override
    public synchronized Comparable<?> generateKey() {
        long currentMilliseconds = timeService.getCurrentMillis();
        if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
            currentMilliseconds = timeService.getCurrentMillis();
        }
        if (lastMilliseconds == currentMilliseconds) {
            if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
                currentMilliseconds = waitUntilNextTime(currentMilliseconds);
            }
        } else {
            vibrateSequenceOffset();
            sequence = sequenceOffset;
        }
        lastMilliseconds = currentMilliseconds;
        return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }
    
    @SneakyThrows
    private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
        if (lastMilliseconds <= currentMilliseconds) {
            return false;
        }
        long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
        Preconditions.checkState(timeDifferenceMilliseconds < getMaxTolerateTimeDifferenceMilliseconds(), 
                "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastMilliseconds, currentMilliseconds);
        Thread.sleep(timeDifferenceMilliseconds);
        return true;
    }
    
    //取得節點的ID
    private long getWorkerId() {
        long result = Long.valueOf(properties.getProperty("worker.id", String.valueOf(WORKER_ID)));
        Preconditions.checkArgument(result >= 0L && result < WORKER_ID_MAX_VALUE);
        return result;
    }
    
    private int getMaxVibrationOffset() {
        int result = Integer.parseInt(properties.getProperty("max.vibration.offset", String.valueOf(DEFAULT_VIBRATION_VALUE)));
        Preconditions.checkArgument(result >= 0 && result <= SEQUENCE_MASK, "Illegal max vibration offset");
        return result;
    }
    
    private int getMaxTolerateTimeDifferenceMilliseconds() {
        return Integer.valueOf(properties.getProperty("max.tolerate.time.difference.milliseconds", String.valueOf(MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS)));
    }
    
    private long waitUntilNextTime(final long lastTime) {
        long result = timeService.getCurrentMillis();
        while (result <= lastTime) {
            result = timeService.getCurrentMillis();
        }
        return result;
    }
    
    private void vibrateSequenceOffset() {
        sequenceOffset = sequenceOffset >= getMaxVibrationOffset() ? 0 : sequenceOffset + 1;
    }
}


EPOCH = calendar.getTimeInMillis(); 計算 2016/11/01 零點開始的毫秒數。

generateKey() 實現邏輯

校驗當前時間小於等於最后生成編號時間戳,避免服務器時鍾同步,可能產生時間回退,導致產生重復編號
獲得序列號。當前時間戳可獲得自增量到達最大值時,調用 #waitUntilNextTime() 獲得下一毫秒
設置最后生成編號時間戳,用於校驗時間回退情況
位操作生成編號

根據代碼可以得出,如果一個毫秒內只產生一個id,那么12位序列號全是0,所以這種情況生成的id全是偶數。

4.3workerId(節點)的配置問題?

問題:Snowflake 算法需要保障每個分布式節點,有唯一的workerId(節點),怎么解決工作進程編號分配?

Twitter Snowflake 算法實現上是相對簡單易懂的,較為麻煩的是怎么解決工作進程編號的分配? 怎么保證全局唯一?

解決方案:
可以通過IP、主機名稱等信息,生成workerId(節點Id)。還可以通過 Zookeeper、Consul、Etcd 等提供分布式配置功能的中間件。

由於ShardingJdbc的雪花算法,不是那么的完成。比較簡單粗暴的解決策略為:

  • 在生產項目中,可以基於 百度的非常成熟、高性能的雪花ID庫,實現一個自定義的ID生成器。

  • 在學習項目中,可以基於瘋狂創客圈的學習類型雪花ID庫,實現一個自定義的ID生成器。

參考文獻:

http://shardingsphere.io/document/current/cn/overview/

https://blog.csdn.net/tianyaleixiaowu/article/details/70242971

https://blog.csdn.net/clypm/article/details/54378502

https://blog.csdn.net/u011116672/article/details/78374724

https://blog.csdn.net/feelwing1314/article/details/80237178

高並發開發環境系列:springcloud環境

組件 鏈接地址
windows centos 虛擬機 安裝&排坑 vagrant+java+springcloud+redis+zookeeper鏡像下載(&制作詳解))
centos mysql 安裝&排坑 centos mysql 筆記(內含vagrant mysql 鏡像)
linux kafka安裝&排坑 kafka springboot (或 springcloud ) 整合
Linux openresty 安裝 Linux openresty 安裝
【必須】Linux Redis 安裝(帶視頻) Linux Redis 安裝(帶視頻)
【必須】Linux Zookeeper 安裝(帶視頻) Linux Zookeeper 安裝, 帶視頻
Windows Redis 安裝(帶視頻) Windows Redis 安裝(帶視頻)
RabbitMQ 離線安裝(帶視頻) RabbitMQ 離線安裝(帶視頻)
ElasticSearch 安裝, 帶視頻 ElasticSearch 安裝, 帶視頻
Nacos 安裝(帶視頻) Nacos 安裝(帶視頻)
【必須】Eureka Eureka 入門,帶視頻
【必須】springcloud Config 入門,帶視頻 springcloud Config 入門,帶視頻
【必須】SpringCloud 腳手架打包與啟動 SpringCloud腳手架打包與啟動
Linux 自啟動 假死自啟動 定時自啟 Linux 自啟動 假死啟動

回到◀瘋狂創客圈

瘋狂創客圈 - Java高並發研習社群,為大家開啟大廠之門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM