Java實(shí)戰(zhàn):教你如何進(jìn)行數(shù)據(jù)庫(kù)分庫(kù)分表
我們知道,當(dāng)前的應(yīng)用都離不開數(shù)據(jù)庫(kù),隨著數(shù)據(jù)庫(kù)中的數(shù)據(jù)越來越多,單表突破性能上限記錄時(shí),如MySQL單表上線估計(jì)在近千萬(wàn)條內(nèi),當(dāng)記錄數(shù)繼續(xù)增長(zhǎng)時(shí),從性能考慮,則需要進(jìn)行拆分處理。而拆分分為橫向拆分和縱向拆分。一般來說,采用橫向拆分較多,這樣的表結(jié)構(gòu)是一致的,只是不同的數(shù)據(jù)存儲(chǔ)在不同的數(shù)據(jù)庫(kù)表中。其中橫向拆分也分為分庫(kù)和分表。
1.示例數(shù)據(jù)庫(kù)準(zhǔn)備
為了說清楚如何用Java語(yǔ)言和相關(guān)框架實(shí)現(xiàn)業(yè)務(wù)表的分庫(kù)和分表處理。這里首先用MySQL數(shù)據(jù)庫(kù)中創(chuàng)建兩個(gè)獨(dú)立的數(shù)據(jù)庫(kù)實(shí)例,名字為mydb和mydb2,此可演示分庫(kù)操作。另外在每個(gè)數(shù)據(jù)庫(kù)實(shí)例中,創(chuàng)建12個(gè)業(yè)務(wù)表,按年月進(jìn)行數(shù)據(jù)拆分。具體的創(chuàng)建表腳本如下:
- CREATE TABLE `t_bill_2021_1` (
- `order_id` bigint(20) NOT NULL COMMENT '訂單id',
- `user_id` int(20) NOT NULL COMMENT '用戶id',
- `address_id` bigint(20) NOT NULL COMMENT '地址id',
- `status` char(1) DEFAULT NULL COMMENT '訂單狀態(tài)',
- `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
- PRIMARY KEY (`order_id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- CREATE TABLE `t_bill_2021_2` (
- `order_id` bigint(20) NOT NULL COMMENT '訂單id',
- `user_id` int(20) NOT NULL COMMENT '用戶id',
- `address_id` bigint(20) NOT NULL COMMENT '地址id',
- `status` char(1) DEFAULT NULL COMMENT '訂單狀態(tài)',
- `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
- PRIMARY KEY (`order_id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- -- 省略....
- CREATE TABLE `t_bill_2021_12` (
- `order_id` bigint(20) NOT NULL COMMENT '訂單id',
- `user_id` int(20) NOT NULL COMMENT '用戶id',
- `address_id` bigint(20) NOT NULL COMMENT '地址id',
- `status` char(1) DEFAULT NULL COMMENT '訂單狀態(tài)',
- `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
- PRIMARY KEY (`order_id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
成功執(zhí)行腳本后,在MySQL管理工具中可以看到如下的示例界面:
2.分庫(kù)分表實(shí)現(xiàn)
在Java語(yǔ)言下的框架中,有眾多的開源框架,其中關(guān)于分庫(kù)分表的框架,可以選擇Apache ShardingSphere,其官網(wǎng)介紹說:ShardingSphere 是一套開源的分布式數(shù)據(jù)庫(kù)解決方案組成的生態(tài)圈,它由 JDBC、Proxy 和 Sidecar(規(guī)劃中)這 3 款既能夠獨(dú)立部署,又支持混合部署配合使用的產(chǎn)品組成。 它們均提供標(biāo)準(zhǔn)化的數(shù)據(jù)水平擴(kuò)展、分布式事務(wù)和分布式治理等功能,可適用于如 Java 同構(gòu)、異構(gòu)語(yǔ)言、云原生等各種多樣化的應(yīng)用場(chǎng)景。Apache ShardingSphere 5.x 版本開始致力于可插拔架構(gòu)。 目前,數(shù)據(jù)分片、讀寫分離、數(shù)據(jù)加密、影子庫(kù)壓測(cè)等功能,以及 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 與協(xié)議的支持,均通過插件的方式織入項(xiàng)目。官網(wǎng)地址為: https://shardingsphere.apache.org/index_zh.html 。
下面的示例采用Spring Boot框架來實(shí)現(xiàn),相關(guān)的庫(kù)通過Maven進(jìn)行管理。首先給出pom.xml配置文件的定義:
- <?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 https://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.5.3</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.example</groupId>
- <artifactId>wyd</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>wyd</name>
- <description>Demo project for Spring Boot</description>
- <properties>
- <java.version>1.8</java.version>
- <mybatis-plus.version>3.1.1</mybatis-plus.version>
- <sharding-sphere.version>4.0.0-RC2</sharding-sphere.version>
- <shardingsphere.version>5.0.0-beta</shardingsphere.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>2.0.1</version>
- </dependency>
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>${mybatis-plus.version}</version>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>joda-time</groupId>
- <artifactId>joda-time</artifactId>
- <version>2.9.8</version>
- </dependency>
- <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-jdbc-spring-namespace</artifactId>
- <version>${sharding-sphere.version}</version>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.postgresql</groupId>
- <artifactId>postgresql</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
其次,給出一個(gè)實(shí)體類,它對(duì)應(yīng)于上述創(chuàng)建的數(shù)據(jù)庫(kù)表t_bill,其定義如下:
- package com.example.wyd.dao;
- import com.baomidou.mybatisplus.annotation.TableName;
- import lombok.Data;
- import java.util.Date;
- @Data
- @TableName("t_bill")
- public class Bill {
- private Long orderId;
- private Integer userId;
- private Long addressId;
- private String status;
- private Date createTime;
- public void setOrderId(Long orderId) {
- this.orderId = orderId;
- }
- public void setUserId(Integer userId) {
- this.userId = userId;
- }
- public void setAddressId(Long addressId) {
- this.addressId = addressId;
- }
- public void setStatus(String status) {
- this.status = status;
- }
- public void setCreateTime(Date createTime) {
- this.createTime = createTime;
- }
- }
- 映射類BillMapper定義如下:
- package com.example.wyd.mapper;
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import com.example.wyd.dao.Bill;
- public interface BillMapper extends BaseMapper<Bill> {
- }
服務(wù)類接口定義如下:
- package com.example.wyd.service;
- import com.baomidou.mybatisplus.extension.service.IService;
- import com.example.wyd.dao.Bill;
- public interface BillService extends IService<Bill> {
- }
服務(wù)類接口的實(shí)現(xiàn)類定義如下:
- package com.example.wyd.service;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.example.wyd.dao.Bill;
- import com.example.wyd.mapper.BillMapper;
- import org.springframework.stereotype.Service;
- @Service
- public class BillServiceImpl extends ServiceImpl<BillMapper, Bill> implements BillService {
- }
這里我們采用了MybatisPlus框架,它可以很方便的進(jìn)行數(shù)據(jù)庫(kù)相關(guān)操作,而無(wú)需過多寫SQL來實(shí)現(xiàn)具體業(yè)務(wù)邏輯。通過上述定義,通過繼承接口的方式,并提供實(shí)體類的定義,MybatisPlus框架會(huì)通過反射機(jī)制來根據(jù)數(shù)據(jù)庫(kù)設(shè)置來生成SQL語(yǔ)句,其中包含增刪改查接口,具體的實(shí)現(xiàn)我們并未具體定義。
下面定義一個(gè)自定義的分庫(kù)算法,具體實(shí)現(xiàn)如下:
- package com.example.wyd;
- import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
- import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
- import java.util.Collection;
- //自定義數(shù)據(jù)庫(kù)分片算法
- public class DBShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
- @Override
- public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
- //真實(shí)數(shù)據(jù)庫(kù)節(jié)點(diǎn)
- availableTargetNames.stream().forEach((item) -> {
- System.out.println("actual db:" + item);
- });
- //邏輯表以及分片的字段名
- System.out.println("logicTable:"+shardingValue.getLogicTableName()+";shardingColumn:"+ shardingValue.getColumnName());
- //分片數(shù)據(jù)字段值
- System.out.println("shardingColumn value:"+ shardingValue.getValue().toString());
- //獲取字段值
- long orderId = shardingValue.getValue();
- //分片索引計(jì)算 0 , 1
- long db_index = orderId & (2 - 1);
- for (String each : availableTargetNames) {
- if (each.equals("ds"+db_index)) {
- //匹配的話,返回?cái)?shù)據(jù)庫(kù)名
- return each;
- }
- }
- throw new IllegalArgumentException();
- }
- }
下面給出數(shù)據(jù)的分表邏輯,這個(gè)定義稍顯復(fù)雜一點(diǎn),就是根據(jù)業(yè)務(wù)數(shù)據(jù)的日期字段值,根據(jù)月份落入對(duì)應(yīng)的物理數(shù)據(jù)表中。實(shí)現(xiàn)示例代碼如下:
- package com.example.wyd;
- import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
- import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
- import java.util.Collection;
- import java.util.Date;
- //表按日期自定義分片
- public class TableShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
- @Override
- public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
- //真實(shí)數(shù)據(jù)庫(kù)節(jié)點(diǎn)
- availableTargetNames.stream().forEach((item) -> {
- System.out.println("actual db:" + item);
- });
- //邏輯表以及分片的字段名
- System.out.println("logicTable:"+shardingValue.getLogicTableName()+";shardingColumn:"+ shardingValue.getColumnName());
- //分片數(shù)據(jù)字段值
- System.out.println("shardingColumn value:"+ shardingValue.getValue().toString());
- //獲取表名前綴
- String tb_name = shardingValue.getLogicTableName() + "_";
- //根據(jù)日期分表
- Date date = shardingValue.getValue();
- String year = String.format("%tY", date);
- String mon =String.valueOf(Integer.parseInt(String.format("%tm", date)));
- //String dat = String.format("%td", date); //也可以安裝年月日來分表
- // 選擇表
- tb_name = tb_name + year + "_" + mon;
- //實(shí)際的表名
- System.out.println("tb_name:" + tb_name);
- for (String each : availableTargetNames) {
- //System.out.println("availableTableName:" + each);
- if (each.equals(tb_name)) {
- //返回物理表名
- return each;
- }
- }
- throw new IllegalArgumentException();
- }
- }
數(shù)據(jù)的分庫(kù)分表可以在Spring Boot的屬性配置文件中進(jìn)行設(shè)( application.properties ):
- server.port=8080
- #########################################################################################################
- # 配置ds0 和ds1兩個(gè)數(shù)據(jù)源
- spring.shardingsphere.datasource.names = ds0,ds1
- #ds0 配置
- spring.shardingsphere.datasource.ds0.type = com.zaxxer.hikari.HikariDataSource
- spring.shardingsphere.datasource.ds0.driver-class-name = com.mysql.cj.jdbc.Driver
- spring.shardingsphere.datasource.ds0.jdbc-url = jdbc:mysql://127.0.0.1:3306/mydb?characterEncoding=utf8
- spring.shardingsphere.datasource.ds0.username = uname
- spring.shardingsphere.datasource.ds0.password = pwd
- #ds1 配置
- spring.shardingsphere.datasource.ds1.type = com.zaxxer.hikari.HikariDataSource
- spring.shardingsphere.datasource.ds1.driver-class-name = com.mysql.cj.jdbc.Driver
- spring.shardingsphere.datasource.ds1.jdbc-url = jdbc:mysql://127.0.0.1:3306/mydb2characterEncoding=utf8
- spring.shardingsphere.datasource.ds1.username = uname
- spring.shardingsphere.datasource.ds1.password = pwd
- #########################################################################################################
- # 默認(rèn)的分庫(kù)策略:id取模
- spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column = id
- spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression = ds$->{id % 2}
- #########################################################################################################
- spring.shardingsphere.sharding.tables.t_bill.actual-data-nodes=ds$->{0..1}.t_bill_$->{2021..2021}_$->{1..12}
- #數(shù)據(jù)庫(kù)分片字段
- spring.shardingsphere.sharding.tables.t_bill.database-strategy.standard.sharding-column=order_id
- #自定義數(shù)據(jù)庫(kù)分片策略
- spring.shardingsphere.sharding.tables.t_bill.database-strategy.standard.precise-algorithm-class-name=com.example.wyd.DBShardingAlgorithm
- #表分片字段
- spring.shardingsphere.sharding.tables.t_bill.table-strategy.standard.sharding-column=create_time
- #自定義表分片策略
- spring.shardingsphere.sharding.tables.t_bill.table-strategy.standard.precise-algorithm-class-name=com.example.wyd.TableShardingAlgorithm
- #########################################################################################################
- # 使用SNOWFLAKE算法生成主鍵
- spring.shardingsphere.sharding.tables.t_bill.key-generator.column = order_id
- spring.shardingsphere.sharding.tables.t_bill.key-generator.type = SNOWFLAKE
- spring.shardingsphere.sharding.tables.t_bill.key-generator.props.worker.id=123
- #########################################################################################################
- spring.shardingsphere.props.sql.show = true
最后,我們給出一個(gè)定義的Controller類型,來測(cè)試分庫(kù)分表的查詢和保存操作是否正確。HomeController類定義如下:
- package com.example.wyd.controller;
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.example.wyd.dao.Bill;
- import com.example.wyd.service.BillService;
- import org.joda.time.DateTime;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
- import java.text.ParseException;
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.List;
- @RestController
- @RequestMapping("/api")
- public class HomeController {
- @Autowired
- private BillService billService;
- //http://localhost:8080/api/query?start=2021-02-07%2000:00:00&end=2021-03-07%2000:00:00
- @RequestMapping("/query")
- public List<Bill> queryList(@RequestParam("start") String start, @RequestParam("end") String end) {
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- try {
- Date date = sdf.parse(start);
- Date date2 = sdf.parse(end);
- QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
- queryWrapper.ge("create_time",date)
- .and(qw-> qw.le("create_time", date2)).last("limit 1,10");
- List<Bill> billIPage = billService.list(queryWrapper);
- System.out.println(billIPage.size());
- billIPage.forEach(System.out::println);
- return billIPage;
- } catch (ParseException e) {
- e.printStackTrace();
- }
- return null;
- }
- //http://localhost:8080/api/save?userid=999&addressId=999&status=M&date=2021-03-07%2000:00:00
- @RequestMapping("/save")
- public String Save(@RequestParam("userid") int userId, @RequestParam("addressId") long AddressId,
- @RequestParam("status") String status
- ,@RequestParam("date") String strDate) {
- String ret ="0";
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- try {
- Date date = sdf.parse(strDate);
- Bill bill = new Bill();
- bill.setUserId(userId);
- bill.setAddressId(AddressId);
- bill.setStatus(status);
- bill.setCreateTime(date);
- boolean isOk = billService.save(bill);
- if (isOk){
- ret ="1";
- }
- } catch (ParseException e) {
- e.printStackTrace();
- }
- return ret;
- }
- }
至此,我們可以用測(cè)試類初始化一些數(shù)據(jù),并做一些初步的數(shù)據(jù)操作測(cè)試:
- package com.example.wyd;
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.example.wyd.dao.Bill;
- import com.example.wyd.dao.Order;
- import com.example.wyd.service.BillService;
- import com.example.wyd.service.OrderService;
- import org.joda.time.DateTime;
- import org.junit.jupiter.api.Test;
- import org.springframework.beans.factory.annotation.Autowired;
- import java.text.ParseException;
- import java.text.SimpleDateFormat;
- import java.util.*;
- public class OrderServiceImplTest extends WydApplicationTests {
- @Autowired
- private BillService billService;
- @Test
- public void testBillSave(){
- for (int i = 0 ; i< 120 ; i++){
- Bill bill = new Bill();
- bill.setUserId(i);
- bill.setAddressId((long)i);
- bill.setStatus("K");
- bill.setCreateTime((new Date(new DateTime(2021,(i % 11)+1,7,00, 00,00,000).getMillis())));
- billService.save(bill);
- }
- }
- @Test
- public void testGetByOrderId(){
- long id = 626038622575374337L; //根據(jù)數(shù)據(jù)修改,無(wú)數(shù)據(jù)會(huì)報(bào)錯(cuò)
- QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
- queryWrapper.eq("order_id", id);
- Bill bill = billService.getOne(queryWrapper);
- System.out.println(bill.toString());
- }
- @Test
- public void testGetByDate(){
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- try {
- Date date = sdf.parse("2021-02-07 00:00:00");
- QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
- queryWrapper.eq("create_time",date);
- List<Bill> billIPage = billService.list(queryWrapper);
- System.out.println(billIPage.size());
- System.out.println(billIPage.toString());
- } catch (ParseException e) {
- e.printStackTrace();
- }
- }
- @Test
- public void testGetByDate2(){
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- try {
- Date date = sdf.parse("2021-02-07 00:00:00");
- Date date2 = sdf.parse("2021-03-07 00:00:00");
- QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
- queryWrapper.ge("create_time",date)
- .and(qw-> qw.le("create_time", date2));
- List<Bill> billIPage = billService.list(queryWrapper);
- System.out.println(billIPage.size());
- billIPage.forEach(System.out::println);
- } catch (ParseException e) {
- e.printStackTrace();
- }
- }
- }
執(zhí)行上述測(cè)試,通過后會(huì)生成測(cè)試數(shù)據(jù)。
3.驗(yàn)證
打開瀏覽器,輸入網(wǎng)址進(jìn)行查詢測(cè)試:http://localhost:8080/api/query?start=2021-02-07%2000:00:00&end=2021-03-07%2000:00:00
輸入如下網(wǎng)址進(jìn)行數(shù)據(jù)新增測(cè)試:http://localhost:8080/api/save?userid=999&addressId=999&status=M&date=2021-03-07%2000:00:00
通過跟蹤分析,此數(shù)據(jù)落入如下的表中,SQL語(yǔ)句如下:
- SELECT * FROM mydb2.t_bill_2021_3 LIMIT 0, 1000
這里還需要注意, ShardingSphere 還支持分布式事務(wù) ,感興趣的可以閱讀官網(wǎng)相關(guān)資料進(jìn)行學(xué)習(xí)。