前置说明:本次多数据源整合依赖于之前搭建的MySQL主从复制
前置文章:MySQL搭建主从复制
作为后端开发者,使用多数据源是必不可少的技能
登录MySQL主从,创建开发账号
# 创建账号并赋予用户spring-boot-dynamic-datasource-demo库的所有操作权限
grant all on `spring-boot-dynamic-datasource-demo`.* to 'dynamic-datasource-user'@'%' identified by '@Aa1234567890';
# 刷新权限
flush privileges;
Mybatis在进行数据库操作的时候,最终获取的连接是由DataSource接口的getConnection方法获取连接,整合多数据源的目的就是根据业务场景的不同,使用不同的数据源进行操作,举个例子,
比如MySQL的读写分离,在进行查询的时候,我们需要让MyBatis使用从库,进行增删改的时候,需要让MyBatis使用主库,那我们可不可以从这个方法下手呢(自定义一个DataSource的实现类,重写getConnection()方法,根据需求切换数据源,返回不同的Connection)
这个方法也贴一下
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password)
throws SQLException;
}
这个肯定是不行的,虽然可以达到读写分离的效果,(亲测可行),但是实现这个接口需要重写很多方法,我们只能处理getConnection这个方法,其他的方法我们无法处理,如果Spring调用其他方法的话,那后果就可想而知了,其实Spring已经考虑到开发者会整合多数据源,帮我们处理好了
AbstractRoutingDataSource这个类是Spring帮我们提供的多数据源整合的抽象类,下面我们简单说说这个类,我截取了主要的一部分代码,
主要的是3个成员变量,一个抽象方法
targetDataSources:所有数据源,即我们的所有主从节点,需要我们手动设置
defaultTargetDataSource:默认数据源,当根据determineCurrentLookupKey()返回的key找不到就是默认的,看下图,很清楚了
resolvedDataSources:这个变量不需要我们手动配置,在afterPropertiesSet()方法中将targetDataSources复制了一份给resolvedDataSources,至于为什么,不需要我们操心
determineCurrentLookupKey():这个方法需要我们重写,主要是给一个数据源的标识,比如W–写, R–读
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
protected abstract Object determineCurrentLookupKey();
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
}
原理图
前置知识说明
这种方式需要对于Mybatis及插件有一定了解程度
上面说了AbstractRoutingDataSource类,中有个determineCurrentLookupKey()方法,需要我们返回一个数据源标识,那如何去实现呢,进行Select的时候设置为R,增删改的时候设置为W,这里使用Mybatis插件的方式实现
创建项目&导入依赖
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.4version>
dependency>
<dependency>
<groupId>javax.persistencegroupId>
<artifactId>persistence-apiartifactId>
<version>1.0version>
dependency>
dependencies>
application.yml
server:
port: 2022
spring:
datasource-w:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://10.10.10.10/spring-boot-dynamic-datasource-demo?characterEncoding=utf8&useSSL=false
username: dynamic-datasource-user
password: "@Aa1234567890"
datasource-r:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://10.10.10.20/spring-boot-dynamic-datasource-demo?characterEncoding=utf8&useSSL=false
username: dynamic-datasource-user
password: "@Aa1234567890"
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
type-aliases-package: com.cxs.model
mapper-locations: classpath:mapper/*.xml
DynamicDataSource
AbstractRoutingDataSourcede实现类,主要做两件事,
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@Primary
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
private DataSource master;
@Autowired
private DataSource slave;
@Override
protected Object determineCurrentLookupKey() {
return DynamicDatasourceLocalUtil.getLocalCache();
}
@Override
public void afterPropertiesSet() {
super.setDefaultTargetDataSource(master);
Map<Object, Object> map = new HashMap<>();
map.put(TypeEnum.W.name(), master);
map.put(TypeEnum.R.name(), slave);
super.setTargetDataSources(map);
super.afterPropertiesSet();
}
}
DynamicDatasourceLocalUtil
需要记录全局的数据源标识,这里使用ThreadLocal
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
public class DynamicDatasourceLocalUtil {
private static final ThreadLocal<String> localCache = new ThreadLocal<>();
public static void setLocalCache(TypeEnum typeEnum) {
localCache.set(typeEnum.name());
}
public static String getLocalCache() {
return localCache.get();
}
public static void removeLocalCache() {
localCache.remove();
}
}
DynamicDataSourcePlugin
Mybatis插件,帮助我们设置数据源标识,插件的使用方式就不多说了,这里将数据源标识封装成一个枚举中,规范代码,防止硬编码
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class DynamicDataSourcePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement statement = (MappedStatement) args[0];
// SqlCommandType就是对数据库操作的类型
SqlCommandType sqlCommandType = statement.getSqlCommandType();
if (sqlCommandType.equals(SqlCommandType.SELECT)) {
DynamicDatasourceLocalUtil.setLocalCache(TypeEnum.R);
} else {
DynamicDatasourceLocalUtil.setLocalCache(TypeEnum.W);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
}
测试接口
/user/list: 查询用户列表,查询操作,预期走从库,(10.10.10.20)
/user/insert: 插入一条数据,DML操作,预期走主库,(10.10.10.10)
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/list")
public Object list(){
return userService.list();
}
@GetMapping("/insert")
public Object insert(){
User user = new User();
user.setUserName("admin");
return userService.insert(user);
}
}
/user/list |
---|
![]() |
/user/insert |
![]() |
验证通过
补充
上述功能已经实现,但是存在一个问题,我们使用了ThreadLocal,弊端就不多说了,解决方式,新建一个拦截器
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
public class ClearDynamicKeyInterCeptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理DynamicLocal
DynamicDatasourceLocalUtil.removeLocalCache();
}
}
前置知识说明
这种方式需要对于Spring的切面有一定了解程度
上面说了使用mybatis插件实现数据源切换,那么有没有弊端,当然是有的,举个例子,首先表明一下,多数据源不一定都是主从
有这个场景,有两个数据源,分别是order库,shop库。订单库和商品库,查询商品的时候需要查询shop库,下订单的时候我要连order库,这种业务场景就不适用于插件方式了
总结一下哈:
插件方式对于主从结构来说,比较方便,但是多个数据源就会有瓶颈
AOP比较灵活,适用于多个数据源的业务场景
数据库环境搭建
aop方式就不适用主从结构了,实际场景应该是两个不同机器的主库,我这为了方便,同一台机器创建连个库,一个shop库,一个order库,想想哈,其实是一样的
在主库执行
create database `spring-boot-dynamic-datasource-aop-order` character set 'utf8mb4';
create database `spring-boot-dynamic-datasource-aop-shop` character set 'utf8mb4';
grant all on `spring-boot-dynamic-datasource-aop-order`.* to 'dynamic-datasource-user'@'%' identified by '@Aa1234567890';
grant all on `spring-boot-dynamic-datasource-aop-shop`.* to 'dynamic-datasource-user'@'%' identified by '@Aa1234567890';
flush privileges;
在spring-boot-dynamic-datasource-aop-shop库建立t_product表
create table t_product(
id int primary key auto_increment,
product_name varchar(50),
price int
) comment '商品表';
insert into t_product values(1, '华为手机', 3000);
在spring-boot-dynamic-datasource-aop-order表创建t_order表
create table t_order(
id int primary key auto_increment,
product_id int,
create_time datetime
) comment '订单表';
insert into t_order values(1, 1, '2022-12-27 12:30:15');
创建项目&导入依赖
依赖于上述类似,新增aop依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
application.yml
当前场景使用两个主库
server:
port: 2022
spring:
datasource-order:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://10.10.10.10/spring-boot-dynamic-datasource-aop-order?characterEncoding=utf8&useSSL=false
username: dynamic-datasource-user
password: "@Aa1234567890"
datasource-shop:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://10.10.10.10/spring-boot-dynamic-datasource-aop-shop?characterEncoding=utf8&useSSL=false
username: dynamic-datasource-user
password: "@Aa1234567890"
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
type-aliases-package: com.cxs.model
mapper-locations: classpath:mapper/*.xml
MybatisConfig
多个数据源配置,配置两个(可自由配置)
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@Configuration
@MapperScan(basePackages = "com.cxs.mapper")
public class MybatisConfig {
@Bean("dataSourceOrder")
@ConfigurationProperties(prefix = "spring.datasource-order")
public DataSource dataSourceOrder(){
return DruidDataSourceBuilder.create().build();
}
@Bean("dataSourceShop")
@ConfigurationProperties(prefix = "spring.datasource-shop")
public DataSource dataSourceShop(){
return DruidDataSourceBuilder.create().build();
}
}
DynamicDataSource
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@Primary
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
private DataSource dataSourceShop;
@Autowired
private DataSource dataSourceOrder;
@Override
protected Object determineCurrentLookupKey() {
return DynamicDatasourceLocalUtil.getLocalCache();
}
@Override
public void afterPropertiesSet() {
Map<Object, Object> map = new HashMap<>();
super.setDefaultTargetDataSource(dataSourceShop);
map.put(TypeEnum.T_ORDER, dataSourceOrder);
map.put(TypeEnum.T_SHOP, dataSourceShop);
super.setTargetDataSources(map);
super.afterPropertiesSet();
}
}
自定义注解&切面实现
aop方式可以在方法执行完毕后,清除ThreadLocal,无需建立拦截器处理
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceType {
TypeEnum value();
}
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@Aspect
@Component
public class DynamicDataSourceAspect {
@Pointcut(value = "within(com.cxs.service.impl.*)")
public void pointCut(){}
@Around(value = "pointCut() && @annotation(dataSourceType)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, DataSourceType dataSourceType){
Object result = null;
try {
// 设置当前处理线程的数据源标识
DynamicDatasourceLocalUtil.setLocalCache(dataSourceType.value());
result = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
} finally {
// 清除ThreadLocal,防止内存泄漏
DynamicDatasourceLocalUtil.removeLocalCache();
}
return result;
}
}
业务实现
以ProductServiceImpl为例
@DataSourceType(TypeEnum.T_SHOP) 指定当前业务的数据源
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
@Override
@DataSourceType(TypeEnum.T_SHOP)
public List<Product> productList() {
return productMapper.selectList();
}
}
GlobalController
两个接口:
/shop/list:使用spring-boot-dynamic-datasource-aop-shop库
/order/add: 使用spring-boot-dynamic-datasource-aop-order库
/*
* @Project:spring-boot-dynamic-datasource-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@RestController
public class GlobalController {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
@GetMapping("/shop/list")
public Object list(){
return productService.productList();
}
@GetMapping("/order/add")
public Object add(){
Order order = new Order();
order.setProductId(1);
order.setCreateTime(LocalDateTime.now());
return orderService.addOrder(order);
}
}
/shop/list |
---|
![]() |
/order/add |
![]() |
整合多数据源的方式不止这些,由于篇幅原因,就不继续了,完整笔记&整合源码已放置公众号后台,自己获取即可,目录如下
1、制作不易,一键三连再走吧,您的支持永远是我最大的动力!
3、Java全栈技术交流Q群:941095490,欢迎您的加入,案例代码及笔记见群文件!