-
狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高並發實戰》 面試必備 + 面試必備 + 面試必備 【博客園總入口 】
-
瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高並發核心編程》 大廠必備 + 大廠必備 + 大廠必備 【博客園總入口 】
-
入大廠+漲工資必備: 高並發【 億級流量IM實戰】 實戰系列 【 SpringCloud Nginx秒殺】 實戰系列 【博客園總入口 】
目錄:分庫分表 -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如雪花般獨一無二。
1. 雪花算法概述
雪花算法生成的ID是純數字且具有時間順序的。其原始版本是scala版,后面出現了許多其他語言的版本如Java、C++等。
2. 組成結構
大致由:首位無效符、時間戳差值,機器(進程)編碼,序列號四部分組成。
基於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
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高並發研習社群,為大家開啟大廠之門