背景
傳統的將數據集中存儲至單一數據節點的解決方案,在性能、可用性和運維成本這三方面已經難於滿足互聯網的海量數據場景。
從性能方面來說,由於關系型數據庫大多采用 B+ 樹類型的索引,在數據量超過閾值的情況下,索引深度的增加也將使得磁盤訪問的 IO 次數增加,進而導致查詢性能的下降;同時,高並發訪問請求也使得集中式數據庫成為系統的最大瓶頸。
從可用性的方面來講,服務化的無狀態型,能夠達到較小成本的隨意擴容,這必然導致系統的最終壓力都落在數據庫之上。而單一的數據節點,或者簡單的主從架構,已經越來越難以承擔。數據庫的可用性,已成為整個系統的關鍵。
從運維成本方面考慮,當一個數據庫實例中的數據達到閾值以上,對於 DBA 的運維壓力就會增大。數據備份和恢復的時間成本都將隨着數據量的大小而愈發不可控。一般來講,單一數據庫實例的數據的閾值在 1TB 之內,是比較合理的范圍。
在傳統的關系型數據庫無法滿足互聯網場景需要的情況下,將數據存儲至原生支持分布式的 NoSQL 的嘗試越來越多。 但 NoSQL 對 SQL 的不兼容性以及生態圈的不完善,使得它們在與關系型數據庫的博弈中始終無法完成致命一擊,而關系型數據庫的地位卻依然不可撼動。
數據分片指按照某個維度將存放在單一數據庫中的數據分散地存放至多個數據庫或表中以達到提升性能瓶頸以及可用性的效果。 數據分片的有效手段是對關系型數據庫進行分庫和分表。分庫和分表均可以有效的避免由數據量超過可承受閾值而產生的查詢瓶頸。 除此之外,分庫還能夠用於有效的分散對數據庫單點的訪問量;分表雖然無法緩解數據庫壓力,但卻能夠提供盡量將分布式事務轉化為本地事務的可能,一旦涉及到跨庫的更新操作,分布式事務往往會使問題變得復雜。 使用多主多從的分片方式,可以有效的避免數據單點,從而提升數據架構的可用性。
通過分庫和分表進行數據的拆分來使得各個表的數據量保持在閾值以下,以及對流量進行疏導應對高訪問量,是應對高並發和海量數據系統的有效手段。 數據分片的拆分方式又分為垂直分片和水平分片。
垂直分片
按照業務拆分的方式稱為垂直分片,又稱為縱向拆分,它的核心理念是專庫專用。 在拆分之前,一個數據庫由多個數據表構成,每個表對應着不同的業務。而拆分之后,則是按照業務將表進行歸類,分布到不同的數據庫中,從而將壓力分散至不同的數據庫。 下圖展示了根據業務需要,將用戶表和訂單表垂直分片到不同的數據庫的方案。
垂直分片
垂直分片往往需要對架構和設計進行調整。通常來講,是來不及應對互聯網業務需求快速變化的;而且,它也並無法真正的解決單點瓶頸。 垂直拆分可以緩解數據量和訪問量帶來的問題,但無法根治。如果垂直拆分之后,表中的數據量依然超過單節點所能承載的閾值,則需要水平分片來進一步處理。
水平分片
水平分片又稱為橫向拆分。 相對於垂直分片,它不再將數據根據業務邏輯分類,而是通過某個字段(或某幾個字段),根據某種規則將數據分散至多個庫或表中,每個分片僅包含數據的一部分。 例如:根據主鍵分片,偶數主鍵的記錄放入 0 庫(或表),奇數主鍵的記錄放入 1 庫(或表),如下圖所示。
水平分片
水平分片從理論上突破了單機數據量處理的瓶頸,並且擴展相對自由,是分庫分表的標准解決方案。
挑戰
雖然數據分片解決了性能、可用性以及單點備份恢復等問題,但分布式的架構在獲得了收益的同時,也引入了新的問題。
面對如此散亂的分庫分表之后的數據,應用開發工程師和數據庫管理員對數據庫的操作變得異常繁重就是其中的重要挑戰之一。他們需要知道數據需要從哪個具體的數據庫的分表中獲取。
另一個挑戰則是,能夠正確的運行在單節點數據庫中的 SQL,在分片之后的數據庫中並不一定能夠正確運行。例如,分表導致表名稱的修改,或者分頁、排序、聚合分組等操作的不正確處理。
跨庫事務也是分布式的數據庫集群要面對的棘手事情。 合理采用分表,可以在降低單表數據量的情況下,盡量使用本地事務,善於使用同庫不同表可有效避免分布式事務帶來的麻煩。 在不能避免跨庫事務的場景,有些業務仍然需要保持事務的一致性。 而基於 XA 的分布式事務由於在並發度高的場景中性能無法滿足需要,並未被互聯網巨頭大規模使用,他們大多采用最終一致性的柔性事務代替強一致事務。
目標
盡量透明化分庫分表所帶來的影響,讓使用方盡量像使用一個數據庫一樣使用水平分片之后的數據庫集群,是 Apache ShardingSphere 數據分片模塊的主要設計目標。
Hello World Demo
以下為一個demo簡單示例。我們將user水平切分為3張表,根據id hash存儲到對應的表中。
准備環境
准備mysql。這里采用docker(https://github.com/Ryan-Miao/docker-china-source/tree/master/docker-mysql)創建一個mysql實例。
git clone https://github.com/Ryan-Miao/docker-china-source.git
cd docker-mysql
sudo docker-compose up
連接mysql並創建表:
show databases;
drop database if exists test0;
drop database if exists test1;
drop database if exists test2;
create database test0;
create database test1;
create database test2;
use test0;
drop table if exists user0;
drop table if exists user1;
drop table if exists user2;
create table user0(
id bigint(11) not null auto_increment primary key ,
name varchar(11) not null default '',
city_name varchar(11) not null default ''
);
create table user1 like user0;
create table user2 like user0;
use test1;
drop table if exists user0;
drop table if exists user1;
drop table if exists user2;
create table user0(
id bigint(11) not null auto_increment primary key ,
name varchar(11) not null default '',
city_name varchar(11) not null default ''
);
create table user1 like user0;
create table user2 like user0;
准備一個Springboot工程
這里簡單創建一個Springboot項目。見https://github.com/Ryan-Miao/sharding-demo
簡單引入web, mysql, mybatis-plus, sharding-sphere
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
啟動類DemoApplication
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan(basePackages = {
"com.example.demo.user.mapper"
})
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
創建entity對象User
/**
* @author Ryan Miao
* @since 2021-06-18
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value = "User對象", description = "")
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String cityName;
}
核心重點配置文件如下application.yml
spring:
shardingsphere:
datasource:
names: test0,test1
test0:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: "jdbc:mysql://localhost:3306/test0?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai"
username: root
password: 123456
test1:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: "jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai"
username: root
password: 123456
sharding:
# 默認分庫策略 根據id分庫, 這里暫且全部默認為test0
default-database-strategy:
inline:
sharding-column: id
algorithm-expression: test0
tables:
user:
# 表為db.table $->{0..2}表示0到2。
actualDataNodes: test$->{0..1}.user$->{0..2}
# 分表策略 根據id分表 3個表
table-strategy:
inline:
shardingColumn: id
algorithmExpression: user$->{id % 3}
# 分布式id生成方案: 雪花算法
keyGenerator:
type: SNOWFLAKE
column: id
props:
sql.show: true
mybatis-plus:
mapper-locations: "classpath*:/mapper/**/*.xml"
創建UserMapper
package com.example.demo.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.user.entity.User;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author Ryan
* @since 2021-06-18
*/
@Component
public interface UserMapper extends BaseMapper<User> {
}
編寫一個Unit Test類, 我們制造一批數據:
package com.example.demo;
import com.example.demo.user.entity.User;
import com.example.demo.user.service.IUserService;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
class DemoApplicationTests {
@Autowired
private IUserService userService;
@Test
void insert() {
List<String> cityList = Arrays.asList("北京市", "深圳市", "重慶市", "上海市", "武漢市", "成都市", "廣州市");
int initialCapacity = 10000;
Random random = new Random();
List<User> list = new ArrayList<>(initialCapacity);
for (int i = 0; i < initialCapacity; i++) {
User user = new User();
user.setName("a_" + i);
int index = random.nextInt(7);
user.setCityName(cityList.get(index));
list.add(user);
}
userService.saveBatch(list);
}
}
數據庫查詢:
接口查詢:
上述demo,基本實現了分表算法:按id hash。插入的時候自動生成分布式id,自動計算路由存儲到對應的表。查詢的時候自動計算路由,返回查詢結果。
注意,上述demo采用inline的分表策略,目前只支持按id=來查詢。復雜的查詢應該走es查詢id,再反查全部。