為解決關系型數據庫面對海量數據由於數據量過大而導致的性能問題時,將數據進行分片是行之有效的解決方案,而將集中於單一節點的數據拆分並分別存儲到多個數據庫或表,稱為分庫分表。
分庫可以有效分散高並發量,分表雖然無法緩解並發量,但僅跨表仍然可以使用數據庫原生的ACID事務。而一旦跨庫,涉及到事務的問題就會變得無比復雜。
1.使用
pom.xml添加依賴:
<dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>3.0.0.M1</version> </dependency>
基於Spring Boot的規則配置:
sharding.jdbc.datasource.names=ds_0,ds_1 sharding.jdbc.datasource.ds_0.type=org.apache.commons.dbcp.BasicDataSource sharding.jdbc.datasource.ds_0.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds_0.url=jdbc:mysql://localhost:3306/demo_ds_0 sharding.jdbc.datasource.ds_0.username=root sharding.jdbc.datasource.ds_0.password=hongda$123456 sharding.jdbc.datasource.ds_1.type=org.apache.commons.dbcp.BasicDataSource sharding.jdbc.datasource.ds_1.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds_1.url=jdbc:mysql://localhost:3306/demo_ds_1 sharding.jdbc.datasource.ds_1.username=root sharding.jdbc.datasource.ds_1.password=hongda$123456 sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds_$->{user_id % 2} sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds_$->{0..1}.t_order_$->{0..1} sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{order_id % 2} sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=order_id sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds_$->{0..1}.t_order_item_$->{0..1} sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.algorithm-expression=t_order_item_$->{order_id % 2} sharding.jdbc.config.sharding.tables.t_order_item.key-generator-column-name=order_item_id
使用上來說的話,這樣就可以了
上面的配置是根據user_id取模分庫, 且根據order_id取模分表的兩庫兩表的配置
總的來說,Sharding-jdbc是個非常輕量級的框架,使用並不困難。
其余框架的使用,可以查看官方提供的sharding-sphere-example測試案例
2.Sharding-jdbc分布式id:
在使用官方提供的測試案例使用時,發現一個問題,就是Sharding-jdbc生成的分布式id全是偶數
sharding-jdbc的分布式ID采用twitter開源的snowflake算法,不需要依賴任何第三方組件,這樣其擴展性和維護性得到最大的簡化;
但是snowflake算法的缺陷(強依賴時間,如果時鍾回撥,就會生成重復的ID),sharding-jdbc沒有給出解決方案,如果用戶想要強化,需要自行擴展;
1 符號位 等於 0 41 時間戳 從 2016/11/01 零點開始的毫秒數,支持 2 ^41 /365/24/60/60/1000=69.7年 10 工作進程編號 支持 1024 個進程 12 序列號 每毫秒從 0 開始自增,支持 4096 個編號
Sharding-jdbc分布式id核心源碼:
public final class DefaultKeyGenerator implements KeyGenerator { public static final long EPOCH; // 自增長序列的長度(單位是位時的長度) private static final long SEQUENCE_BITS = 12L; // workerId的長度(單位是位時的長度) 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; // 位運算計算workerId的最大值(workerId占10位,那么1向左移10位就是workerId的最大值) private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS; @Setter private static TimeService timeService = new TimeService(); private static long workerId; // EPOCH就是起始時間,從2016-11-01 00:00:00開始的毫秒數 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(); } private long sequence; private long lastTime; /** * 得到分布式唯一ID需要先設置workerId,workId的值范圍[0, 1024) * @param workerId work process id */ public static void setWorkerId(final long workerId) { // google-guava提供的入參檢查方法:workerId只能在0~WORKER_ID_MAX_VALUE之間; Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE); DefaultKeyGenerator.workerId = workerId; } /** * 調用該方法,得到分布式唯一ID * @return key type is @{@link Long}. */ @Override public synchronized Number generateKey() { long currentMillis = timeService.getCurrentMillis(); // 每次取分布式唯一ID的時間不能少於上一次取時的時間 Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis); // 如果同一毫秒范圍內,那么自增,否則從0開始 if (lastTime == currentMillis) { // 如果自增后的sequence值超過4096,那么等待直到下一個毫秒 if (0L == (sequence = ++sequence & SEQUENCE_MASK)) { currentMillis = waitUntilNextTime(currentMillis); } } else { sequence = 0; } // 更新lastTime的值,即最后一次獲取分布式唯一ID的時間 lastTime = currentMillis; // 從這里可知分布式唯一ID的組成部分; return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence; } // 獲取下一毫秒的方法:死循環獲取當前毫秒與lastTime比較,直到大於lastTime的值; private long waitUntilNextTime(final long lastTime) { long time = timeService.getCurrentMillis(); while (time <= lastTime) { time = timeService.getCurrentMillis(); } return time; } }
EPOCH = calendar.getTimeInMillis(); 計算 2016/11/01 零點開始的毫秒數。
#generateKey() 實現邏輯
校驗當前時間小於等於最后生成編號時間戳,避免服務器時鍾同步,可能產生時間回退,導致產生重復編號
獲得序列號。當前時間戳可獲得自增量到達最大值時,調用 #waitUntilNextTime() 獲得下一毫秒
設置最后生成編號時間戳,用於校驗時間回退情況
位操作生成編號
根據代碼可以得出,如果一個毫秒內只產生一個id,那么12位序列號全是0,所以這種情況生成的id全是偶數。
3.怎么解決工作進程編號分配?
Twitter Snowflake 算法實現上是相對簡單易懂的,較為麻煩的是怎么解決工作進程編號的分配?
超過 1024 個怎么辦? 怎么保證全局唯一?
第一個問題,將分布式主鍵生成獨立成一個發號器服務,提供生成分布式編號的功能。
第二個問題,通過 Zookeeper、Consul、Etcd 等提供分布式配置功能的中間件。當然 Sharding-JDBC 也提供了不依賴這些服務的方式,我們一個一個往下看。
獲取workerId的三種方式
sharding-jdbc的sharding-jdbc-plugin模塊中,提供了三種方式獲取workerId的方式,並提供接口獲取分布式唯一ID的方法–generateKey(),接下來對各種方式如何生成workerId進行分析;
HostNameKeyGenerator
根據hostname獲取,源碼如下(HostNameKeyGenerator.java):
/** * 根據機器名最后的數字編號獲取工作進程Id.如果線上機器命名有統一規范,建議使用此種方式. * 例如機器的HostName為:dangdang-db-sharding-dev-01(公司名-部門名-服務名-環境名-編號) * ,會截取HostName最后的編號01作為workerId. * * @author DonneyYoung **/ static void initWorkerId() { InetAddress address; Long workerId; try { address = InetAddress.getLocalHost(); } catch (final UnknownHostException e) { throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!"); } // 先得到服務器的hostname,例如JTCRTVDRA44,linux上可通過命令"cat /proc/sys/kernel/hostname"查看; String hostName = address.getHostName(); try { // 計算workerId的方式: // 第一步hostName.replaceAll("\\d+$", ""),即去掉hostname后純數字部分,例如JTCRTVDRA44去掉后就是JTCRTVDRA // 第二步hostName.replace(第一步的結果, ""),即將原hostname的非數字部分去掉,得到純數字部分,就是workerId workerId = Long.valueOf(hostName.replace(hostName.replaceAll("\\d+$", ""), "")); } catch (final NumberFormatException e) { throw new IllegalArgumentException(String.format("Wrong hostname:%s, hostname must be end with number!", hostName)); } DefaultKeyGenerator.setWorkerId(workerId); }
IPKeyGenerator
根據ip獲取:
/** * 根據機器IP獲取工作進程Id,如果線上機器的IP二進制表示的最后10位不重復,建議使用此種方式 * ,列如機器的IP為192.168.1.108,二進制表示:11000000 10101000 00000001 01101100 * ,截取最后10位 01 01101100,轉為十進制364,設置workerId為364. */ static void initWorkerId() { InetAddress address; try { // 首先得到IP地址,例如192.168.1.108 address = InetAddress.getLocalHost(); } catch (final UnknownHostException e) { throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!"); } // IP地址byte[]數組形式,這個byte數組的長度是4,數組0~3下標對應的值分別是192,168,1,108 byte[] ipAddressByteArray = address.getAddress(); // 由這里計算workerId源碼可知,workId由兩部分組成: // 第一部分(ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE:ipAddressByteArray[ipAddressByteArray.length - 2]即取byte[]倒數第二個值,即1,然后&0B11,即只取最后2位(IP段倒數第二個段取2位,IP段最后一位取全部8位,總計10位),然后左移Byte.SIZE,即左移8位(因為這一部分取得的是IP段中倒數第二個段的值); // 第二部分(ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF):ipAddressByteArray[ipAddressByteArray.length - 1]即取byte[]最后一位,即108,然后&0xFF,即通過位運算將byte轉為int; // 最后將第一部分得到的值加上第二部分得到的值就是最終的workId DefaultKeyGenerator.setWorkerId((long) (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF))); }
IPSectionKeyGenerator
根據 ip段獲取:
/** * 瀏覽 {@link IPKeyGenerator} workerId生成的規則后,感覺對服務器IP后10位(特別是IPV6)數值比較約束. * * <p> * 有以下優化思路: * 因為workerId最大限制是2^10,我們生成的workerId只要滿足小於最大workerId即可。 * 1.針對IPV4: * ....IP最大 255.255.255.255。而(255+255+255+255) < 1024。 * ....因此采用IP段數值相加即可生成唯一的workerId,不受IP位限制。 * 2.針對IPV6: * ....IP最大ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff * ....為了保證相加生成出的workerId < 1024,思路是將每個bit位的后6位相加。這樣在一定程度上也可以滿足workerId不重復的問題。 * </p> * 使用這種IP生成workerId的方法,必須保證IP段相加不能重復 * * @author DogFc */ static void initWorkerId() { InetAddress address; try { address = InetAddress.getLocalHost(); } catch (final UnknownHostException e) { throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!"); } // 得到IP地址的byte[]形式值 byte[] ipAddressByteArray = address.getAddress(); long workerId = 0L; //如果是IPV4,計算方式是遍歷byte[],然后把每個IP段數值相加得到的結果就是workerId if (ipAddressByteArray.length == 4) { for (byte byteNum : ipAddressByteArray) { workerId += byteNum & 0xFF; } //如果是IPV6,計算方式是遍歷byte[],然后把每個IP段后6位(& 0B111111 就是得到后6位)數值相加得到的結果就是workerId } else if (ipAddressByteArray.length == 16) { for (byte byteNum : ipAddressByteArray) { workerId += byteNum & 0B111111; } } else { throw new IllegalStateException("Bad LocalHost InetAddress, please check your network!"); } DefaultKeyGenerator.setWorkerId(workerId); }
參考:
http://shardingsphere.io/document/current/cn/overview/
https://blog.csdn.net/tianyaleixiaowu/article/details/70242971
https://blog.csdn.net/clypm/article/details/54378502