Java實現(xiàn)分庫分表實踐指南
一、為啥要分庫分表
在大型互聯(lián)網(wǎng)系統(tǒng)中,大部分都會選擇mysql作為業(yè)務數(shù)據(jù)存儲。一般來說,mysql單表行數(shù)超過500萬行或者單表容量超過2GB,查詢效率就會隨著數(shù)據(jù)量的增長而下降。這個時候,就需要對表進行拆分。
那么應該怎么拆分呢?
通常有兩種拆分方法,垂直拆分和水平拆分。
先說垂直拆分,這個比較簡單,我們可以把原先的一張表根據(jù)業(yè)務屬性拆分成多張表。比如用戶表user有很多字段,我們可以新建一張用戶屬性表user_profile,把一些不常用的字段都拆分到user_profile表里,再用user_id作為外鍵將兩張表關聯(lián)起來就可以了。
再說水平拆分,水平拆分針對的不是表,而是數(shù)據(jù)。比如訂單表,數(shù)據(jù)量一般都會非常大。我們可以創(chuàng)建多個數(shù)據(jù)庫實例,每個實例上創(chuàng)建多張訂單表,把訂單數(shù)據(jù)相對均勻的分散存儲到這些表里。查詢的時候,根據(jù)分表策略可直接定位到數(shù)據(jù)在哪個表里,可以大大提高查詢效率。
下面講到的都是如何水平拆分。
二、怎么做分庫分表
分庫分表已經(jīng)有一些成熟的解決方案,本文是用ShardingSphere-JDBC框架來實現(xiàn)的。
1.什么是ShardingSphere-JDBC
ShardingSphere-JDBC定義為輕量級Java框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數(shù)據(jù)庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅(qū)動,完全兼容 JDBC 和各種 ORM 框架。
- 適用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC;
- 支持任何第三方的數(shù)據(jù)庫連接池,如:DBCP, C3P0, BoneCP, HikariCP 等;
- 支持任意實現(xiàn) JDBC 規(guī)范的數(shù)據(jù)庫,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 訪問的數(shù)據(jù)庫。

更多詳細內(nèi)容可直接參考:ShardingSphere官方文檔
2.ShardingSphere-JDBC分表實踐
ShardingSphere-JDBC分庫和分表配置類似,下面介紹下分表怎么實現(xiàn)。
(1)先建分表
先在mysql數(shù)據(jù)庫建10張用戶表:tb_user_0到9,建表語句如下,改下表名,執(zhí)行10遍即可:
CREATE TABLE `tb_user_0` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵', `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名', `sex` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性別', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
(2)POM依賴
使用spring boot + mybatis-plus + shardingsphere-jdbc來實現(xiàn),pom主要引入的包配置如下:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.0.0</version>
</dependency>(3)實體類和Mapper代碼
注意,實體類和Mapper只有一個就行,注意這里的tableName注解一定要和后面配置分表策略的邏輯名一致,不然無法匹配路由策略。
@TableName(value = "tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主鍵,注意此處IdType必須是AUTO,不然框架就會自動生成id,分表時生成id的策略就不生效了
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 姓名
*/
@TableField(value = "name")
private String name;
/**
* 性別
*/
@TableField(value = "sex")
private String sex;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name=" + name +
", sex=" + sex +
"}";
}
}public interface UserMapper extends BaseMapper<User> {
}(4)配置數(shù)據(jù)源和分表規(guī)則
我們引入的包是shardingsphere-jdbc-core-spring-boot-starter,直接在application.yml里配置數(shù)據(jù)源和分表規(guī)則就行。
spring:
shardingsphere:
datasource:
# 數(shù)據(jù)源名稱,有幾個數(shù)據(jù)源就寫幾個,如果是分表,就會寫多個
names: db0
# 為每個數(shù)據(jù)源單獨配置,注意這里要跟上面寫的名稱一致
db0:
# 數(shù)據(jù)庫連接池實現(xiàn)類型,這里使用的是Hikari
type: com.zaxxer.hikari.HikariDataSource
# 數(shù)據(jù)庫驅(qū)動類,連接地址,用戶名,密碼等
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/sharding?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
rules:
sharding:
tables:
# 分表的表名,程序中對這張表的操作,都會采用下面的路由方案
tb_user:
# 這里是實際的數(shù)據(jù)節(jié)點信息,要把庫名和表名都寫全,這里也支持使用表達式,比如下面這張$->{0..9}
actual-data-nodes: db0.tb_user_$->{0..9}
# 配置分表策略
table-strategy:
# 這里選擇的標準策略,也可以配置復雜策略,或者也可以用代碼來實現(xiàn)
standard:
# 分片字段,這里是用用戶id作為分片字段
sharding-column: id
# 這里是我們自定義的分片算法名稱,后面會有實現(xiàn)方案
sharding-algorithm-name: user-inline
# 主鍵生成策略
key-generate-strategy:
# 生成主鍵算法的名稱
key-generator-name: snowflake
# 主鍵字段
column: id
# 自定義的主鍵算法
key-generators:
snowflake:
# 使用雪花算法生成主鍵
type: SNOWFLAKE
# 自定義的分表算法
sharding-algorithms:
user-inline:
#使用inline類型實現(xiàn)
type: inline
props:
#分片表達式,用id對10取模,然后分散到10個表中
algorithm-expression: tb_user_$->{id % 10}
props:
# 打印日志,方便我們觀察執(zhí)行的sql語句
sql-show: true(5)寫單測
先測試插入語句,如下插入100條數(shù)據(jù):
@Autowired
private UserMapper userMapper;
@Test
public void insertTest() {
for (int i=0; i<100; i++) {
User user = new User();
user.setName("test" + i);
user.setSex("男");
userMapper.insert(user);
}
}執(zhí)行之后,發(fā)現(xiàn)每張表都有數(shù)據(jù)插入,但是分布并不均勻,這是由雪花算法特性導致的。下圖是tb_user_0表的數(shù)據(jù):

再測試下查詢語句,先測試用id查詢:
@Test
public void selectByIdTest() {
userMapper.selectById(1668501944537858050L);
}查詢sql語句如下圖,從圖中可以看出,根據(jù)id查詢的時候,會自動走分表路由策略,查詢id為1668501944537858050L的數(shù)據(jù),會自動去tb_user_table_0中查找。

再測試一下根據(jù)name字段查詢:
@Test
public void selectByNameTest() {
QueryWrapper<User> qy = new QueryWrapper<>();
qy.eq("name","test1");
userMapper.selectList(qy);
}查詢sql語句如下圖,從圖中可以看出,如果不是根據(jù)分表字段來查詢的話,會自動union所有分表查詢,這樣反而效率會更低。

所以,分庫分表時一定要選擇合適的字段,并且查詢的時候盡量要在查詢條件里先指定分庫分表的字段,這樣可以直接定位到表中,提高查詢效率。
3.ShardingSphere-JDBC自定義分表策略類
ShardingSphere-JDBC可支持多種分片算法,比如標準分片,復合分片等,每種分片算法有多種類型,如行表達式INLINE,時間范圍分片INTERVAL等,上面的例子我們就是用的標準分片行表達式做的。對于一些需要自定義的分片算法,我們可以通過自定義分片算法類來實現(xiàn)。
比如我們還是要實現(xiàn)取模算法,可以自定義一個UserShardingAlgorithm類來實現(xiàn)StandardShardingAlgorithm接口,實現(xiàn)doSharding接口來自定義分片算法,代碼如下:
//分片字段數(shù)據(jù)類型是什么,這里泛型就寫什么
public class UserShardingAlgorithm implements StandardShardingAlgorithm<Long> {
//精確分片算法實現(xiàn),collection是實際表,也就是配置文件里的actual-data-nodes內(nèi)容
//preciseShardingValue對象包括邏輯表名,分表算法的字段和字段值
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
//對分片字段也就是用戶id取模
String suffix = String.valueOf(preciseShardingValue.getValue() % 10);
//遍歷表名,找到符合要求的表,返回即可
for (String tableName : collection) {
if (tableName.endsWith(suffix)) {
return tableName;
}
}
throw new UnsupportedOperationException();
}
//范圍分片,我們暫不支持
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
throw new UnsupportedOperationException();
}
//初始化信息接口
@Override
public void init() {
}
//分片算法類型
@Override
public String getType() {
return "USER_SHARDING";
}
}在配置文件里,我們只需要改一下分片算法部分的配置即可,之前的配置是這樣的:
sharding-algorithms:
user-inline:
type: inline
props:
algorithm-expression: tb_user_$->{id % 10}分片類型改成class_based,也就是自定義類分片算法,配置如下:
sharding-algorithms:
user-inline:
type: class_based # 自定義類分片算法類型
props:
strategy: standard
# 自定義算法類的路徑
algorithmClassName: com.github.learn.sharding.algorithm.UserShardingAlgorithm還是再跑一下上面selectById單測,如下圖,可以順利去tb_user_0中查詢數(shù)據(jù),證明我們自定義的分片算法生效了:

4.主鍵生成策略
ShardingSphere-JDBC提供了兩種內(nèi)置的分布式主鍵生成器,uuid和雪花算法。
uuid:采用UUID.randomUUID()的方式產(chǎn)生分布式主鍵。
雪花算法:
雪花算法是由 Twitter 公布的分布式主鍵生成算法,它能夠保證不同進程主鍵的不重復性,以及相同進程主鍵的有序性。
(1)實現(xiàn)原理
在同一個進程中,它首先是通過時間位保證不重復,如果時間相同則是通過序列位保證。 同時由于時間位是單調(diào)遞增的,且各個服務器如果大體做了時間同步,那么生成的主鍵在分布式環(huán)境可以認為是總體有序的,這就保證了對索引字段的插入的高效性。 例如 MySQL 的 Innodb 存儲引擎的主鍵。
使用雪花算法生成的主鍵,二進制表示形式包含 4 部分,從高位到低位分表為:1bit 符號位、41bit 時間戳位、10bit 工作進程位以及 12bit 序列號位。
- 符號位(1bit)
預留的符號位,恒為零。
- 時間戳位(41bit)
41 位的時間戳可以容納的毫秒數(shù)是 2 的 41 次冪,一年所使用的毫秒數(shù)是:365 * 24 * 60 * 60 * 1000。 通過計算可知:
Math.pow(2,41)/(365*24*60*60*1000L);
結(jié)果約等于 69.73 年。 Apache ShardingSphere 的雪花算法的時間紀元從 2016年11月1日 零點開始,可以使用到 2086 年,相信能滿足絕大部分系統(tǒng)的要求。
- 工作進程位(10bit)
該標志在 Java 進程內(nèi)是唯一的,如果是分布式應用部署應保證每個工作進程的 id 是不同的。 該值默認為 0,可通過屬性設置。
- 序列號位(12bit)
該序列是用來在同一個毫秒內(nèi)生成不同的 ID。如果在這個毫秒內(nèi)生成的數(shù)量超過 4096 (2 的 12 次冪),那么生成器會等待到下個毫秒繼續(xù)生成。
雪花算法主鍵的詳細結(jié)構(gòu)見下圖。

(2)配置信息
在ShardingSphere-JDBC中,雪花算法提供了三個屬性。
worker-id:工作機器唯一標識
max-vibration-offset:最大抖動上限值,范圍[0, 4096)。注:若使用此算法生成值作分片值,建議配置此屬性。此算法在不同毫秒內(nèi)所生成的 key 取模 2^n (2^n一般為分庫或分表數(shù)) 之后結(jié)果總為 0 或 1。為防止上述分片問題,建議將此屬性值配置為 (2^n)-1。如果有10個分表,可將此值設置為9,這樣數(shù)據(jù)分布會更均勻一下。
max-tolerate-time-difference-milliseconds:最大容忍時鐘回退時間,單位:毫秒,默認10毫秒
(3)多節(jié)點worker-id配置
服務器可能是有多個節(jié)點的,此時如果worker-id用同一個配置,有可能會產(chǎn)生重復的id,因此每個節(jié)點的worker-id最好是不同的。我們可以用ip地址的一部分來作為節(jié)點的worker-id,worker-id是十位,我們直接取ip地址的后10位即可,一般都是不會重復的。比如機器的IP為192.168.1.108,二進制表示:11000000 10101000 00000001 01101100,截取最后10位 01 01101100,轉(zhuǎn)為十進制364,設置workerId為364。
實現(xiàn)方式如下:
首先是配置文件,要加入work-id屬性配置:
key-generators:
user-id-generator:
type: SNOWFLAKE
props:
max-vibration-offset: 9
worker-id: ${workerId}然后,加一個配置類,在static代碼塊中獲取ip地址,取后十位,作為worker-id。
@Configuration
public class WorkerIdConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(WorkerIdConfig.class);
static {
try {
InetAddress address = InetAddress.getLocalHost();
// IP地址byte[]數(shù)組形式,這個byte數(shù)組的長度是4,數(shù)組0~3下標對應的值分別是192,168,1,108
byte[] ipAddressByteArray = address.getAddress();
// workerId取ip地址后十位
long workerId = ((ipAddressByteArray[ipAddressByteArray.length - 2] & 0x03) << 8) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF);
LOGGER.info("當前機器 workerId: {}", workerId);
System.setProperty("workerId", String.valueOf(workerId));
} catch (Exception e) {
LOGGER.error("worker id failed:{}", e.getMessage(), e);
}
}
}總結(jié)
到此這篇關于Java實現(xiàn)分庫分表的文章就介紹到這了,更多相關Java分庫分表內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java StringBuilder 實現(xiàn)原理全攻略
StringBuilder 是 Java 提供的可變字符序列類,位于 java.lang 包中,專門用于高效處理字符串的拼接和修改操作,本文給大家介紹Java StringBuilder 實現(xiàn)原理深度解析,感興趣的朋友跟隨小編一起看看吧2025-09-09
spring-redis-session 自定義 key 和過期時間
這篇文章主要介紹了spring-redis-session 自定義 key 和過期時間,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12

