微服务学习系列14:分库分表ShardingSphere
系列文章目錄
目錄
系列文章目錄
前言
一、什么是 ShardingSphere
二、ShardingSphere-JDBC獨立部署
三、ShardingSphere-Proxy獨立部署
四、混合部署架構
五、數據分片
垂直分片?
水平分片
六、ShardingSphere 基礎知識
表
邏輯表
真實表
?編輯
?????????編輯
七、ShardingSphere-JDBC 入門案例
單庫多表
多庫多表
集成mybatis-plus
引入POM
application.yml 配置?
Order 定義?
OrderMapper 定義?
單元測試?
八、ShardingSphere-JDBC 經典案例
十億用戶的系統
表結構怎么設計?
十億用戶系統的登錄流程
總結
前言
傳統的將數據集中存儲至單一節點的解決方案,在性能、可用性和運維成本這三方面已經難于滿足海量數據的場景。
從性能方面來說,由于關系型數據庫大多采用 B+ 樹類型的索引,在數據量超過閾值的情況下,索引深度的增加也將使得磁盤訪問的 IO 次數增加,進而導致查詢性能的下降; 同時,高并發訪問請求也使得集中式數據庫成為系統的最大瓶頸。
數據分片指按照某個維度將存放在單一數據庫中的數據分散地存放至多個數據庫或表中以達到提升性能瓶頸以及可用性的效果。 數據分片的有效手段是對關系型數據庫進行分庫和分表。分庫和分表均可以有效的避免由數據量超過可承受閾值而產生的查詢瓶頸。 除此之外,分庫還能夠用于有效的分散對數據庫單點的訪問量;分表雖然無法緩解數據庫壓力,但卻能夠提供盡量將分布式事務轉化為本地事務的可能,一旦涉及到跨庫的更新操作,分布式事務往往會使問題變得復雜。 使用多主多從的分片方式,可以有效的避免數據單點,從而提升數據架構的可用性。
一、什么是 ShardingSphere
Apache ShardingSphere 是一款分布式的數據庫生態系統, 可以將任意數據庫轉換為分布式數據庫,并通過數據分片、彈性伸縮、加密等能力對原有數據庫進行增強。
Apache ShardingSphere 設計哲學為 Database Plus,旨在構建異構數據庫上層的標準和生態。 它關注如何充分合理地利用數據庫的計算和存儲能力,而并非實現一個全新的數據庫。 它站在數據庫的上層視角,關注它們之間的協作多于數據庫自身。
https://shardingsphere.apache.org/
二、ShardingSphere-JDBC獨立部署
ShardingSphere-JDBC 定位為輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅動,完全兼容 JDBC 和各種 ORM 框架。
- 適用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC;
- 支持任何第三方的數據庫連接池,如:DBCP, C3P0, BoneCP, HikariCP 等;
- 支持任意實現 JDBC 規范的數據庫,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 訪問的數據庫。
? ?
三、ShardingSphere-Proxy獨立部署
ShardingSphere-Proxy 定位為透明化的數據庫代理端,通過實現數據庫二進制協議,對異構語言提供支持。 目前提供 MySQL 和 PostgreSQL 協議,透明化數據庫操作,對 DBA 更加友好。
- 向應用程序完全透明,可直接當做 MySQL/PostgreSQL 使用;
- 兼容 MariaDB 等基于 MySQL 協議的數據庫,以及 openGauss 等基于 PostgreSQL 協議的數據庫;
- 適用于任何兼容 MySQL/PostgreSQL 協議的的客戶端,如:MySQL Command Client, MySQL Workbench, Navicat 等。
? ?
四、混合部署架構
ShardingSphere-JDBC 采用無中心化架構,與應用程序共享資源,適用于 Java 開發的高性能的輕量級 OLTP 應用; ShardingSphere-Proxy 提供靜態入口以及異構語言的支持,獨立于應用程序部署,適用于 OLAP 應用以及對分片數據庫進行管理和運維的場景。
Apache ShardingSphere 是多接入端共同組成的生態圈。 通過混合使用 ShardingSphere-JDBC 和 ShardingSphere-Proxy,并采用同一注冊中心統一配置分片策略,能夠靈活的搭建適用于各種場景的應用系統,使得架構師更加自由地調整適合于當前業務的最佳系統架構。
五、數據分片
數據分片指按照某個維度將存放在單一數據庫中的數據分散地存放至多個數據庫或表中以達到提升性能瓶頸以及可用性的效果。 數據分片的有效手段是對關系型數據庫進行分庫和分表。分庫和分表均可以有效的避免由數據量超過可承受閾值而產生的查詢瓶頸。 除此之外,分庫還能夠用于有效的分散對數據庫單點的訪問量;分表雖然無法緩解數據庫壓力,但卻能夠提供盡量將分布式事務轉化為本地事務的可能,一旦涉及到跨庫的更新操作,分布式事務往往會使問題變得復雜。 使用多主多從的分片方式,可以有效的避免數據單點,從而提升數據架構的可用性。
通過分庫和分表進行數據的拆分來使得各個表的數據量保持在閾值以下,以及對流量進行疏導應對高訪問量,是應對高并發和海量數據系統的有效手段。 數據分片的拆分方式又分為垂直分片和水平分片。
垂直分片?
按照業務拆分的方式稱為垂直分片,又稱為縱向拆分,它的核心理念是專庫專用。 在拆分之前,一個數據庫由多個數據表構成,每個表對應著不同的業務。而拆分之后,則是按照業務將表進行歸類,分布到不同的數據庫中,從而將壓力分散至不同的數據庫。 下圖展示了根據業務需要,將用戶表和訂單表垂直分片到不同的數據庫的方案。
垂直分片往往需要對架構和設計進行調整。通常來講,是來不及應對互聯網業務需求快速變化的;而且,它也并無法真正的解決單點瓶頸。 垂直拆分可以緩解數據量和訪問量帶來的問題,但無法根治。如果垂直拆分之后,表中的數據量依然超過單節點所能承載的閾值,則需要水平分片來進一步處理。
水平分片
水平分片又稱為橫向拆分。 相對于垂直分片,它不再將數據根據業務邏輯分類,而是通過某個字段(或某幾個字段),根據某種規則將數據分散至多個庫或表中,每個分片僅包含數據的一部分。 例如:根據主鍵分片,偶數主鍵的記錄放入 0 庫(或表),奇數主鍵的記錄放入 1 庫(或表),如下圖所示。
水平分片從理論上突破了單機數據量處理的瓶頸,并且擴展相對自由,是數據分片的標準解決方案。?
六、ShardingSphere 基礎知識
表
表是透明化數據分片的關鍵概念。 Apache ShardingSphere 通過提供多樣化的表類型,適配不同場景下的數據分片需求。
邏輯表
相同結構的水平拆分數據庫(表)的邏輯名稱,是 SQL 中表的邏輯標識。 例:訂單數據根據主鍵尾數拆分為 3?張表,分別是?t_order_0?,t_order_2,?t_order_3,他們的邏輯表名為?t_order。
真實表
在水平拆分的數據庫中真實存在的物理表。 即上個示例中的?t_order_0?,t_order_2,?t_order_3
????????
七、ShardingSphere-JDBC 入門案例
單庫多表
數據庫設計?
數據庫:test
訂單表:t_order_0,t_order_1,t_order_2
CREATE TABLE `t_order_0` (`order_id` bigint(200) NOT NULL,`order_no` varchar(100) DEFAULT NULL,`create_name` varchar(50) DEFAULT NULL,`price` decimal(10,2) DEFAULT NULL,PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `t_order_1` (`order_id` bigint(200) NOT NULL,`order_no` varchar(100) DEFAULT NULL,`create_name` varchar(50) DEFAULT NULL,`price` decimal(10,2) DEFAULT NULL,PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `t_order_2` (`order_id` bigint(200) NOT NULL,`order_no` varchar(100) DEFAULT NULL,`create_name` varchar(50) DEFAULT NULL,`price` decimal(10,2) DEFAULT NULL,PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;application.yml配置
spring:main:allow-bean-definition-overriding: true #當遇到同樣名字的時候,是否允許覆蓋注冊profiles:active: sharding-master-slave server:port: 8808mybatis:mapper-locations: classpath:mapper/*.xmltypeAliasesPackage: org.example.test.entityconfiguration:map-underscore-to-camel-case: true?application-sharding-master-slave.yml配置
spring:shardingsphere:datasource:names: m1 #配置庫的名字,隨意m1: #配置目前m1庫的數據源信息type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?useUnicode=trueusername: rootpassword: 123456sharding:tables:t_order: # 指定t_order表的數據分布情況,配置數據節點actualDataNodes: m1.t_order_$->{0..2}tableStrategy:inline: # 指定t_order表的分片策略,分片策略包括分片鍵和分片算法shardingColumn: order_idalgorithmExpression: t_order_$->{order_id % 3}keyGenerator: # 指定t_order表的主鍵生成策略為SNOWFLAKEtype: SNOWFLAKE #主鍵生成策略為SNOWFLAKEcolumn: order_id #指定主鍵props:sql:show: trueOrder實體類代碼如下
@Data public class Order {private Long orderId;private String orderNo;private String createName;private BigDecimal price; }??OrderMapper
@Mapper public interface OrderMapper {/*** 新增數據*/int insert(Order order);Order queryById(Long orderId); } <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.test.mapper.OrderMapper"><resultMap type="org.example.test.entity.Order" id="orderMap"><result property="orderId" column="order_id" /><result property="orderNo" column="order_no"/><result property="createName" column="create_name"/><result property="price" column="price"/></resultMap><!--查詢單個--><select id="queryById" resultMap="orderMap">selectorder_id, orderNo, createName,pricefrom t_orderwhere id = #{orderId}</select><!--新增所有列--><insert id="insert" keyProperty="orderId" useGeneratedKeys="true">insert into t_order(order_no,create_name, price)values (#{orderNo},#{createName},#{price})</insert> </mapper>OrderTest 單元測試?
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = ExampleWeb.class) public class OrderTest {@Resourceprivate OrderMapper orderMapper;@Testpublic void insert() {for (int i = 1; i <= 30; i++) {Order order = new Order();order.setOrderNo(i + "");order.setCreateName("test" + i);order.setPrice(BigDecimal.valueOf(i));orderMapper.insert(order);System.out.println(JSON.toJSONString(order));}} }多庫多表
application-sharding.yml 配置信息
spring:shardingsphere:datasource:names: ds0,ds1 #配置庫的名字,隨意ds0: #配置目前m1庫的數據源信息type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/ds_0?useUnicode=trueusername: rootpassword: 123456ds1: #配置目前m1庫的數據源信息type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/ds_1?useUnicode=trueusername: rootpassword: 123456sharding:tables:t_order: # 指定t_order表的數據分布情況,配置數據節點actualDataNodes: ds$->{0..1}.t_order_$->{0..3}database-strategy:inline: # 指定t_order表的分片策略,分片策略包括分片鍵和分片算法shardingColumn: order_noalgorithmExpression: ds$->{order_no % 2}tableStrategy:inline: # 指定t_order表的分片策略,分片策略包括分片鍵和分片算法shardingColumn: order_noalgorithmExpression: t_order_$->{order_no % 4}keyGenerator: # 指定t_order表的主鍵生成策略為SNOWFLAKEtype: SNOWFLAKE #主鍵生成策略為SNOWFLAKEcolumn: order_id #指定主鍵props:sql:show: true集成mybatis-plus
引入POM
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>sharding</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.1</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.44</version></dependency><!--添加mybatis分頁插件支持 根據需求可要可不要--><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.2.0</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.11</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3.3</version></dependency><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>sharding-jdbc-spring-boot-starter</artifactId><version>4.1.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.7.9</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.1</version><scope>test</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.14</version></dependency></dependencies> </project>application.yml 配置?
spring:shardingsphere:datasource:names: ds0,ds1 #配置庫的名字,隨意ds0: #配置目前m1庫的數據源信息type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/ds_0?useUnicode=trueusername: rootpassword: 123456ds1: #配置目前m1庫的數據源信息type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/ds_1?useUnicode=trueusername: rootpassword: 123456sharding:tables:myOrder: # 指定t_order表的數據分布情況,配置數據節點actualDataNodes: ds$->{0..1}.t_order_$->{0..3}database-strategy:inline: # 指定t_order表的分片策略,分片策略包括分片鍵和分片算法shardingColumn: order_idalgorithmExpression: ds$->{order_id % 2}tableStrategy:inline: # 指定t_order表的分片策略,分片策略包括分片鍵和分片算法shardingColumn: order_idalgorithmExpression: t_order_$->{order_id % 4}keyGenerator: # 指定t_order表的主鍵生成策略為SNOWFLAKEtype: SNOWFLAKE #主鍵生成策略為SNOWFLAKEcolumn: order_id #指定主鍵props:sql:show: true server:port: 8808Order 定義?
@Data @TableName("myOrder") public class Order {@TableId(type = IdType.ASSIGN_ID)private Long orderId;private Integer orderNo;private String createName;private BigDecimal price; }OrderMapper 定義?
public interface OrderMapper extends BaseMapper<Order> {}單元測試?
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = ExampleWeb.class) public class OrderTest {@Resourceprivate OrderMapper orderMapper;@Testpublic void insert() throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(30);for (int i = 1; i <= 30; i++) {executorService.execute(() -> {Order order = new Order();order.setOrderNo(111);order.setCreateName("test" );order.setPrice(BigDecimal.valueOf(1));orderMapper.insert(order);System.out.println(String.format(">>>>>>>>>>>>>>>>>>>>>>>>>>%s,%s", order.getOrderId(), order.getOrderId() % 4));});}Thread.sleep(10000);} }八、ShardingSphere-JDBC 經典案例
十億用戶的系統
十億用戶的系統,用戶有多種登錄方式,可以使用手機號、賬號、郵箱、昵稱等登錄,這樣的表結構應該怎樣設計?登錄流程大致是怎樣的?
表結構怎么設計?
當出現多種登錄方式的時候,就意味著一個用戶對應的賬號可能會有若干個。現在可能用手機和昵稱登錄,以后就可能用郵箱登錄,甚至將來還可能通過微信、QQ、微博等第三方渠道登錄。
我們最先想到的是每增加一種登錄方式就新增一個字端,
| 字段 | 字段描述 |
| id | Long |
| name | 登錄名 |
| mobile | 手機號碼 |
| 電子郵箱 |
但這樣會產生以下問題:?
當用戶登錄的時候,我們需要根據用戶的登錄類型,先要知道去查找用戶表的哪個字段才可以進行登錄邏輯判斷。例如,用戶登錄用手機號了,我們就要知道去表里查找對應的 mobile?字段去校驗登錄;登錄用郵箱了,我們就要知道去表里查找對應的 email 字段才可以。這樣做,代碼邏輯會很復雜。
再增加一種登錄方式的時候,我們還得給數據庫的表里再增加一個字段,同時還得修改登錄的代碼。這顯然違反了我們設計模式中的開閉原則。而且這種修改很容易造成線上 bug。每增加一種登錄方式,就新增一種流程,成本有點過高了。
我們設計的表,必須易擴展
加記錄比加字段要更容易擴展
因此方案如下:
創建一張用戶登錄 user_login表,專門用來處理登錄。當新增登錄類型的時候,只需要考慮增加一條記錄即可:記錄登錄類型、登錄名稱以及相關密碼,同時有個 user_id 字段,去和用戶表做關聯。
| 字段名稱 | 字段描述 |
| id | Long |
| name | 用戶名 |
| type | 1:登錄名 2:手機號碼 3:郵箱 |
| password | 密碼(多個用戶名可共用一個密碼,也可以存儲token) |
| user_id | 用戶表ID |
用戶表user存儲基礎信息?
| 字段名稱 | 字段描述 |
| id | 用戶ID主鍵 |
| nick_name | 用戶昵稱 |
| user_logo | 用戶頭像 |
| user_names | 存儲和user_id關聯的賬號 |
這樣設計后,很明顯就做到了易擴展。
假如我自己有兩種登錄方式,user_login 表的數據:?
| id | name | type | password | user_id |
| 1 | xiyangyang | 1 | 123456 | 1 |
| 2 | xiyangyang@qq.com | 3 | 123456 | 1 |
用戶表(User)的數據:
| id | nick_name | user_logo | user_names |
| 1 | 喜🐑🐑 | xiyangyang,xiyangyang@qq.com |
十億用戶系統的登錄流程
因為十億用戶,哪怕有百分之一的活躍用戶,也是千萬級別的。所以,在這樣的情況下,必然需要考慮分庫分表。分庫是為了應對高并發,分表則是為了應對大數據。
以 MYSQL 為例, 一般來講,在 4 核CPU/8G 內存/RAID10 的普通硬盤的服務器配置下,一臺 MYSQL 庫能一直可靠運行的可承載壓力是 1000TPS 左右。一張通常的 20 個字段以內的表,能保證查詢性能沒有大的下降的話,可承載的數據量大致是 1000 萬條數據左右。
所以,咱們分表的時候就要盡量控制表數據不超過一千萬條數據。也因此,十億用戶,分表就是分 100 張表。
同時呢,咱們說了,一臺庫大概能承載的可靠運行并發數是 1000TPS 左右。分庫一般來說,100 張表分 10 個庫,每個庫 10 張表,就很綽綽有余了。
好,現在問題來了,分庫分表的策略是什么呢?就是按什么分呢?
一般是按照 user_id 分。假如我們要分 10 個庫 100 張表是吧, 一般來說就是先通過 user_id mod 10去定義好庫,再通過 user_id mod 100 定義好表。
比如 user_id mod 10 = 3,user_id mod 100 = 33,那么這個用戶的數據就被定位到了數據庫 3 中的 33 號表。
注意,這里又來了一個問題。
假設一個 user_id = 100 會怎么樣?user_id ?mod 10 = 0,user_id mod 100 = 0。它會被分在 0 號庫,0 號表。
那如果我想分到 1 號庫,0 號表呢,有對應的 user_id 嗎?是沒有的。為什么呢?
因為當一個 user_id mod 100 = 0 時,這個 user_id mod 10 也一定為 0 。所以,不會存在 1 號庫,0 號表的情況。
所以,我們還需要對庫進行調整,要把庫變成 11 個庫,然后呢,每個庫有 10?張表。原因就是:
庫數和表數之間不能存在公約數,也就是他們需要互質,只有這樣,我們分配數據的時候,才會盡量均勻。
好了,當 user_id 分完之后,你會發現,按照咱們的設計,只能解決 User 表的問題。那登錄在哪里?該怎么辦?
咱們繼續看,前面說了,登錄邏輯是靠 user_login 表來驗證。那 user_login 表數據大,也得分庫分表啊?它怎么分?
其實挺簡單,分庫分表的時候,我們根據 user_login 表的 name 的 Hash 去分。
假設有個用戶的 name 是 abc,然后將這個 abc 進行下 hash,再除以庫的數量。現在是 11 個庫,所以就是 hash(abc) mod 11 這樣得到庫的編號,然后再 hash(abc) mod 100 得到表的編號。
于是,當我們登錄的時候,流程如下:
輸入 abc 和密碼;
驗證出賬號類型;
將信息傳遞給服務器;
服務器在數據庫層之上會有一個路由層,根據 hash(abc) mod 11/100 定位數據庫和表;
查詢 user_login 表驗證。
實際上,很多這種高并發、大數據的登錄,我們根據手頭的資源,雖然依然會使用分庫分表,但是,往往還會采用 ElasticSearch 緩存一些用戶基本信息和用戶數據所在的數據庫和表的地址信息,將他們作為索引,去真正的做相關登錄業務行為。并根據用戶字段的使用熱度,會在登錄時,把一些用戶關鍵字段讀取出來,放到外置的 Redis 緩存中,供以后重用。
這樣做的好處就是,分庫分表我們可以根據資源隨意增加減少,只需要到時候修改下 ElasticSearch 中的索引信息即可。同時,有了 Redis,也能減少后面分庫分表資源的消耗。
總結
參考:
數據分片 :: ShardingSphere
登錄 - Gitee.com
十億用戶的系統!
分庫分表經典15連問_摩杜云論壇
總結
以上是生活随笔為你收集整理的微服务学习系列14:分库分表ShardingSphere的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C语言】——迷宫问题详解
- 下一篇: 【NOI2014】起床困难综合症 位运算