說明
- 頂頂大名的分庫分表中間件,廢話不多說,官網(wǎng)地址:https://shardingsphere.apache.org/
- 本文中數(shù)據(jù)庫用的是mysql5.7,并且實現(xiàn)了一主一從。
- 場景是訂單表的分表,并且要支持只根據(jù)user_id進行查詢的場景,所以要將用戶的標(biāo)識信息放到主鍵order_id中,這樣才能既能只根據(jù)主鍵order_id進行查詢,又能只根據(jù)user_id進行查詢。
- 順便支持一下主從分離,這個比較簡單,加一下配置即可
- 完整代碼地址在結(jié)尾!!
官方簡介
- 定位為輕量級Java框架,在Java的JDBC層提供的額外服務(wù)。 它使用客戶端直連數(shù)據(jù)庫,以jar包形式提供服務(wù),無需額外部署和依賴,可理解為增強版的JDBC驅(qū)動,完全兼容JDBC和各種ORM框架。
- 適用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
- 支持任何第三方的數(shù)據(jù)庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
- 支持任意實現(xiàn)JDBC規(guī)范的數(shù)據(jù)庫。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92標(biāo)準(zhǔn)的數(shù)據(jù)庫。
image.png
第一步,在pom.xml加入依賴,如下
<!-- MySQL驅(qū)動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatisPlus 核心庫 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- sharding-jdbc -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<!-- hutool工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.5</version>
</dependency>
注:
- 本文的ORM框架使用的是MyBatis-Plus。
- hutool工具用于生成自定義id。
第二步,在application.yml配置shardingsphere,mybatis-plus相關(guān)配置
spring:
application:
name: shardingjdbc-demo-server
shardingsphere:
datasource:
# 數(shù)據(jù)源
names: master,salve
master:
driver-class-name: com.mysql.cj.jdbc.Driver
password: root
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://xxx:3306/db1
username: root
salve:
driver-class-name: com.mysql.cj.jdbc.Driver
password: root
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://xxx:3306/db2
username: root
sharding:
# 主從分離
master-slave-rules:
master:
master-data-source-name: master
slave-data-source-names: salve
# 表分片
tables:
my_order:
# 主表分片規(guī)則表名
actual-data-nodes: master.my_order_$->{0..3}
# 主鍵策略
# key-generator:
# column: id
# type: MyShardingKeyGenerator
table-strategy:
# 行表達式分片
# inline:
# algorithm-expression: order_$->{id.longValue() % 4}
# sharding-column: id
# 標(biāo)準(zhǔn)分片
# standard:
# sharding-column: id
# 指定自定義分片算法類的全路徑
# precise-algorithm-class-name: com.jinhx.shardingjdbc.config.MyPreciseShardingAlgorithm
# 復(fù)合分片
complex:
# 分片鍵
sharding-columns: order_id,user_id
# 指定自定義分片算法類的全路徑
algorithm-class-name: com.jinhx.shardingjdbc.config.MyComplexKeysShardingAlgorithm
# defaultTableStrategy:
# 打開sql控制臺輸出日志
props:
sql:
show: true
# mybatis-plus相關(guān)配置
mybatis-plus:
# xml掃描,多個目錄用逗號或者分號分隔(告訴 Mapper 所對應(yīng)的 XML 文件位置)
mapper-locations: classpath:com/jinhx/shardingjdbc/mapper/xml/*.xml
# 別名包掃描路徑,通過該屬性可以給包中的類注冊別名
type-aliases-package: com.jinhx.shardingjdbc.entity
configuration:
# 不開啟二級緩存
cache-enabled: false
# 是否開啟自動駝峰命名規(guī)則映射:從數(shù)據(jù)庫列名到Java屬性駝峰命名的類似映射
map-underscore-to-camel-case: true
# 如果查詢結(jié)果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段
call-setters-on-nulls: true
# 這個配置會將執(zhí)行的sql打印出來,在開發(fā)或測試的時候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8093
第三步,在主庫創(chuàng)建數(shù)據(jù)庫db1,創(chuàng)建訂單表,如下
說明
- 本文不進行分庫,只分4個表,分別為my_order_0,my_order_1,my_order_2,my_order_3
sql
CREATE DATABASE db1;
use db1;
create table my_order_0
(
order_id bigint not null comment '訂單id主鍵'
primary key,
user_id bigint not null comment '用戶id',
money bigint not null comment '金額'
)
comment '用戶訂單表';
其他表結(jié)構(gòu)一致,此處省略
第四步,創(chuàng)建表操作相應(yīng)類
- 使用mybatis-plus的代碼生成器對數(shù)據(jù)庫的表生成相應(yīng)的類,不懂的請參考另外一篇文章-SpringBoot 2.2.5 整合MyBatis-Plus 3.3.1 教程,配置多數(shù)據(jù)源并支持事務(wù),附帶代碼生成器使用教程
- 手動創(chuàng)建,包括Order,IOrderService,OrderServiceImpl等,如下
Order
package com.jinhx.shardingjdbc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jinhx.shardingjdbc.util.SnowFlakeUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Objects;
/**
* Order
*
* @author jinhx
* @since 2021-07-27
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("my_order")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分表的數(shù)量,一定要2的n次方
*/
public static final int TABLE_COUNT = 4;
/**
* 訂單id主鍵
*/
@TableId(type = IdType.INPUT)
private Long orderId;
/**
* 用戶id
*/
private Long userId;
/**
* 金額
*/
private Long money;
public void buildOrderId(){
if (Objects.isNull(this.userId)){
throw new RuntimeException("userId為空,無法生成orderId");
}
this.orderId = SnowFlakeUtil.getSnowflakeId(SnowFlakeUtil.getDataCenterId(this.userId) & (TABLE_COUNT - 1));
}
public void buildUserId(Integer dataCenterId){
if (Objects.isNull(dataCenterId)){
throw new RuntimeException("dataCenterId為空,無法生成userId");
}
this.userId = SnowFlakeUtil.getSnowflakeId(dataCenterId & (TABLE_COUNT - 1));
}
}
IOrderService
package com.jinhx.shardingjdbc.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jinhx.shardingjdbc.entity.Order;
import java.util.List;
/**
* IOrderService
*
* @author jinhx
* @since 2021-07-27
*/
public interface IOrderService extends IService<Order> {
/**
* 根據(jù)orderIds查詢
*
* @param orderIds orderIds
* @return List<Order>
*/
List<Order> selectByOrderIds(List<Long> orderIds);
/**
* 根據(jù)userIds查詢
*
* @param userIds userIds
* @return List<Order>
*/
List<Order> selectByUserIds(List<Long> userIds);
/**
* 批量插入
*
* @param orders orders
*/
void insertOrders(List<Order> orders);
}
OrderServiceImpl
package com.jinhx.shardingjdbc.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jinhx.shardingjdbc.entity.Order;
import com.jinhx.shardingjdbc.mapper.OrderMapper;
import com.jinhx.shardingjdbc.service.IOrderService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* OrderServiceImpl
*
* @author jinhx
* @since 2021-07-27
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
/**
* 根據(jù)orderIds查詢
*
* @param orderIds orderIds
* @return List<Order>
*/
@Override
public List<Order> selectByOrderIds(List<Long> orderIds) {
return baseMapper.selectBatchIds(orderIds);
}
/**
* 根據(jù)userIds查詢
*
* @param userIds userIds
* @return List<Order>
*/
@Override
public List<Order> selectByUserIds(List<Long> userIds) {
return baseMapper.selectList(new LambdaQueryWrapper<Order>()
.in(CollectionUtils.isNotEmpty(userIds), Order::getUserId, userIds));
}
/**
* 批量插入
*
* @param orders orders
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void insertOrders(List<Order> orders) {
if (CollectionUtils.isNotEmpty(orders)){
if (orders.stream().mapToInt(item -> baseMapper.insert(item)).sum() != orders.size()){
log.error("批量插入order表失敗 orders={}" + orders);
throw new RuntimeException("批量插入order表失敗");
}
}
}
}
第五步,配置MybatisPlus,如下
1. 在啟動類MybatisplusApplication新增@MapperScan注解,里面寫入生成的文件中的mapper存放的路徑,用于掃描mapper文件。
package com.jinhx.shardingjdbc;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.jinhx.shardingjdbc.mapper")
@SpringBootApplication
public class ShardingjdbcApplication {
public static void main(String[] args) {
SpringApplication.run(ShardingjdbcApplication.class, args);
}
}
2. 創(chuàng)建MybatisPlus配置類,MybatisPlusConfig,主要是配置一些插件的使用,此步可省略
package com.jinhx.shardingjdbc.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Mybatis-Plus配置類
*
* @author jinhx
* @since 2021-07-27
*/
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {
/**
* mybatis-plus SQL執(zhí)行效率插件【生產(chǎn)環(huán)境可以關(guān)閉】,設(shè)置 dev test 環(huán)境開啟
*/
// @Bean
// @Profile({"dev", "qa"})
// public PerformanceInterceptor performanceInterceptor() {
// PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
// performanceInterceptor.setMaxTime(1000);
// performanceInterceptor.setFormat(true);
// return performanceInterceptor;
// }
/**
* 分頁插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 設(shè)置請求的頁面大于最大頁后操作, true調(diào)回到首頁,false 繼續(xù)請求 默認(rèn)false
paginationInterceptor.setOverflow(false);
// 設(shè)置最大單頁限制數(shù)量,默認(rèn) 500 條,-1 不受限制
paginationInterceptor.setLimit(500);
// 開啟 count 的 join 優(yōu)化,只針對部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
第六步,創(chuàng)建自定義復(fù)合分片算法類MyComplexKeysShardingAlgorithm,注意自己替換application.yml里面的全路徑
package com.jinhx.shardingjdbc.config;
import com.jinhx.shardingjdbc.util.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 配置Sharding-JDBC復(fù)合分片算法
* 根據(jù)id和age計算,來確定是路由到那個表中
* 目前處理 = 和 in 操作,其余的操作,比如 >、< 等范圍操作均不支持。
*
* @author jinhx
* @since 2021-07-27
*/
@Slf4j
public class MyComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
/**
* orderId
*/
private static final String COLUMN_ORDER_ID = "order_id";
/**
* userId
*/
private static final String COLUMN_USER_ID = "user_id";
/**
* 重寫復(fù)合分片算法
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
if (!shardingValue.getColumnNameAndRangeValuesMap().isEmpty()) {
throw new RuntimeException("條件全部為空,無法路由到具體的表,暫時不支持范圍查詢");
}
// 獲取orderId
Collection<Long> orderIds = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_ORDER_ID, new ArrayList<>(1));
// 獲取userId
Collection<Long> userIds = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_USER_ID, new ArrayList<>(1));
if (CollectionUtils.isEmpty(orderIds) && CollectionUtils.isEmpty(userIds)) {
throw new RuntimeException("orderId,userId字段同時為空,無法路由到具體的表,暫時不支持范圍查詢");
}
// 獲取最終要查詢的表后綴序號的集合,入?yún)㈨樞虿荒茴嵉? List<Integer> tableNos = getTableNoList(orderIds, userIds);
return tableNos.stream()
// 對可用的表數(shù)量求余數(shù),獲取到真實的表的后綴
// .map(idSuffix -> String.valueOf(idSuffix % availableTargetNames.size()))
// 拼接獲取到真實的表
.map(tableSuffix -> availableTargetNames.stream().filter(targetName -> targetName.endsWith(String.valueOf(tableSuffix))).findFirst().orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 獲取最終要查詢的表后綴序號的集合
*
* @param orderIds orderId字段集合
* @param userIds userId字段集合
* @return 最終要查詢的表后綴序號的集合
*/
private List<Integer> getTableNoList(Collection<Long> orderIds, Collection<Long> userIds) {
List<Integer> result = new ArrayList<>();
if (CollectionUtils.isNotEmpty(orderIds)){
// 獲取表位信息
result.addAll(orderIds.stream()
.filter(item -> Objects.nonNull(item) && item > 0)
.map(item -> (int) SnowFlakeUtil.getDataCenterId(item))
.collect(Collectors.toList()));
}
if (CollectionUtils.isNotEmpty(userIds)) {
// 獲取表位信息
result.addAll(userIds.stream().filter(item -> Objects.nonNull(item) && item > 0)
.map(item -> (int) SnowFlakeUtil.getDataCenterId(item))
.collect(Collectors.toList()));
}
if (CollectionUtils.isNotEmpty(result)) {
log.info("SharingJDBC解析路由表后綴成功 redEnvelopeIds={} uids={} 路由表后綴列表={}", orderIds, userIds, result);
// 合并去重
return result.stream().distinct().collect(Collectors.toList());
}
log.error("SharingJDBC解析路由表后綴失敗 redEnvelopeIds={} uids={}", orderIds, userIds);
throw new RuntimeException("orderId,userId解析路由表后綴為空,無法路由到具體的表,暫時不支持范圍查詢");
}
}
第七步,編寫單元測試類,ShardingjdbcApplicationTests,并進行測試
測試步驟
- 先運行insertOrdersTest方法向數(shù)據(jù)庫插入數(shù)據(jù),跑完分別查看四個表,是否都有數(shù)據(jù),且理論上來說應(yīng)該是數(shù)據(jù)均勻
- 分別從4個表里面隨機撈幾條數(shù)據(jù)的order_id出來,然后運行selectByOrderIdsTest,看是否都能查出數(shù)據(jù),此步驟是為了驗證只根據(jù)order_id進行路由查詢是否正常
- 分別從4個表里面隨機撈幾條數(shù)據(jù)的user_id出來,然后運行selectByUserIdsTest,看是否都能查出數(shù)據(jù),此步驟是為了驗證只根據(jù)user_id進行路由查詢是否正常
ShardingjdbcApplicationTests
package com.jinhx.shardingjdbc;
import com.jinhx.shardingjdbc.entity.Order;
import com.jinhx.shardingjdbc.service.IOrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@Slf4j
// 獲取啟動類,加載配置,確定裝載 Spring 程序的裝載方法,它回去尋找 主配置啟動類(被 @SpringBootApplication 注解的)
@SpringBootTest
class ShardingjdbcApplicationTests {
@Autowired
private IOrderService iOrderService;
@Test
void selectByOrderIdsTest() {
List<Long> orderIds = Lists.newArrayList(1443844581547311109L, 1443844581547442181L, 1443844581547573255L, 1443844581547704327L);
log.info(iOrderService.selectByOrderIds(orderIds).toString());
}
@Test
void selectByUserIdsTest() {
List<Long> userIds = Lists.newArrayList(1443844581547311108L, 1443844581547311106L, 1443844581547442180L, 1443844581547704326L);
log.info(iOrderService.selectByUserIds(userIds).toString());
}
@Test
void insertOrdersTest() {
List<Order> orders = Lists.newArrayList();
for (int i = 1;i < 100;i++){
Order order = new Order();
order.buildUserId(i);
order.setMoney(i * 1000L);
order.buildOrderId();
orders.add(order);
}
log.info("orders={}", orders);
iOrderService.insertOrders(orders);
}
@BeforeEach
void testBefore(){
log.info("測試開始!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
}
@AfterEach
void testAfter(){
log.info("測試結(jié)束!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
}
}