shardingsphere 4.1.1多主多从集成dynamic多数据源与mybatis-plus
shardingsphere 4.1.1多主多从集成dynamic多数据源与mybatis-plus
最近接到一个需求,在一个老旧的项目中,需要集成读写分离框架,并且后续需要考虑扩展到分库存储,基于以上考虑,我将技术框架锁定在了shardingsphere
一、简介
Shardingsphere的源码地址:https://github.com/apache/shardingsphere/tree/4.1.1
Shardingsphere官网地址:https://shardingsphere.apache.org/
Apache ShardingSphere 定位为 Database Plus,旨在构建异构数据库之上的标准层和生态系统。它侧重于如何重用现有的数据库及其各自的上层,而不是创建一个新的数据库。目标是最小化或消除底层数据库碎片化带来的挑战
二、业务背景
我们的项目使用到了dynamic-datasource-spring-boot-starter实现动态数据源切换功能,我们的这个项目需要按分类分别调用不同的数据库,并且每个数据库还新增了一个从库,后续可能会增加从库来提升读的效率
我们的实际业务一次请求只需要切换一次数据源,所以重写了源框架获取数据源的操作
三、实现业务需求代码
1、创建一个springboot项目,并且配置对应的maven配置
<!-- springboot 项目配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis-plus相关配置-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.0</version>
</dependency>
<!-- 自动生成代码工具-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.realityforge.org.jetbrains.annotations</groupId>
<artifactId>org.jetbrains.annotations</artifactId>
<version>1.7.0</version>
</dependency>
<!-- alibaba数据源配置-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<!--shardingsphere版本配置 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<!--mysql连接配置 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--多数据源切换配置 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
2、配置数据源与sharding
spring:
port: 9090 #端口号
application:
name: shardingdemo #app名称
datasource:
dynamic:
strict: true #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
datasource:
master:
type: com.alibaba.druid.pool.DruidDataSource # 连接池
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://数据库IP:端口/数据库名(普通形式数据库) #用于不做读写分离与分库分表的数据库
username: root
password: hiTKKihv5HU6v9
shardingsphere:
datasource:
names: miningpoolmaster,miningpoolslave,miningprofitmaster,miningprofitslave # 数据源名字
miningpoolmaster:
type: com.alibaba.druid.pool.DruidDataSource # 连接池
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://数据库IP:端口/数据库名(master1) # 主库地址
username: root # 主库用户名
password: hiTKKihv5HU6v9 # 主库密码
miningpoolslave:
type: com.alibaba.druid.pool.DruidDataSource # 连接池
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://数据库IP:端口/数据库名(slave1) # 从库地址
username: root
password: hiTKKihv5HU6v9
miningprofitmaster:
type: com.alibaba.druid.pool.DruidDataSource # 连接池
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://数据库IP:端口/数据库名(master2)
username: root
password: hiTKKihv5HU6v9
miningprofitslave:
type: com.alibaba.druid.pool.DruidDataSource # 连接池
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://数据库IP:端口/数据库名(slave2) # 从库地址
username: root
password: hiTKKihv5HU6v9
sharding:
#配置数据源的读写分离,但是数据库一定要做主从复制
master-slave-rules:
# 配置主从名称,可以任意取名字
ms0:
master-data-source-name: miningpoolmaster
slave-data-source-names: miningpoolslave
load-balance-algorithm-type: round_robin
ms1:
master-data-source-name: miningprofitmaster
slave-data-source-names: miningprofitslave
load-balance-algorithm-type: round_robin
default-data-source-name: ms0
tables:
help_center_menu:
# logic-lable: help_center_menu
actual-data-nodes: ms0.help_center_menu
# database-strategy:
# hint:
# algorithm-class-name: com.example.shardingdemo.config.HintShardingKeyAlgorithm
props:
sql:
show: true
1、解析配置:
spring.datasource.dynamic.strict:若配置为true,需要注意下面的datasource里面的默认数据源必须为master,否则会抛异常
解决办法:
1、datasource:
master:
2、dynamic.primary: 指定默认数据源
2、sharding配置
- 首先将多个数据源配置好:miningpoolmaster,miningpoolslave,miningprofitmaster,miningprofitslave
- 建议取别名,好像用_连接的时候会报错
- 配置读写分离配置
- 注意:将一组读写配置取一个组名,用于后续切换数据源操作
- 设置默认数据源,可以设置为上面的组名
- 配置需要路由的库,由于我的业务逻辑,需要按不同的表路由到指定的数据源中,并且每个数据源中的库表结构不一样,所以需要采用表路由方式将数据库进行与表绑定(适用与表不多的情况)
3、动态数据源配置
通过翻看Dynamic源码可知,它的主要配置类:DynamicDataSourceAutoConfiguration,所以我们可以进行如下的扩展:
package com.example.shardingdemo.config;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.example.shardingdemo.utils.SpringUtil;
import org.apache.shardingsphere.core.yaml.config.masterslave.YamlMasterSlaveRuleConfiguration;
import org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.MasterSlaveDataSource;
import org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.metadata.MasterSlaveDatabaseMetaData;
import org.apache.shardingsphere.shardingjdbc.spring.boot.sharding.SpringBootShardingRuleConfigurationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Map;
/**
* 动态数据源配置:
*
* 使用{@link com.baomidou.dynamic.datasource.annotation.DS}注解,切换数据源
*
* <code>@DS(DataSourceConfiguration.SHARDING_DATA_SOURCE_NAME)</code>
*
* @author wanping
* @date 2022/4/25 上午10:36
*/
@Configuration
@AutoConfigureBefore({DynamicDataSourceAutoConfiguration.class, SpringBootConfiguration.class})
public class DataSourceConfiguration {
/**
* 分表数据源名称
*/
public static final String SHARDING_DATA_SOURCE_NAME = "sharding";
/**
* 动态数据源配置项
*/
@Autowired
private DynamicDataSourceProperties dynamicDataSourceProperties;
/**
* shardingjdbc有四种数据源,需要根据业务注入不同的数据源
*
* <p>1. 未使用分片, 脱敏的名称(默认): shardingDataSource;
* <p>2. 主从数据源: masterSlaveDataSource;
* <p>3. 脱敏数据源:encryptDataSource;
* <p>4. 影子数据源:shadowDataSource
*
* shardingjdbc默认就是shardingDataSource
* 如果需要设置其他的可以使用
* @Resource(value="") 设置
*/
@Lazy
@Resource
DataSource shardingDataSource;
/**
* 将shardingDataSource放到了多数据源(dataSourceMap)中
* 注意有个版本的bug,3.1.1版本 不会进入loadDataSources 方法,这样就一直造成数据源注册失败
*/
@Bean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = dynamicDataSourceProperties.getDatasource();
return new AbstractDataSourceProvider() {
@Override
public Map<String, DataSource> loadDataSources() {
Map<String, DataSource> dataSourceMap = createDataSourceMap(datasourceMap);
// 将 shardingjdbc 管理的数据源也交给动态数据源管
dataSourceMap.put(SHARDING_DATA_SOURCE_NAME, shardingDataSource);
return dataSourceMap;
}
};
}
/**
* 将动态数据源设置为首选的
* 当spring存在多个数据源时, 自动注入的是首选的对象
* 设置为主要的数据源之后,就可以支持shardingjdbc原生的配置方式了
*
* @return
*/
@Primary
@Bean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(dynamicDataSourceProperties.getPrimary());
dataSource.setStrict(dynamicDataSourceProperties.getStrict());
dataSource.setStrategy(dynamicDataSourceProperties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(dynamicDataSourceProperties.getP6spy());
dataSource.setSeata(dynamicDataSourceProperties.getSeata());
return dataSource;
}
}
4、编写测试案例
1、dao层
通过生成工具生成dao与entity
HelpCenterMenu:
package com.example.shardingdemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 帮助中心菜单表
* </p>
*
* @author wanping
* @since 2022-04-24
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class HelpCenterMenu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 菜单主键
*/
@TableId(value = "menu_id", type = IdType.AUTO)
private Long menuId;
/**
* 菜单名称
*/
private String menuNm;
/**
* 菜单图片链接
*/
private String menuImgUrl;
/**
* 菜单父Id
*/
private Long pId;
/**
* 排序
*/
private Integer sequence;
/**
* 是否禁用
*/
@TableField("is_enable")
private Boolean enable;
/**
* 语言类型(中文:Chinese,英文:English)
*/
private String languageType;
/**
* 创建时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date updateTime;
/**
* 菜单级别
*/
private Integer menuLevel;
public static final String MENU_ID = "menu_id";
public static final String MENU_NM = "menu_nm";
public static final String MENU_IMG_URL = "menu_img_url";
public static final String P_ID = "p_id";
public static final String SEQUENCE = "sequence";
public static final String IS_ENABLE = "is_enable";
public static final String LANGUAGE_TYPE = "language_type";
public static final String CREATE_TIME = "create_time";
public static final String UPDATE_TIME = "update_time";
public static final String MENU_LEVEL = "menu_level";
}
HelpCenterMenuMapper:
package com.example.shardingdemo.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.shardingdemo.entity.HelpCenterMenu;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* 帮助中心菜单表 Mapper 接口
* </p>
*
* @author wanping
* @since 2022-04-24
*/
@Mapper
public interface HelpCenterMenuMapper extends BaseMapper<HelpCenterMenu> {
}
HelpCenterMenuService:
package com.example.shardingdemo.service;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.example.shardingdemo.dao.HelpCenterMenuMapper;
import com.example.shardingdemo.entity.HelpCenterMenu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@DS("sharding")
public class HelpCenterMenuService {
@Autowired
private HelpCenterMenuMapper helpCenterMenuMapper;
public List<HelpCenterMenu> selectList(){
return helpCenterMenuMapper.selectList(null);
}
public int insert(HelpCenterMenu helpCenterMenu){
return helpCenterMenuMapper.insert(helpCenterMenu);
}
}
注意:由于咋们的动态数据源默认选的是master,所以这里需要指定sharding数据源
ShardingdemoApplicationTests:
- 测试写入
@Test
void insertMenus(){
HelpCenterMenu helpCenterMenu = new HelpCenterMenu();
helpCenterMenu.setMenuId(IdWorker.getId());
helpCenterMenu.setMenuNm("测试00005");
helpCenterMenu.setSequence(10);
helpCenterMenu.setEnable(false);
helpCenterMenu.setLanguageType("Chinese");
helpCenterMenu.setCreateTime(new Date(System.currentTimeMillis()));
helpCenterMenuService.insert(helpCenterMenu);
}
写入主库结果如下图:
-
测试读取
@Test void contextLoads() { List<HelpCenterMenu> helpCenterMenus = helpCenterMenuService.selectList(); helpCenterMenus.forEach(f->{ System.out.println(f.getMenuNm()); }); }
读取从库如下图:
目录结构: