ShardingJdbc
一、ShardingJdbc的概述
1、概述
官網:http://shardingsphere.apache.org/index_zh.html
下載地址:https://shardingsphere.apache.org/document/current/cn/downloads/
快速入門:https://shardingsphere.apache.org/document/current/cn/quick-start/shardingsphere-jdbc-quick-start/
以下來自官網的原話:
Apache ShardingSphere 是一套開源的分布式數據庫解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規划中)這 3 款既能夠獨立部署,又支持混合部署配合使用的產品組成。 它們均提供標准化的數據水平擴展、分布式事務和分布式治理等功能,可適用於如 Java 同構、異構語言、雲原生等各種多樣化的應用場景。
Apache ShardingSphere 旨在充分合理地在分布式的場景下利用關系型數據庫的計算和存儲能力,而並非實現一個全新的關系型數據庫。 關系型數據庫當今依然占有巨大市場份額,是企業核心系統的基石,未來也難於撼動,我們更加注重在原有基礎上提供增量,而非顛覆。
Apache ShardingSphere 5.x 版本開始致力於可插拔架構,項目的功能組件能夠靈活的以可插拔的方式進行擴展。 目前,數據分片、讀寫分離、數據加密、影子庫壓測等功能,以及 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 與協議的支持,均通過插件的方式織入項目。 開發者能夠像使用積木一樣定制屬於自己的獨特系統。Apache ShardingSphere 目前已提供數十個 SPI 作為系統的擴展點,仍在不斷增加中。
ShardingSphere 已於2020年4月16日成為 Apache 軟件基金會的頂級項目。
2、關於改名問題
在3.0以后就更改成了ShardingSphere。
3、認識shardingjdbc
定位為輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅動,完全兼容 JDBC 和各種 ORM 框架。
適用於任何基於 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
支持任何第三方的數據庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
支持任意實現 JDBC 規范的數據庫,目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 標准的數據庫。
4、認識shardingjdbc功能架構圖
5、認識Sharding-Proxy
- 向應用程序完全透明,可直接當做 MySQL/PostgreSQL 使用。
- 適用於任何兼容 MySQL/PostgreSQL 協議的的客戶端。
6、三個組件的比較
7、ShardingJdbc混合架構
ShardingSphere-JDBC 采用無中心化架構,適用於 Java 開發的高性能的輕量級 OLTP(連接事務處理) 應用;ShardingSphere-Proxy 提供靜態入口以及異構語言的支持,適用於 OLAP(連接數據分析) 應用以及對分片數據庫進行管理和運維的場景。
Apache ShardingSphere 是多接入端共同組成的生態圈。 通過混合使用 ShardingSphere-JDBC 和 ShardingSphere-Proxy,並采用同一注冊中心統一配置分片策略,能夠靈活的搭建適用於各種場景的應用系統,使得架構師更加自由地調整適合與當前業務的最佳系統架構。
8、ShardingShpere的功能清單
功能列表
數據分片
分庫 & 分表
讀寫分離
分片策略定制化
無中心化分布式主鍵
分布式事務
標准化事務接口
XA 強一致事務
柔性事務
數據庫治理
分布式治理
彈性伸縮
可視化鏈路追蹤
數據加密
9、 ShardingSphere數據分片內核剖析
ShardingSphere 的 3 個產品的數據分片主要流程是完全一致的。 核心由 SQL 解析 => 執行器優化 => SQL 路由 => SQL 改寫 => SQL 執行 => 結果歸並的流程組成。
SQL 解析
分為詞法解析和語法解析。 先通過詞法解析器將 SQL 拆分為一個個不可再分的單詞。再使用語法解析器對 SQL 進行理解,並最終提煉出解析上下文。 解析上下文包括表、選擇項、排序項、分組項、聚合函數、分頁信息、查詢條件以及可能需要修改的占位符的標記。
執行器優化
合並和優化分片條件,如 OR 等。
SQL 路由
根據解析上下文匹配用戶配置的分片策略,並生成路由路徑。目前支持分片路由和廣播路由。
SQL 改寫
將 SQL 改寫為在真實數據庫中可以正確執行的語句。SQL 改寫分為正確性改寫和優化改寫。
SQL 執行
通過多線程執行器異步執行。
結果歸並
將多個執行結果集歸並以便於通過統一的 JDBC 接口輸出。結果歸並包括流式歸並、內存歸並和使用裝飾者模式的追加歸並這幾種方式。
二、ShardingJdbc准備 - Linux安裝MySQL5.7
1、yum安裝mysql
下載mysql的rpm地址
http://repo.mysql.com/yum/mysql-5.7-community/el/7/x86_64/
配置Mysql擴展源
rpm -ivh http://repo.mysql.com/yum/mysql-5.7-community/el/7/x86_64/mysql57-community-release-el7-10.noarch.rpm
yum安裝mysql
yum install mysql-community-server -y
啟動Mysql,並加入開機自啟
systemctl start mysqld
systemctl stop mysqld
systemctl enable mysqld
使用Mysq初始密碼登錄數據庫
>grep "password" /var/log/mysqld.log
> mysql -uroot -pma1S8xjuEA/F
或者一步到位的做法如下
>mysql -uroot -p$(awk '/temporary password/{print $NF}' /var/log/mysqld.log)
修改數據庫密碼
數據庫默認密碼規則必須攜帶大小寫字母、特殊符號,字符長度大於8否則會報錯。
因此設定較為簡單的密碼時需要首先修改set global validate_password_policy和_length參數值。
mysql> set global validate_password_policy=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set global validate_password_length=1;
Query OK, 0 rows affected (0.00 sec)
修改密碼
mysql> set password for root@localhost = password('mkxiaoer');
Query OK, 0 rows affected, 1 warning (0.00 sec)
或者
mysql>ALTER USER 'root'@'localhost' IDENTIFIED BY 'new password';
登錄測試
[root@http-server ~]# mysql -uroot -pmkxiaoer
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)
mysql> exit
可視化工具的登錄授權:(如果授權不成功,請查看防火牆)
操作完成上面的,現在還不能用可視化的客戶端進行連接,需要我們進行授權:
mysql > grant all on *.* to root@'%' identified by '數據庫密碼';
mysql > flush privileges;
操作完畢,接下來可以使用navicat或者sqlylog進行遠程連接了.
sqlylog的下載:https://sqlyog.en.softonic.com/
三、ShardingJdbc准備 - MySql完成主從復制
概述
主從復制(也稱 AB 復制)允許將來自一個MySQL數據庫服務器(主服務器)的數據復制到一個或多個MySQL數據庫服務器(從服務器)。
復制是異步的 從站不需要永久連接以接收來自主站的更新。
根據配置,您可以復制數據庫中的所有數據庫,所選數據庫甚至選定的表。
1、MySQL中復制的優點包括:
橫向擴展解決方案 - 在多個從站之間分配負載以提高性能。在此環境中,所有寫入和更新都必須在主服務器上進行。但是,讀取可以在一個或多個從設備上進行。該模型可以提高寫入性能(因為主設備專用於更新),同時顯着提高了越來越多的從設備的讀取速度。
數據安全性 - 因為數據被復制到從站,並且從站可以暫停復制過程,所以可以在從站上運行備份服務而不會破壞相應的主數據。
分析 - 可以在主服務器上創建實時數據,而信息分析可以在從服務器上進行,而不會影響主服務器的性能。
遠程數據分發 - 您可以使用復制為遠程站點創建數據的本地副本,而無需永久訪問主服務器。
2、Replication 的原理
前提是作為主服務器角色的數據庫服務器必須開啟二進制日志
主服務器上面的任何修改都會通過自己的 I/O tread(I/O 線程)保存在二進制日志 Binary log 里面。
從服務器上面也啟動一個 I/O thread,通過配置好的用戶名和密碼, 連接到主服務器上面請求讀取二進制日志,然后把讀取到的二進制日志寫到本地的一個Realy log(中繼日志)里面。
從服務器上面同時開啟一個 SQL thread 定時檢查 Realy log(這個文件也是二進制的),如果發現有更新立即把更新的內容在本機的數據庫上面執行一遍。
每個從服務器都會收到主服務器二進制日志的全部內容的副本。
從服務器設備負責決定應該執行二進制日志中的哪些語句。
除非另行指定,否則主從二進制日志中的所有事件都在從站上執行。
如果需要,您可以將從服務器配置為僅處理一些特定數據庫或表的事件。
3、具體配置如下
Master節點配置/etc/my.cnf (master節點執行)
> vim /etc/my.cnf
[mysqld]
## 同一局域網內注意要唯一
server-id=100
## 開啟二進制日志功能,可以隨便取(關鍵)
log-bin=mysql-bin
## 復制過濾:不需要備份的數據庫,不輸出(mysql庫一般不同步)
binlog-ignore-db=mysql
## 為每個session 分配的內存,在事務過程中用來存儲二進制日志的緩存
binlog_cache_size=1M
## 主從復制的格式(mixed,statement,row,默認格式是statement)
binlog_format=mixed
Slave節點配置/etc/my.cnf
(slave節點執行)
> vim /etc/my.cnf
[mysqld]
## 設置server_id,注意要唯一
server-id=102
## 開啟二進制日志功能,以備Slave作為其它Slave的Master時使用
log-bin=mysql-slave-bin
## relay_log配置中繼日志
relay_log=edu-mysql-relay-bin
##復制過濾:不需要備份的數據庫,不輸出(mysql庫一般不同步)
binlog-ignore-db=mysql
## 如果需要同步函數或者存儲過程
log_bin_trust_function_creators=true
## 為每個session 分配的內存,在事務過程中用來存儲二進制日志的緩存
binlog_cache_size=1M
## 主從復制的格式(mixed,statement,row,默認格式是statement)
binlog_format=mixed
## 跳過主從復制中遇到的所有錯誤或指定類型的錯誤,避免slave端復制中斷。
## 如:1062錯誤是指一些主鍵重復,1032錯誤是因為主從數據庫數據不一致
slave_skip_errors=1062
在master服務器授權slave服務器可以同步權限(master節點執行)
注意:在master服務器上執行
mysql > mysql -uroot -pmaster的密碼
# 授予slave服務器可以同步master服務
mysql > grant replication slave, replication client on *.* to 'root'@'slave服務的ip' identified by 'slave服務器的密碼';
mysql > flush privileges;
# 查看MySQL現在有哪些用戶及對應的IP權限(可以不執行,只是一個查看)
mysql > select user,host from mysql.user;
- 日志文件名:mysql-bin.000002
- 復制的位置:2079
slave進行關聯master節點(slave節點執行)
進入到slave節點:
mysql > mysql -uroot -p你slave的密碼
開始綁定
mysql> change master to master_host='master服務器ip', master_user='root', master_password='master密碼', master_port=3306, master_log_file='mysql-bin.000002',master_log_pos=2079;
這里注意一下 master_log_file 和 master_log_pos 都是通過 master服務器通過show master status獲得。
在slave節點上查看主從同步狀態(slave節點執行)
啟動主從復制
mysql> start slave;
Query OK, 0 rows affected (0.00 sec)
再查看主從同步狀態
mysql> show slave status\G;
其他命令 (slave節點執行)
# 停止復制
mysql> stop slave;
主從復制測試
在master下創建數據庫和表,或者修改和新增,刪除記錄都會進行同步(master節點執行)
點擊查看slave節點信息(slave節點執行)
切記
在主從復制操作的時候,不要基於去創建數據庫或者相關操作。然后又去刪除。這樣會造成主從復制的pos改變,而造成復制失敗,如果出現此類問題,查看04-03的常見問題排查。
4、主從復制相關問題排查
1、主從復制Connecting問題
使用start slave開啟主從復制過程后,如果SlaveIORunning一直是Connecting,則說明主從復制一直處於連接狀態,這種情況一般是下面幾種原因造成的,我們可以根據 Last_IO_Error提示予以排除。
網絡不通
檢查ip,端口
密碼不對
檢查是否創建用於同步的用戶和用戶密碼是否正確
pos不對
檢查Master的 Position
2、MYSQL鏡像服務器因錯誤停止的恢復 —Slave_SQL_Running: No
先stop slave,然后執行了一下提示的語句,再
> stop slave;
> set global sql_slave_skip_counter=1;
> start slave;
> show slave status\G ;
3、從MYSQL服務器Slave_IO_Running: No的解決2
master節點執行,獲取日志文件和post
mysql > show master status;
1
slave節點進行重新綁定
mysql > stop slave;
mysql > CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000008', MASTER_LOG_POS=519086591;
mysql > start slave;
1
2
3
造成這類問題的原因一般是在主從復制的時候,基於創建表,然后又去刪除和操作了數據表或者表。
四、ShardingJdbc的配置及讀寫分離
1、內容大綱
新建一個springboot工程
引入相關sharding依賴、ssm依賴、數據庫驅動
定義配置application.yml
定義entity、mapper、controller
訪問測試查看效果
小結
2、具體實現步驟
1、 新建一個springboot工程
2、 引入相關sharding依賴、ssm依賴、數據庫驅動
<properties>
<java.version>1.8</java.version>
<sharding-sphere.version>4.0.0-RC1</sharding-sphere.version>
</properties>
<!-- 依賴web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 依賴mybatis和mysql驅動 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--依賴lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--依賴sharding-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-core-common</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<!--依賴數據源druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
3、 定義配置application.yml
server:
port: 8085
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
# 參數配置,顯示sql
props:
sql:
show: true
# 配置數據源
datasource:
# 給每個數據源取別名,下面的ds1,ds2,ds3任意取名字
names: ds1,ds2,ds3
# 給master-ds1每個數據源配置數據庫連接信息
ds1:
# 配置druid數據源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.115.94.78:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置ds2-slave
ds2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://114.215.145.201:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置ds3-slave
ds3:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://114.215.145.201:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置默認數據源ds1
sharding:
# 默認數據源,主要用於寫,注意一定要配置讀寫分離 ,注意:如果不配置,那么就會把三個節點都當做從slave節點,新增,修改和刪除會出錯。
default-data-source-name: ds1
# 配置數據源的讀寫分離,但是數據庫一定要做主從復制
masterslave:
# 配置主從名稱,可以任意取名字
name: ms
# 配置主庫master,負責數據的寫入
master-data-source-name: ds1
# 配置從庫slave節點
slave-data-source-names: ds2,ds3
# 配置slave節點的負載均衡均衡策略,采用輪詢機制
load-balance-algorithm-type: round_robin
# 整合mybatis的配置XXXXX
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.xuexiangban.shardingjdbc.entity
注意問題:
# 配置默認數據源ds1
sharding:
# 默認數據源,主要用於寫,注意一定要配置讀寫分離
# 注意:如果不配置,那么就會把三個節點都當做從slave節點,新增,修改和刪除會出錯。
default-data-source-name: ds1
# 配置數據源的讀寫分離,但是數據庫一定要做主從復制
masterslave:
# 配置主從名稱,可以任意取名字
name: ms
# 配置主庫master,負責數據的寫入
master-data-source-name: ds1
# 配置從庫slave節點
slave-data-source-names: ds2,ds3
# 配置slave節點的負載均衡均衡策略,采用輪詢機制
load-balance-algorithm-type: round_robin
如果上面的,那么shardingjdbc會采用隨機的方式進行選擇數據源。如果不配置default-data-source-name,那么就會把三個節點都當做從slave節點,那么新增,修改和刪除會出錯。
4、 定義mapper、controller,entity
entity
package com.xuexiangban.shardingjdbc.entity;
import lombok.Data;
/**
* @author: 學相伴-飛哥
* @description: User
* @Date : 2021/3/10
*/
@Data
public class User {
// 主鍵
private Integer id;
// 昵稱
private String nickname;
// 密碼
private String password;
// 性
private Integer sex;
// 性
private String birthday;
}
mapper
package com.xuexiangban.shardingjdbc.mapper;
import com.xuexiangban.shardingjdbc.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author: 學相伴-飛哥
* @description: UserMapper
* @Date : 2021/3/10
*/
public interface UserMapper {
/**
* @author 學相伴-飛哥
* @description 保存用戶
* @params [user]
* @date 2021/3/10 17:14
*/
@Insert("insert into ksd_user(nickname,password,sex,birthday) values(#{nickname},#{password},#{sex},#{birthday})")
void addUser(User user);
/**
* @author 學相伴-飛哥
* @description 保存用戶
* @params [user]
* @date 2021/3/10 17:14
*/
@Select("select * from ksd_user")
List<User> findUsers();
}
controller
package com.xuexiangban.shardingjdbc.controller;
import com.xuexiangban.shardingjdbc.entity.User;
import com.xuexiangban.shardingjdbc.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Random;
/**
* @author: 學相伴-飛哥
* @description: UserController
* @Date : 2021/3/10
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/save")
public String insert() {
User user = new User();
user.setNickname("zhangsan"+ new Random().nextInt());
user.setPassword("1234567");
user.setSex(1);
user.setBirthday("1988-12-03");
userMapper.addUser(user);
return "success";
}
@GetMapping("/listuser")
public List<User> listuser() {
return userMapper.findUsers();
}
}
5、 訪問測試查看效果
1:訪問 http://localhost:8085/user/save
一直進入到ds1主節點
2:訪問 http://localhost:8085/user/listuser
一直進入到ds2、ds3節點,並且輪詢進入。
6、 日志查看
7、 小結
回顧流程
ShardingSphere 的 3 個產品的數據分片主要流程是完全一致的。 核心由 SQL 解析 => 執行器優化 => SQL 路由 => SQL 改寫 => SQL 執行 => 結果歸並的流程組成。
SQL 解析
分為詞法解析和語法解析。 先通過詞法解析器將 SQL 拆分為一個個不可再分的單詞。再使用語法解析器對 SQL 進行理解,並最終提煉出解析上下文。 解析上下文包括表、選擇項、排序項、分組項、聚合函數、分頁信息、查詢條件以及可能需要修改的占位符的標記。
執行器優化
合並和優化分片條件,如 OR 等。
SQL 路由
根據解析上下文匹配用戶配置的分片策略,並生成路由路徑。目前支持分片路由和廣播路由。
SQL 改寫
將 SQL 改寫為在真實數據庫中可以正確執行的語句。SQL 改寫分為正確性改寫和優化改寫。
SQL 執行
通過多線程執行器異步執行。
結果歸並
將多個執行結果集歸並以便於通過統一的 JDBC 接口輸出。結果歸並包括流式歸並、內存歸並和使用裝飾者模式的追加歸並這幾種方式。
3、Props的其他相關配置
acceptor.size: # accept連接的線程數量,默認為cpu核數2倍
executor.size: #工作線程數量最大,默認值: 無限制
max.connections.size.per.query: # 每個查詢可以打開的最大連接數量,默認為1
check.table.metadata.enabled: #是否在啟動時檢查分表元數據一致性,默認值: false
proxy.frontend.flush.threshold: # proxy的服務時候,對於單個大查詢,每多少個網絡包返回一次
proxy.transaction.type: # 默認LOCAL,proxy的事務模型 允許LOCAL,XA,BASE三個值,LOCAL無分布式事務,XA則是采用atomikos實現的分布式事務 BASE目前尚未實現
proxy.opentracing.enabled: # 是否啟用opentracing
proxy.backend.use.nio: # 是否采用netty的NIO機制連接后端數據庫,默認False ,使用epoll機制
proxy.backend.max.connections: # 使用NIO而非epoll的話,proxy后台連接每個netty客戶端允許的最大連接數量(注意不是數據庫連接限制) 默認為8
proxy.backend.connection.timeout.seconds: #使用nio而非epoll的話,proxy后台連接的超時時間,默認60s
五、MySQL分庫分表原理
1、為什么要分庫分表
一般的機器(4核16G),單庫的MySQL並發(QPS+TPS)超過了2k,系統基本就完蛋了。最好是並發量控制在1k左右。這里就引出一個問題,為什么要分庫分表?
分庫分表目的:解決高並發,和數據量大的問題。
1、高並發情況下,會造成IO讀寫頻繁,自然就會造成讀寫緩慢,甚至是宕機。一般單庫不要超過2k並發,NB的機器除外。
2、數據量大的問題。主要由於底層索引實現導致,MySQL的索引實現為B+TREE,數據量其他,會導致索引樹十分龐大,造成查詢緩慢。第二,innodb的最大存儲限制64TB。
要解決上述問題。最常見做法,就是分庫分表。
分庫分表的目的,是將一個表拆成N個表,就是讓每個表的數據量控制在一定范圍內,保證SQL的性能。 一個表數據建議不要超過500W
2、分庫分表
又分為垂直拆分和水平拆分。
**水平拆分:**統一個表的數據拆到不同的庫不同的表中。可以根據時間、地區、或某個業務鍵維度,也可以通過hash進行拆分,最后通過路由訪問到具體的數據。拆分后的每個表結構保持一致。
**垂直拆分:**就是把一個有很多字段的表給拆分成多個表,或者是多個庫上去。每個庫表的結構都不一樣,每個庫表都包含部分字段。一般來說,可以根據業務維度進行拆分,如訂單表可以拆分為訂單、訂單支持、訂單地址、訂單商品、訂單擴展等表;也可以,根據數據冷熱程度拆分,20%的熱點字段拆到一個表,80%的冷字段拆到另外一個表。
3、不停機分庫分表數據遷移
一般數據庫的拆分也是有一個過程的,一開始是單表,后面慢慢拆成多表。那么我們就看下如何平滑的從MySQL單表過度到MySQL的分庫分表架構。
1、利用mysql+canal做增量數據同步,利用分庫分表中間件,將數據路由到對應的新表中。
2、利用分庫分表中間件,全量數據導入到對應的新表中。
3、通過單表數據和分庫分表數據兩兩比較,更新不匹配的數據到新表中。
4、數據穩定后,將單表的配置切換到分庫分表配置上。
4、小結
垂直拆分:業務模塊拆分、商品庫,用戶庫,訂單庫
水平拆分:對表進行水平拆分(也就是我們說的:分表)
表進行垂直拆分:表的字段過多,字段使用的頻率不一。(可以拆分兩個表建立1:1關系)
六、ShardingJdbc的分庫和分表
1、分庫分表的方式
**水平拆分:**統一個表的數據拆到不同的庫不同的表中。可以根據時間、地區、或某個業務鍵維度,也可以通過hash進行拆分,最后通過路由訪問到具體的數據。拆分后的每個表結構保持一致。
**垂直拆分:**就是把一個有很多字段的表給拆分成多個表,或者是多個庫上去。每個庫表的結構都不一樣,每個庫表都包含部分字段。一般來說,可以根據業務維度進行拆分,如訂單表可以拆分為訂單、訂單支持、訂單地址、訂單商品、訂單擴展等表;也可以,根據數據冷熱程度拆分,20%的熱點字段拆到一個表,80%的冷字段拆到另外一個表。
2、邏輯表
邏輯表是指:水平拆分的數據庫或者數據表的相同路基和數據結構表的總稱。比如用戶數據根據用戶id%2拆分為2個表,分別是:ksd_user0和ksd_user1。他們的邏輯表名是:ksd_user。
在shardingjdbc中的定義方式如下:
spring:
shardingsphere:
sharding:
tables:
# ksd_user 邏輯表名
ksd_user:
3、分庫分表數據節點 - actual-data-nodes
tables:
# ksd_user 邏輯表名
ksd_user:
# 數據節點:多數據源$->{0..N}.邏輯表名$->{0..N} 相同表
actual-data-nodes: ds$->{0..2}.ksd_user$->{0..1}
# 數據節點:多數據源$->{0..N}.邏輯表名$->{0..N} 不同表
actual-data-nodes: ds0.ksd_user$->{0..1},ds1.ksd_user$->{2..4}
# 指定單數據源的配置方式
actual-data-nodes: ds0.ksd_user$->{0..4}
# 全部手動指定
actual-data-nodes: ds0.ksd_user0,ds1.ksd_user0,ds0.ksd_user1,ds1.ksd_user1,
數據分片是最小單元。由數據源名稱和數據表組成,比如:ds0.ksd_user0。
尋找規則如下:
4、分庫分表5種分片策略
img
數據源分片分為兩種:
數據源分片
表分片
這兩個是不同維度的分片規則,但是它們額能用的分片策略和規則是一樣的。它們由兩部分構成:
分片鍵
分片算法
第一種:none
對應NoneShardingStragey,不分片策略,SQL會被發給所有節點去執行,這個規則沒有子項目可以配置。
第二種:inline 行表達時分片策略(核心,必須要掌握)
對應InlineShardingStragey。使用Groovy的表達時,提供對SQL語句種的=和in的分片操作支持,只支持單分片鍵。對於簡單的分片算法,可以通過簡單的配置使用,從而避免繁瑣的Java代碼開放,如:ksd_user${分片鍵(數據表字段)userid % 5} 表示ksd_user表根據某字段(userid)模 5.從而分為5張表,表名稱為:ksd_user0到ksd_user4 。如果庫也是如此。
server:
port: 8085
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
# 參數配置,顯示sql
props:
sql:
show: true
sharding:
# 默認數據源,主要用於寫,注意一定要配置讀寫分離 ,注意:如果不配置,那么就會把三個節點都當做從slave節點,新增,修改和刪除會出錯。
default-data-source-name: ds0
# 配置分表的規則
tables:
# ksd_user 邏輯表名
ksd_user:
# 數據節點:數據源$->{0..N}.邏輯表名$->{0..N}
actual-data-nodes: ds$->{0..1}.ksd_user$->{0..1}
# 拆分庫策略,也就是什么樣子的數據放入放到哪個數據庫中。
database-strategy:
inline:
sharding-column: sex # 分片字段(分片鍵)
algorithm-expression: ds$->{sex % 2} # 分片算法表達式
# 拆分表策略,也就是什么樣子的數據放入放到哪個數據表中。
table-strategy:
inline:
sharding-column: age # 分片字段(分片鍵)
algorithm-expression: ksd_user$->{age % 2} # 分片算法表達式
algorithm-expression行表達式:
${begin…end} 表示區間范圍。
${[unit1,unit2,….,unitn]} 表示枚舉值。
行表達式種如果出現連續多個 e x p r e s s s i o n 或 {expresssion}或 expresssion或->{expression}表達式,整個表達時最終的結果將會根據每個子表達式的結果進行笛卡爾組合。
1、完整案例和配置如下
准備兩個數據庫ksd_sharding-db。名字相同,兩個數據源ds0和ds1
每個數據庫下方ksd_user0和ksd_user1即可。
數據庫規則,性別為偶數的放入ds0庫,奇數的放入ds1庫。
數據表規則:年齡為偶數的放入ksd_user0庫,奇數的放入ksd_user1庫。
server:
port: 8085
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
# 參數配置,顯示sql
props:
sql:
show: true
# 配置數據源
datasource:
# 給每個數據源取別名,下面的ds1,ds1任意取名字
names: ds0,ds1
# 給master-ds1每個數據源配置數據庫連接信息
ds0:
# 配置druid數據源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.115.94.78:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置ds1-slave
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://114.215.145.201:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置默認數據源ds0
sharding:
# 默認數據源,主要用於寫,注意一定要配置讀寫分離 ,注意:如果不配置,那么就會把三個節點都當做從slave節點,新增,修改和刪除會出錯。
default-data-source-name: ds0
# 配置分表的規則
tables:
# ksd_user 邏輯表名
ksd_user:
# 數據節點:數據源$->{0..N}.邏輯表名$->{0..N}
actual-data-nodes: ds$->{0..1}.ksd_user$->{0..1}
# 拆分庫策略,也就是什么樣子的數據放入放到哪個數據庫中。
database-strategy:
inline:
sharding-column: sex # 分片字段(分片鍵)
algorithm-expression: ds$->{sex % 2} # 分片算法表達式
# 拆分表策略,也就是什么樣子的數據放入放到哪個數據表中。
table-strategy:
inline:
sharding-column: age # 分片字段(分片鍵)
algorithm-expression: ksd_user$->{age % 2} # 分片算法表達式
# 整合mybatis的配置XXXXX
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.xuexiangban.shardingjdbc.entity
結果如下圖:
5、第三種:根據實時間日期 - 按照標准規則分庫分表
1、 標准分片 - Standard(了解)
對應StrandardShardingStrategy.提供對SQL語句中的=,in和惡between and 的分片操作支持。
StrandardShardingStrategy只支持但分片鍵。提供PreciseShardingAlgorithm和RangeShardingAlgorithm兩個分片算法。
PreciseShardingAlgorithm是必選的呃,用於處理=和IN的分片
和RangeShardingAlgorithm是可選的,是用於處理Betwwen and分片,如果不配置和RangeShardingAlgorithm,SQL的Between AND 將按照全庫路由處理。
2、定義分片的日期規則配置
server:
port: 8085
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
# 參數配置,顯示sql
props:
sql:
show: true
# 配置數據源
datasource:
# 給每個數據源取別名,下面的ds1,ds1任意取名字
names: ds0,ds1
# 給master-ds1每個數據源配置數據庫連接信息
ds0:
# 配置druid數據源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.115.94.78:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT%2b8
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置ds1-slave
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://114.215.145.201:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT%2b8
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置默認數據源ds0
sharding:
# 默認數據源,主要用於寫,注意一定要配置讀寫分離 ,注意:如果不配置,那么就會把三個節點都當做從slave節點,新增,修改和刪除會出錯。
default-data-source-name: ds0
# 配置分表的規則
tables:
# ksd_user 邏輯表名
ksd_user:
# 數據節點:數據源$->{0..N}.邏輯表名$->{0..N}
actual-data-nodes: ds$->{0..1}.ksd_user$->{0..1}
# 拆分庫策略,也就是什么樣子的數據放入放到哪個數據庫中。
database-strategy:
standard:
shardingColumn: birthday
preciseAlgorithmClassName: com.xuexiangban.shardingjdbc.algorithm.BirthdayAlgorithm
table-strategy:
inline:
sharding-column: age # 分片字段(分片鍵)
algorithm-expression: ksd_user$->{age % 2} # 分片算法表達式
# 整合mybatis的配置XXXXX
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.xuexiangban.shardingjdbc.entity
3、定義分片的日期規則
package com.xuexiangban.shardingjdbc.algorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import sun.util.resources.cldr.CalendarData;
import java.util.*;
/**
* @author: 學相伴-飛哥
* @description: BirthdayAlgorithm
* @Date : 2021/3/11
*/
public class BirthdayAlgorithm implements PreciseShardingAlgorithm<Date> {
List<Date> dateList = new ArrayList<>();
{
Calendar calendar1 = Calendar.getInstance();
calendar1.set(2020, 1, 1, 0, 0, 0);
Calendar calendar2 = Calendar.getInstance();
calendar2.set(2021, 1, 1, 0, 0, 0);
Calendar calendar3 = Calendar.getInstance();
calendar3.set(2022, 1, 1, 0, 0, 0);
dateList.add(calendar1.getTime());
dateList.add(calendar2.getTime());
dateList.add(calendar3.getTime());
}
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Date> preciseShardingValue) {
// 獲取屬性數據庫的值
Date date = preciseShardingValue.getValue();
// 獲取數據源的名稱信息列表
Iterator<String> iterator = collection.iterator();
String target = null;
for (Date s : dateList) {
target = iterator.next();
// 如果數據晚於指定的日期直接返回
if (date.before(s)) {
break;
}
}
return target;
}
}
4、測試查看結果
http://localhost:8085/user/save?sex=3&age=3&birthday=2020-03-09 —- ds1
http://localhost:8085/user/save?sex=3&age=3&birthday=2021-03-09 —- ds0
6、第四種:ShardingSphere - 符合分片策略(了解)
對應接口:HintShardingStrategy。通過Hint而非SQL解析的方式分片的策略。
對於分片字段非SQL決定,而是由其他外置條件決定的場景,克使用SQL hint靈活的注入分片字段。例如:按照用戶登錄的時間,主鍵等進行分庫,而數據庫中並無此字段。SQL hint支持通過Java API和SQL注解兩種方式使用。讓后分庫分表更加靈活。
7、第五種:ShardingSphere - hint分片策略(了解)
對應ComplexShardingStrategy。符合分片策略提供對SQL語句中的-,in和between and的分片操作支持。
ComplexShardingStrategy支持多分片鍵,由於多分片鍵之間的關系復雜,因此並未進行過多的封裝,而是直接將分片鍵組合以及分片操作符透傳至分片算法,完全由開發者自己實現,提供最大的靈活度。
七、ShardingSphere - 分布式主鍵配置
1、ShardingSphere - 分布式主鍵配置
ShardingSphere提供靈活的配置分布式主鍵生成策略方式。在分片規則配置模塊克配置每個表的主鍵生成策略。默認使用雪花算法。(snowflake)生成64bit的長整型數據。支持兩種方式配置
SNOWFLAKE
UUID
這里切記:主鍵列不能自增長。數據類型是:bigint(20)
spring:
shardingsphere:
sharding:
tables:
# ksd_user 邏輯表名
ksd_user:
key-generator:
# 主鍵的列明,
column: userid
type: SNOWFLAKE
執行
http://localhost:8085/user/save?sex=3&age=3&birthday=2020-03-09
可以查看到新增的語句多了一個userid為576906137413091329的唯一值。這個值是通過雪花算法計算出來的唯一值
2021-03-11 22:59:01.605 INFO 4900 --- [nio-8085-exec-1] ShardingSphere-SQL : Actual SQL: ds1 ::: insert into ksd_user1 (nickname, password, sex, age, birthday, userid) VALUES (?, ?, ?, ?, ?, ?) ::: [zhangsan-70137485, 1234567, 3, 3, 2020-03-09 00:00:00.0, 576906137413091329]
八、ShardingSphere - 分庫分表 - 年月案例
實戰完成按照年月分庫分表。
1、策略類
package com.xuexiangban.shardingjdbc.algorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.util.Collection;
public class YearMonthShardingAlgorithm implements PreciseShardingAlgorithm<String> {
private static final String SPLITTER = "_";
@Override
public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
String tbName = shardingValue.getLogicTableName() + "_" + shardingValue.getValue();
System.out.println("Sharding input:" + shardingValue.getValue() + ", output:{}" + tbName);
return tbName;
}
}
2、entity
package com.xuexiangban.shardingjdbc.entity;
import lombok.Data;
import java.util.Date;
/**
* @author: 學相伴-飛哥
* @description: User
* @Date : 2021/3/10
*/
@Data
public class Order {
// 主鍵
private Long orderid;
// 訂單編號
private String ordernumber;
// 用戶ID
private Long userid;
// 產品id
private Long productid;
// 創建時間
private Date createTime;
}
3、mapper
package com.xuexiangban.shardingjdbc.mapper;
import com.xuexiangban.shardingjdbc.entity.Order;
import com.xuexiangban.shardingjdbc.entity.UserOrder;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.springframework.stereotype.Repository;
/**
* @author: 學相伴-飛哥
* @description: UserMapper
* @Date : 2021/3/10
*/
@Mapper
@Repository
public interface UserOrderMapper {
/**
* @author 學相伴-飛哥
* @description 保存訂單
* @params [user]
* @date 2021/3/10 17:14
*/
@Insert("insert into ksd_user_order(ordernumber,userid,create_time,yearmonth) values(#{ordernumber},#{userid},#{createTime},#{yearmonth})")
@Options(useGeneratedKeys = true,keyColumn = "orderid",keyProperty = "orderid")
void addUserOrder(UserOrder userOrder);
}
4、配置如下:
server:
port: 8085
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
# 參數配置,顯示sql
props:
sql:
show: true
# 配置數據源
datasource:
# 給每個數據源取別名,下面的ds1,ds1任意取名字
names: ds0,ds1
# 給master-ds1每個數據源配置數據庫連接信息
ds0:
# 配置druid數據源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.115.94.78:3306/ksd_order_db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT%2b8
username: root
password: mkxiaoer1986.
maxPoolSize: 100
minPoolSize: 5
# 配置ds1-slave
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://114.215.145.201:3306/ksd_order_db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT%2b8
username: root
password: mkxiaoer
maxPoolSize: 100
minPoolSize: 5
# 配置默認數據源ds0
sharding:
# 默認數據源,主要用於寫,注意一定要配置讀寫分離 ,注意:如果不配置,那么就會把三個節點都當做從slave節點,新增,修改和刪除會出錯。
default-data-source-name: ds0
# 配置分表的規則
tables:
# ksd_user 邏輯表名
ksd_user:
key-generator:
column: id
type: SNOWFLAKE
# 數據節點:數據源$->{0..N}.邏輯表名$->{0..N}
actual-data-nodes: ds$->{0..1}.ksd_user$->{0..1}
# 拆分庫策略,也就是什么樣子的數據放入放到哪個數據庫中。
database-strategy:
standard:
shardingColumn: birthday
preciseAlgorithmClassName: com.xuexiangban.shardingjdbc.algorithm.BirthdayAlgorithm
table-strategy:
inline:
sharding-column: age # 分片字段(分片鍵)
algorithm-expression: ksd_user$->{age % 2} # 分片算法表達式
ksd_order:
# 數據節點:數據源$->{0..N}.邏輯表名$->{0..N}
actual-data-nodes: ds0.ksd_order$->{0..1}
key-generator:
column: orderid
type: SNOWFLAKE
# 拆分庫策略,也就是什么樣子的數據放入放到哪個數據庫中。
table-strategy:
inline:
sharding-column: orderid # 分片字段(分片鍵)
algorithm-expression: ksd_order$->{orderid % 2} # 分片算法表達式
ksd_user_order:
# 數據節點:數據源$->{0..N}.邏輯表名$->{0..N}
actual-data-nodes: ds0.ksd_user_order_$->{2021..2022}${(1..3).collect{t ->t.toString().padLeft(2,'0')} }
key-generator:
column: orderid
type: SNOWFLAKE
# 拆分庫策略,也就是什么樣子的數據放入放到哪個數據庫中。
table-strategy:
# inline:
# shardingColumn: yearmonth
# algorithmExpression: ksd_user_order_$->{yearmonth}
standard:
shardingColumn: yearmonth
preciseAlgorithmClassName: com.xuexiangban.shardingjdbc.algorithm.YearMonthShardingAlgorithm
# 整合mybatis的配置XXXXX
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.xuexiangban.shardingjdbc.entity
5、test
package com.xuexiangban.shardingjdbc;
import com.xuexiangban.shardingjdbc.entity.Order;
import com.xuexiangban.shardingjdbc.entity.User;
import com.xuexiangban.shardingjdbc.entity.UserOrder;
import com.xuexiangban.shardingjdbc.mapper.UserOrderMapper;
import com.xuexiangban.shardingjdbc.service.UserOrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
@SpringBootTest
class ShardingJdbcApplicationTests {
@Autowired
private UserOrderService userOrderService;
@Test
void contextLoads() throws Exception {
User user = new User();
user.setNickname("zhangsan" + new Random().nextInt());
user.setPassword("1234567");
user.setSex(1);
user.setAge(2);
user.setBirthday(new Date());
Order order = new Order();
order.setCreateTime(new Date());
order.setOrdernumber("133455678");
order.setProductid(1234L);
userOrderService.saveUserOrder(user, order);
}
@Autowired
private UserOrderMapper userOrderMapper;
@Test
public void orderyearMaster() {
UserOrder userOrder = new UserOrder();
userOrder.setOrderid(10000L);
userOrder.setCreateTime(new Date());
userOrder.setOrdernumber("133455678");
userOrder.setYearmonth("202103");
userOrder.setUserid(1L);
userOrderMapper.addUserOrder(userOrder);
}
}
九、ShardingJdbc的事務管理
1、分布式式事務的應用和實踐
官方地址:https://shardingsphere.apache.org/document/legacy/4.x/document/cn/features/transaction/function/base-transaction-seata/
https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/sharding-jdbc/usage/transaction/
數據庫事務需要滿足ACID(原子性、一致性、隔離性、持久性)四個特性。
原子性(Atomicity)指事務作為整體來執行,要么全部執行,要么全不執行。
一致性(Consistency)指事務應確保數據從一個一致的狀態轉變為另一個一致的狀態。
隔離性(Isolation)指多個事務並發執行時,一個事務的執行不應影響其他事務的執行。
持久性(Durability)指已提交的事務修改數據會被持久保存。
在單一數據節點中,事務僅限於對單一數據庫資源的訪問控制,稱之為本地事務。幾乎所有的成熟的關系型數據庫都提供了對本地事務的原生支持。 但是在基於微服務的分布式應用環境下,越來越多的應用場景要求對多個服務的訪問及其相對應的多個數據庫資源能納入到同一個事務當中,分布式事務應運而生。
關系型數據庫雖然對本地事務提供了完美的ACID原生支持。 但在分布式的場景下,它卻成為系統性能的桎梏。如何讓數據庫在分布式場景下滿足ACID的特性或找尋相應的替代方案,是分布式事務的重點工作。
本地事務
在不開啟任何分布式事務管理器的前提下,讓每個數據節點各自管理自己的事務。 它們之間沒有協調以及通信的能力,也並不互相知曉其他數據節點事務的成功與否。 本地事務在性能方面無任何損耗,但在強一致性以及最終一致性方面則力不從心。
兩階段提交
XA協議最早的分布式事務模型是由X/Open國際聯盟提出的X/Open Distributed Transaction Processing(DTP)模型,簡稱XA協議。
基於XA協議實現的分布式事務對業務侵入很小。 它最大的優勢就是對使用方透明,用戶可以像使用本地事務一樣使用基於XA協議的分布式事務。 XA協議能夠嚴格保障事務ACID特性。
嚴格保障事務ACID特性是一把雙刃劍。 事務執行在過程中需要將所需資源全部鎖定,它更加適用於執行時間確定的短事務。 對於長事務來說,整個事務進行期間對數據的獨占,將導致對熱點數據依賴的業務系統並發性能衰退明顯。 因此,在高並發的性能至上場景中,基於XA協議的分布式事務並不是最佳選擇。
柔性事務
如果將實現了ACID的事務要素的事務稱為剛性事務的話,那么基於BASE事務要素的事務則稱為柔性事務。 BASE是基本可用、柔性狀態和最終一致性這三個要素的縮寫。
基本可用(Basically Available)保證分布式事務參與方不一定同時在線。
柔性狀態(Soft state)則允許系統狀態更新有一定的延時,這個延時對客戶來說不一定能夠察覺。
而最終一致性(Eventually consistent)通常是通過消息傳遞的方式保證系統的最終一致性。
在ACID事務中對隔離性的要求很高,在事務執行過程中,必須將所有的資源鎖定。 柔性事務的理念則是通過業務邏輯將互斥鎖操作從資源層面上移至業務層面。通過放寬對強一致性要求,來換取系統吞吐量的提升。
基於ACID的強一致性事務和基於BASE的最終一致性事務都不是銀彈,只有在最適合的場景中才能發揮它們的最大長處。 可通過下表詳細對比它們之間的區別,以幫助開發者進行技術選型。
2、案例
1:導入分布式事務的依賴
<!--依賴sharding-->
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-transaction-spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
2:事務的幾種類型
本地事務
完全支持非跨庫事務,例如:僅分表,或分庫但是路由的結果在單庫中。
完全支持因邏輯異常導致的跨庫事務。例如:同一事務中,跨兩個庫更新。更新完畢后,拋出空指針,則兩個庫的內容都能回滾。
不支持因網絡、硬件異常導致的跨庫事務。例如:同一事務中,跨兩個庫更新,更新完畢后、未提交之前,第一個庫宕機,則只有第二個庫數據提交。
兩階段XA事務
支持數據分片后的跨庫XA事務
兩階段提交保證操作的原子性和數據的強一致性
服務宕機重啟后,提交/回滾中的事務可自動恢復
SPI機制整合主流的XA事務管理器,默認Atomikos,可以選擇使用Narayana和Bitronix
同時支持XA和非XA的連接池
提供spring-boot和namespace的接入端
不支持:
服務宕機后,在其它機器上恢復提交/回滾中的數據
Seata柔性事務
完全支持跨庫分布式事務
支持RC隔離級別
通過undo快照進行事務回滾
支持服務宕機后的,自動恢復提交中的事務
依賴:
需要額外部署Seata-server服務進行分支事務的協調
待優化項
ShardingSphere和Seata會對SQL進行重復解析
3:service代碼編寫
package com.xuexiangban.shardingjdbc.service;
import com.xuexiangban.shardingjdbc.entity.Order;
import com.xuexiangban.shardingjdbc.entity.User;
import com.xuexiangban.shardingjdbc.mapper.OrderMapper;
import com.xuexiangban.shardingjdbc.mapper.UserMapper;
import io.shardingsphere.transaction.annotation.ShardingTransactionType;
import io.shardingsphere.transaction.api.TransactionType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author: 學相伴-飛哥
* @description: UserService
* @Date : 2021/3/14
*/
@Service
public class UserOrderService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
@ShardingTransactionType(TransactionType.XA)
@Transactional(rollbackFor = Exception.class)
public int saveUserOrder(User user, Order order) {
userMapper.addUser(user);
order.setUserid(user.getId());
orderMapper.addOrder(order);
//int a = 1/0; //測試回滾,統一提交的話,將這行注釋掉就行
return 1;
}
}
測試
package com.xuexiangban.shardingjdbc;
import com.xuexiangban.shardingjdbc.entity.Order;
import com.xuexiangban.shardingjdbc.entity.User;
import com.xuexiangban.shardingjdbc.entity.UserOrder;
import com.xuexiangban.shardingjdbc.mapper.UserOrderMapper;
import com.xuexiangban.shardingjdbc.service.UserOrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
@SpringBootTest
class ShardingJdbcApplicationTests {
@Autowired
private UserOrderService userOrderService;
@Test
void contextLoads() throws Exception {
User user = new User();
user.setNickname("zhangsan" + new Random().nextInt());
user.setPassword("1234567");
user.setSex(1);
user.setAge(2);
user.setBirthday(new Date());
Order order = new Order();
order.setCreateTime(new Date());
order.setOrdernumber("133455678");
order.setProductid(1234L);
userOrderService.saveUserOrder(user, order);
}
}
十、ShardingJdbc的總結
1、基礎規范
表必須有主鍵,建議使用整型作為主鍵
禁止使用外鍵,表之間的關聯性和完整性通過應用層來控制
表在設計之初,應該考慮到大致的數據級,若表記錄小於1000W,盡量使用單表,不建議分表。
建議將大字段,訪問頻率低,或者不需要作為篩選條件的字段拆分到拓展表中,(做好表垂直拆分)
控制單實例表的總數,單個表分表數控制在1024以內。
2、列設計規范
正確區分tinyint、int、bigint的范圍
使用varchar(20)存儲手機號,不要使用整數
使用int存儲ipv4 不要使用char(15)
涉及金額使用decimal/varchar,並制定精度
不要設計為null的字段,而是用空字符,因為null需要更多的空間,並且使得索引和統計變得更復雜。
3、索引規范
唯一索引使用uniq_[字段名]來命名
非唯一索引使用idx_[字段名]來命名
不建議在頻繁更新的字段上建立索引
非必要不要進行JOIN,如果要進行join查詢,被join的字段必須類型相同,並建立索引。
單張表的索引數量建議控制在5個以內,索引過多,不僅會導致插入更新性能下降,還可能導致MYSQL的索引出錯和性能下降
組合索引字段數量不建議超過5個,理解組合索引的最左匹配原則,避免重復建設索引。比如你建立了
(x,y,z) 相當於你建立了(x),(x,y),(x,y,z)
4、SQL規范
禁止使用selet ,只獲取必要字段,select 會增加cpu/i0/內存、帶寬的消耗。
insert 必須指定字段,禁止使用insert into Table values().指定字段插入,在表結果變更時,能保證對應應用程序無影響。
隱私類型轉換會使索引失效,導致全表掃描。(比如:手機號碼搜索時未轉換成字符串)
禁止在where后面查詢列使用內置函數或者表達式,導致不能命中索引,導致全表掃描
禁止負向查詢(!=,not like ,no in等)以及%開頭的模糊查詢,造成不能命中索引,導致全表掃描
避免直接返回大結果集造成內存溢出,可采用分段和游標方式。
返回結果集時盡量使用limit分頁顯示。
盡量在order by/group by的列上創建索引。
大表掃描盡量放在鏡像庫上去做
禁止大表join查詢和子查詢
盡量避免數據庫內置函數作為查詢條件
應用程序盡量捕獲SQL異常
5、表的垂直拆分
垂直拆分:業務模塊拆分、商品庫,用戶庫,訂單庫
水平拆分:對表進行水平拆分(也就是我們說的:分表)
表進行垂直拆分:表的字段過多,字段使用的頻率不一。(可以拆分兩個表建立1:1關系)
將一個屬性過多的表,一行數據較大的表,將不同的屬性分割到不同的數據庫表中。以降低單庫表的大小。
特點:
每個表的結構不一致
每個表的數量都是全量
表和表之間一定會有一列會進行關聯,一般都是主鍵
原則:
將長度較短,訪問頻率較高的字段放在一個表中,主表
將長度較長、訪問頻率比較低的字段放一個表中
將經常訪問字段放一個表中。
所有表的並集是全量數據。
6、如何平滑添加字段
場景:在開發時,有時需要給表加字段,在大數據量且分表的情況下,怎么樣平滑添加。
直接alter table add column,數據量大時不建議,(會產生寫鎖)
alter table ksd_user add column api_pay_no varchar(32) not null comment '用戶擴展訂單號'alter table ksd_user add column api_pay_no varchar(32) not null unique comment '用戶擴展訂單號'
提前預留字段(不優雅:造成空間浪費,預留多少很難控制,拓展性差)
新增一張表,(增加字段),遷移原表數據,在重新命名新表作為原表。
放入extinfo(無法使用索引)
提前設計,使用key/value方法存儲,新增字段時 ,直接加一個key就好了(優雅)
十一、Springboot整合ShardingJdbc3.0和案例分析
目標
使用Sharding-JDBC 分庫分表,掌握什么是Sharding-JDBC.
分析
什么是Sharding-JDBC
Sharding-JDBC提供標准化的數據分片、分布式事務和數據庫治理功能,定位為輕量級Java框架,在Java的JDBC層提供的額外服務。 它使用客戶端直連數據庫,以jar包形式提供服務,無需額外部署和依賴,可理解為增強版的JDBC驅動,完全兼容JDBC和各種ORM框架。適用於任何基於Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
基於任何第三方的數據庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
支持任意實現JDBC規范的數據庫。目前支持MySQL,Oracle,SQLServer和PostgreSQL。
2. 為什么要分片?
傳統的將數據集中存儲至單一數據節點的解決方案,在性能、可用性和運維成本這三方面已經難於滿足互聯網的海量數據場景。
從性能方面來說,由於關系型數據庫大多采用B+樹類型的索引,在數據量超過閾值的情況下,索引深度的增加也將使得磁盤訪問的IO次數增加,進而導致查詢性能的下降;同時,高並發訪問請求也使得集中式數據庫成為系統的最大瓶頸。
從可用性的方面來講,服務化的無狀態型,能夠達到較小成本的隨意擴容,這必然導致系統的最終壓力都落在數據庫之上。而單一的數據節點,或者簡單的主從架構,已經越來越難以承擔。數據庫的可用性,已成為整個系統的關鍵。
從運維成本方面考慮,當一個數據庫實例中的數據達到閾值以上,對於DBA的運維壓力就會增大。數據備份和恢復的時間成本都將隨着數據量的大小而愈發不可控。一般來講,單一數據庫實例的數據的閾值在1TB之內,是比較合理的范圍。
在傳統的關系型數據庫無法滿足互聯網場景需要的情況下,將數據存儲至原生支持分布式的NoSQL的嘗試越來越多。 但NoSQL對SQL的不兼容性以及生態圈的不完善,使得它們在與關系型數據庫的博弈中始終無法完成致命一擊,而關系型數據庫的地位卻依然不可撼動。
據分片指按照某個維度將存放在單一數據庫中的數據分散地存放至多個數據庫或表中以達到提升性能瓶頸以及可用性的效果。 數據分片的有效手段是對關系型數據庫進行分庫和分表。分庫和分表均可以有效的避免由數據量超過可承受閾值而產生的查詢瓶頸。 除此之外,分庫還能夠用於有效的分散對數據庫單點的訪問量;分表雖然無法緩解數據庫壓力,但卻能夠提供盡量將分布式事務轉化為本地事務的可能,一旦涉及到跨庫的更新操作,分布式事務往往會使問題變得復雜。 使用多主多從的分片方式,可以有效的避免數據單點,從而提升數據架構的可用性。
通過分庫和分表進行數據的拆分來使得各個表的數據量保持在閾值以下,以及對流量進行疏導應對高訪問量,是應對高並發和海量數據系統的有效手段。
3. 分片的方式
數據分片的拆分方式又分為垂直分片和水平分片。
垂直拆分是把不同的表拆到不同的數據庫中,‘而水平拆分是把同一個表拆到不同的數據庫中(或者是把一張表數據拆分成n多個小表)。相對於垂直拆分,水平拆分不是將表的數據做分類,而是按照某個字段的某種規則來分散到多個庫中,每個表中包含一部分數據。簡單來說,我們可以將數據的水平切分理解為是**按照數據行的切分**,就是將表中的某些行切分到一個數據庫,而另外某些行又切分到其他的數據庫中,主要有分表,分庫兩種模式 該方式提高了系統的穩定性跟負載能力,但是跨庫join性能較差。
4:Sharding-JDBC的核心/工原理
Sharding-JDBC數據分片主要流程是由SQL解析 →執行器優化 → SQL路由 →SQL改寫 →SQL執行 →結果歸並的流程組成。
SQL解析
分為詞法解析和語法解析。 先通過詞法解析器將SQL拆分為一個個不可再分的單詞。再使用語法解析器對SQL進行理解,並最終提煉出解析上下文。 解析上下文包括表、選擇項、排序項、分組項、聚合函數、分頁信息、查詢條件以及可能需要修改的占位符的標記。
SQL解析分為兩步, 第一步為 詞法解析, 詞法解析的意思是就是將SQL進行拆分。
例:
select from t_user where id = 1
詞法解析:
[select] [] [from] [t_user] [where] [id=1]
第二步語法解析,語法解析器將SQL轉換為抽象語法樹。
執行器優化
合並和優化分片條件,如OR等。
SQL路由
根據解析上下文匹配用戶配置的分片策略,並生成路由路徑。目前支持分片路由和廣播路由。
舉例說明,如果按照order_id的奇數和偶數進行數據分片,一個單表查詢的SQL如下:
SELECT FROM t_order WHERE order_id IN (1, 2);
那么路由的結果應為:
SELECT FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);
SQL改寫
將SQL改寫為在真實數據庫中可以正確執行的語句,SQL改寫分為正確性改寫和優化改寫。
從一個最簡單的例子開始,若邏輯SQL為:
SELECT order_id FROM t_order WHERE order_id=1;
假設該SQL配置分片鍵order_id,並且order_id=1的情況,將路由至分片表1。那么改寫之后的SQL應該為:
SELECT order_id FROM t_order_1 WHERE order_id=1;
SQL執行
通過多線程執行器異步執行。
結果歸並
將多個執行結果集歸並以便於通過統一的JDBC接口輸出。結果歸並包括流式歸並、內存歸並和使用裝飾者模式的追加歸並這幾種方式。
SharedingJdbc完成數據的讀寫分離
目標
使用sharedingjdbc完成數據庫的分庫分表業務
步驟
1:新建一個springboot工程
2:創建兩個數據庫order1,order2,分別創建t_address表如下:
DROP TABLE IF EXISTS `t_address`;
CREATE TABLE `t_address` (
`id` bigint(20) NOT NULL,
`code` varchar(64) DEFAULT NULL COMMENT '編碼',
`name` varchar(64) DEFAULT NULL COMMENT '名稱',
`pid` varchar(64) NOT NULL DEFAULT '0' COMMENT '父id',
`type` int(11) DEFAULT NULL COMMENT '1國家2省3市4縣區',
`lit` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
**3: 開始整合SpringBoot,**這種方式比較簡單只要加入sharding-jdbc-spring-boot-starter依賴,在application.yml中配置數據源,分片策略即可使用,這種方式簡單,方便。pom.xml
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-namespace</artifactId>
<version>3.0.0</version>
</dependency>
appplication.yml
mybatis:
configuration:
mapUnderscoreToCamelCase: true
spring:
main:
allow-bean-definition-overriding: true
# shardingjdbc分庫分表
sharding:
jdbc:
datasource:
names: ds0,ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/order1?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
username: root
password: root
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/order2?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
username: root
password: root
config:
sharding:
props:
sql.show: true
tables:
t_user: #t_user表【即分庫,又分表】
key-generator-column-name: id # 主鍵
actual-data-nodes: ds${0..1}.t_user${0..1} #數據節點
database-strategy: #分庫策略
inline:
sharding-column: city_id
algorithm-expression: ds${city_id % 2}
table-strategy: #分表策略
inline:
shardingColumn: sex
algorithm-expression: t_user${sex % 2}
t_address: #t_address表【只分庫】
key-generator-column-name: id
actual-data-nodes: ds${0..1}.t_address
database-strategy:
inline:
shardingColumn: lit
algorithm-expression: ds${lit % 2}
4:編寫Vo
package com.itheima.springbootshardingpro.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class AddressVo {
private Long id;
private String code;
private String name;
private String pid;
private Integer type;
private Integer lit;
}
5:編寫Dao
package com.itheima.springbootshardingpro.dao;
import com.itheima.springbootshardingpro.vo.AddressVo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface IndexDao {
@Options(useGeneratedKeys = true)
@Insert("insert into t_address (code,name,pid,type,lit)values(#{code},#{name},#{pid},#{type},#{lit})")
int insertAddress(AddressVo addressVo);
@Select("select * from t_address order by lit")
List<AddressVo> listAddress();
}
6: 編寫controller
package com.itheima.springbootshardingpro.web;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.itheima.springbootshardingpro.dao.IndexDao;
import com.itheima.springbootshardingpro.vo.AddressVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class IndexController {
@Autowired
private IndexDao indexDao;
@PostMapping("/addAddress")
public int addAddress(AddressVo addressVo){
int row = indexDao.insertAddress(addressVo);
return row;
}
@GetMapping("/listAddress")
public PageInfo<AddressVo> listAddress(@RequestParam(required=false,defaultValue="1")Integer pageNum,
@RequestParam(required=false,defaultValue="5")Integer pageSize){
PageHelper.startPage(pageNum,pageSize);
List<AddressVo> list = indexDao.listAddress();
PageInfo<AddressVo> info = new PageInfo<>(list);
return info;
}
}
hardingpro.dao;
import com.itheima.springbootshardingpro.vo.AddressVo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface IndexDao {
@Options(useGeneratedKeys = true)
@Insert(“insert into t_address (code,name,pid,type,lit)values(#{code},#{name},#{pid},#{type},#{lit})”)
int insertAddress(AddressVo addressVo);
@Select(“select * from t_address order by lit”)
List listAddress();
}
**6: 編寫controller**
```java
package com.itheima.springbootshardingpro.web;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.itheima.springbootshardingpro.dao.IndexDao;
import com.itheima.springbootshardingpro.vo.AddressVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class IndexController {
@Autowired
private IndexDao indexDao;
@PostMapping("/addAddress")
public int addAddress(AddressVo addressVo){
int row = indexDao.insertAddress(addressVo);
return row;
}
@GetMapping("/listAddress")
public PageInfo<AddressVo> listAddress(@RequestParam(required=false,defaultValue="1")Integer pageNum,
@RequestParam(required=false,defaultValue="5")Integer pageSize){
PageHelper.startPage(pageNum,pageSize);
List<AddressVo> list = indexDao.listAddress();
PageInfo<AddressVo> info = new PageInfo<>(list);
return info;
}
}