多数据源系列
1、springboot2.6+Mybatis静态多数据源(集成JTA(Atomikos案例)实现分布式事务控制)
2、springboot2.6+Mybatis动态多数据源AOP切换(AbstractRoutingDataSource)
3、springboot2.6+Mybatis注解多数据源使用dynamic-datasource-spring-boot-starter为依赖
说明
搭建多数据源有多种方式,上一篇博客介绍了一种最基本的方式搭建多数据源,就是把每个数据源配置了一个DataSource的Bean,这种方式显得比较繁琐,mapper也要放在不同的地方,这里介绍一种动态切换数据源的方式
为什么用动态数据源
其实换一个角度想想,我们使用多个SqlSessionFactory来各自连接不同的数据源是很有局限性的。当我们数据源数量比较多的时候类似上文的模板式的代码将充斥整个项目,配置起来比较的繁琐。而且,试想一下,我们并不是每时每刻都对各个数据源都需要进行操作,每个数据源又会保有一个基本的闲置连接数。这样对本就宝贵的系统内存和CPU等资源产生了浪费,所以,第二种方案就应运而生了–动态数据源。我举一个生活中比较形象的例子:工人使用的钻头,其实钻机是只需要一个的,我们只需要根据不同的墙壁材质和孔的形状需要去替换掉钻机上不同的钻头就可以适应各个场景了呀。而上文我们所做的事情是买了两套甚至多套的钻机(真的有点奢侈了!)
缺点是:使用AbstractRoutingDataSource无法在JTA分布式事务中切换数据源,即放弃掉jta多事务,如果强制使用,要继承template重写源码,效果跟第一篇文章差不多,所以放弃jta。
动态数据源方案
文件结构
maven引入:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<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.1.2</version>
</dependency>
application.yml 配置文件
master1:
url: jdbc:mysql://127.0.0.1:3306/master1?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
username: root
password: 123456
driverClassName: com.mysql.cj.jdbc.Driver
master2:
url: jdbc:mysql://127.0.0.1:3306/master2?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
username: root
password: 123456
driverClassName: com.mysql.cj.jdbc.Driver
oracle:
url: jdbc:oracle:thin:@10.132.212.63:1688:TESTDB
username: flx
password: flx202108
driverClassName: oracle.jdbc.OracleDriver
logging:
level:
com.xkcoding: debug
com.xkcoding.orm.mybatis.mapper: trace
server:
port: 8080
# servlet:
# context-path: /demo
其实换一个角度想想,我们使用多个SqlSessionFactory来各自连接不同的数据源是很有局限性的。当我们数据源数量比较多的时候类似上文的模板式的代码将充斥整个项目,配置起来比较的繁琐。而且,试想一下,我们并不是每时每刻都对各个数据源都需要进行操作,每个数据源又会保有一个基本的闲置连接数。这样对本就宝贵的系统内存和CPU等资源产生了浪费,所以,第二种方案就应运而生了–动态数据源。我举一个生活中比较形象的例子:工人使用的钻头,其实钻机是只需要一个的,我们只需要根据不同的墙壁材质和孔的形状需要去替换掉钻机上不同的钻头就可以适应各个场景了呀。而上文我们所做的事情是买了两套甚至多套的钻机(真的有点奢侈了!)。
1.自定义一个动态数据源上下文类,该类依靠一个ThreadLocal的类变量类标识当前线程是须要访问哪个数据源
为什么要用ThreadLocal?点击下面。
ThreadLocal<String>和static String在多线程的区别
package com.orm.mybatis.dynamic.config;
/**
* 自定义数据源切换类
*/
public class DatabaseContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**
* 设置数据源
*/
public static void setDBKey(String dataSourceKey) {
contextHolder.set(dataSourceKey);
}
/**
* 取得当前数据源
*/
public static String getDBKey() {
return contextHolder.get();
}
/**
* 清除上下文数据
*/
public static void clearDBKey() {
contextHolder.remove();
}
}
2. 建立一个动态数据源
继承自spring的org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource。实现了determineCurrentLookupKey方法,该方法惟一须要作的事情就是从DynamicDataSourceContextHolder获取当前须要访问的数据库名称。
package com.orm.mybatis.dynamic.config;
/**
* 动态数据源
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static DynamicDataSource instance;
private static byte[] lock=new byte[0];
private static Map<Object,Object> dataSourceMap=new HashMap<Object, Object>();
// private AtomicInteger count = new AtomicInteger(0); //读写分离
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceMap.putAll(targetDataSources);
// 必须添加该句,否则新添加数据源无法识别到
super.afterPropertiesSet();
}
public Map<Object, Object> getDataSourceMap() {
return dataSourceMap;
}
/**
* 核心方法,切换数据源上下文 非读写分离
*/
@Override
protected Object determineCurrentLookupKey() {
String dbKey = DatabaseContextHolder.getDBKey();
System.out.println(Thread.currentThread().getName()+" dbKey: "+dbKey);
return dbKey;
}
/**
* 核心方法,切换数据源上下文 读写分离
*/
// @Override
// protected Object determineCurrentLookupKey() {
// String dbKey = DatabaseContextHolder.getDBKey();
// if (dbKey.equals(DbUtil.DB_MASTER1)) // 如果是写库,直接返回
// return dbKey;
// // 读 简单负载均衡
// int number = Math.abs(count.getAndAdd(1));
// int lookupKey = number % 2;
// if (lookupKey==1){
// return DbUtil.DB_MASTER2;
// }else{
// return DbUtil.DB_MASTER3;
// }
// }
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
private DynamicDataSource() {}
public static synchronized DynamicDataSource getInstance(){
if(instance==null){
synchronized (lock){
if(instance==null){
instance=new DynamicDataSource();
}
}
}
return instance;
}
}
3.配置切面
在实际的项目开发中,不可能老是在访问数据库以前,调用DynamicDataSourceContextHolder.setDataSource,这样很差维护、繁琐、代码可阅读性也很差。因此,能够自定义一个注解,用于标识方法是要走从库仍是主库,而后用一个切面,切面对有相应注解的方法作加强,根据注解的属性,设置须要访问的数据源。
如果了解什么是AOP,点击下方连接
SpringBootAOP切面编程方法拦截和自定义注解拦截实现灵活的AOP切面配置
注解代码:
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default DbUtil.DB_MASTER1;
}
DbUtil.class
public class DbUtil {
/**数据库ds_master1**/
public static final String DB_MASTER1 = "ds_master1";
/**数据库ds_master2**/
public static final String DB_MASTER2 = "ds_master2";
/**数据库ds_master3**/
public static final String DB_MASTER3 = "ds_master3";
}
切面代码:
package com.orm.mybatis.dynamic.config;
import com.orm.mybatis.dynamic.annotation.DataSource;
import com.orm.mybatis.dynamic.util.DbUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Order(-10) //加上这个后,会先切面切换数据源再切换事务,可以在方法里叠加 @Transactional @DataSource(value = DbUtil.DB_MASTER1) 这样就会先切换数据源再添加事务,不会导致无法切换数据源
@Aspect
@Component
public class AOPAspectAnnotation {
// private AtomicInteger count = new AtomicInteger(0); //读写分离
@Pointcut(value = "@annotation(com.orm.mybatis.dynamic.annotation.DataSource)") //注意这里是全文件匹配函数
public void getPoint() {
}
@Before("getPoint()")
public void setPointAcc1(){
System.out.println("BeforeGetPoint");
}
@Around("getPoint()")
public Object getDoAround(ProceedingJoinPoint pjp){
ThreadLocal<Long> startTime = new ThreadLocal<>();
startTime.set(System.currentTimeMillis());
System.out.println("我是环绕通知执行");
Object obj;
try{
DataSource dataSource = null;
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
dataSource = method.getAnnotation(DataSource.class);
if (dataSource==null){
System.out.println("空值");
System.out.println(DbUtil.DB_MASTER1);
DatabaseContextHolder.setDBKey(dataSource.value());
obj = pjp.proceed();
System.out.println("执行返回值 : " + obj);
}else {
System.out.println(dataSource.value());
// if (dataSource.value().equals(DbUtil.DB_MASTER1)){ //读写分离 start
DatabaseContextHolder.setDBKey(dataSource.value());
// }else { //读写分离 middle
// // 读 简单负载均衡
// int number = Math.abs(count.getAndAdd(1));
// int lookupKey = number % 2;
// if (lookupKey==1){
// DatabaseContextHolder.setDBKey(DbUtil.DB_MASTER2);
// }else{
// DatabaseContextHolder.setDBKey(DbUtil.DB_MASTER2);
// }
// } //读写分离 end
obj = pjp.proceed();
System.out.println("执行返回值 : " + obj);
}
System.out.println(pjp.getSignature().getName()+"方法执行耗时: " + (System.currentTimeMillis() - startTime.get()));
} catch (Throwable throwable) {
System.out.println(throwable+"报错");
obj=throwable.toString();
}
return obj;
}
@After("getPoint()")
public void setPointAcc2(){
System.out.println("AfterGetPoint");
DatabaseContextHolder.clearDBKey();
}
/**
* 执行完请求可以做的
* @param result
* @throws Throwable
*/
@AfterReturning(returning = "result", pointcut = "getPoint()")
public void doAfterReturning(Object result){
System.out.println("大家好,我是@AfterReturning,他们都秀完了,该我上场了"+result);
}
}
其中@Order是很重要的,必须确保DynamicDataSourceAspect的执行优先于TranctionInterceptor。否则数据源的指定就没法生效(数据源的指定在数据库链接的获取以后!!)
4.配置动态数据源
/**
* 数据源配置
*/
@Configuration
// 扫描 Mapper 接口并容器管理
@MapperScan(basePackages = DatasourceConfig.PACKAGE, sqlSessionTemplateRef = "sqlSessionTemplate")
public class DatasourceConfig {
// mapper扫描
static final String PACKAGE = "com.orm.mybatis.dynamic.mapper.*";
static final String MAPPER_LOCATION = "classpath:mapper/*/*.xml";
@Value("${master1.url}")
private String url1;
@Value("${master1.username}")
private String user1;
@Value("${master1.password}")
private String password1;
@Value("${master1.driverClassName}")
private String driverClass1;
@Value("${master2.url}")
private String url2;
@Value("${master2.username}")
private String user2;
@Value("${master2.password}")
private String password2;
@Value("${master2.driverClassName}")
private String driverClass2;
@Value("${oracle.url}")
private String oracleUrl;
@Value("${oracle.username}")
private String oracleUser;
@Value("${oracle.password}")
private String oraclePassword;
@Value("${oracle.driverClassName}")
private String oracleDriverClass;
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
// master1数据源
DataSource dataSourceMaster1 = initDataSource(driverClass1,url1,user1,password1); //非分布式事务锁
// master2数据源
DataSource dataSourceMaster2 = initDataSource(driverClass2,url2,user2,password2); //非分布式事务锁
// master3数据源
DataSource dataSourceMaster3 = initDataSource(oracleDriverClass,oracleUrl,oracleUser,oraclePassword); //非分布式事务锁
Map<Object,Object> map = new HashMap<>();
map.put(DbUtil.DB_MASTER1, dataSourceMaster1);
map.put(DbUtil.DB_MASTER2, dataSourceMaster2);
map.put(DbUtil.DB_MASTER3, dataSourceMaster3);
dynamicDataSource.setTargetDataSources(map);
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster1);
return dynamicDataSource;
}
/** 非分布式事务
* 初始数据源
* @param driverClass 驱动
* @param url 数据库连接
* @param user 用户名
* @param password 密码
* @return
*/
public HikariDataSource initDataSource(String driverClass, String url, String user, String password){
//jdbc配置
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setDriverClassName(driverClass);
hikariDataSource.setJdbcUrl(url);
hikariDataSource.setUsername(user);
hikariDataSource.setPassword(password);
setPool(hikariDataSource);
return hikariDataSource;
}
/** 非事务分布式
* 连接池配置
* @param hikariDataSource
*/
private void setPool(HikariDataSource hikariDataSource){
//连接池配置
hikariDataSource.setMinimumIdle(5);
hikariDataSource.setMaximumPoolSize(20);
hikariDataSource.setAutoCommit(true);
hikariDataSource.setPoolName("SpringBootDemoHikariCP");
hikariDataSource.setMaxLifetime(1800000);
hikariDataSource.setIdleTimeout(600000);
hikariDataSource.setConnectionTimeout(30000);
}
@Bean(name = "sqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource);
sessionFactory.setTypeAliasesPackage("com.orm.mybatis.dynamic.entity");
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sessionFactory.setConfiguration(configuration);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(DatasourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
/**
* 设置动态数据源DynamicDataSource到会话工厂
*/
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 将动态数据源添加到事务管理器中,并生成新的bean
*
* 切库与事务注意:
* 1.有@Transactional注解的方法,方法内部不可以做切换数据库 操作
* 2.在同一个service其他方法调用带@Transactional的方法,事务不起作用
* 3.在应用中因为使用了 DAO 层的切面切换数据源,所以 @Transactional 注解不能加在类上,只能用于方法;有 @Trasactional注解的方法无法切换数据源
* @return 事务管理实例
*/
@Bean
public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
这一句很重要dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster1);
当调用没有添加@DataSource注解的方法时,默认走主库。到这一步,读写分离的基础都已经有了,接下来只须要按咱们日常调用单数据源那样配置mybatis就能够
UserServiceImpl 业务层
@Service
public class UserServiceImpl {
@Resource
private UserMapper1 userMapper1;
@Resource
private UserMapper2 userMapper2;
@Resource
private AsusPoInfoMapper3 asusPoInfoMapper3;
public List<User> findAllUser(){
DatabaseContextHolder.setDBKey(DbUtil.DB_MASTER1);
List<User> list = userMapper1.selectAllUser();
DatabaseContextHolder.clearDBKey();
return list;
}
public void testTransitional() {
((UserServiceImpl)AopContext.currentProxy()).saveUserMapper2();//调用本类方法如想触发事务需要增强代理。
((UserServiceImpl)AopContext.currentProxy()).saveUserMapper1();
((UserServiceImpl)AopContext.currentProxy()).saveUserMapper3();
}
@Transactional
@DataSource(value = DbUtil.DB_MASTER1)
public void saveUserMapper1(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = simpleDateFormat.format(new Date());
String UUID = java.util.UUID.randomUUID().toString().substring(0,5);
User user = User.builder().email("andrew@qq.com"+UUID).name("andrew"+UUID).password("123456"+UUID).phoneNumber("123"+UUID)
.lastUpdateTime(date).createTime(date).status(0).salt("password"+UUID).build();
userMapper2.saveUser(user);
}
@Transactional
@DataSource(value = DbUtil.DB_MASTER2)
public void saveUserMapper2(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = simpleDateFormat.format(new Date());
String UUID = java.util.UUID.randomUUID().toString().substring(0,5);
User user = User.builder().email("andrew@qq.com"+UUID).name("andrew"+UUID).password("123456"+UUID).phoneNumber("123"+UUID)
.lastUpdateTime(date).createTime(date).status(0).salt("password"+UUID).build();
userMapper2.saveUser(user);
}
@DataSource(value = DbUtil.DB_MASTER3)
@Transactional
public void saveUserMapper3(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = simpleDateFormat.format(new Date());
Random random = new Random(500);
int a = random.nextInt();
AsusPoInfo asusPoInfo = AsusPoInfo.builder().id(UUID.randomUUID().toString().substring(0,20))
.woNo("andrew").po("123456").poLine("poline").cPo("cpo123456").shipType("Direct").build();
asusPoInfoMapper3.insertAsusPoInfo(asusPoInfo);
// throw new RuntimeException();
}
}
测试多数据源回滚
package com.orm.mybatis.dynamic;
public class UserTest extends AndrewApplicationTests {
@Resource
private UserServiceImpl userService;
@Test
public void test1(){
System.out.println(userService.findAllUser());
}
@Test
public void testTransitionalAspect() throws InterruptedException {
userService.testTransitional();
}
切换数据源成功,而且事务能回滚,但如果是多数据源事务,只能回滚报错的数据源的事务。
方案的权衡
- 静态多数据源方案优势在于配置简单并且对业务代码的入侵性极小,缺点也显而易见:我们需要在系统中占用一些资源,而这些资源并不是一直需要,一定程度上会造成资源的浪费。如果你需要在一段业务代码中同时使用多个数据源的数据又要去考虑操作的原子性(事务)可以用spring的jta实现事务,那么这种方案无疑会适合你。
- (aop和dynamic)动态数据源(AbstractRoutingDataSource)方案配置上看起来配置会稍微复杂一些,但是很好的符合了“即拿即用,即用即还”的设计原则,我们把多个数据源看成了一个池子,然后进行消费。它的缺点正如上文所暴露的那样:我们往往需要在事务的需求下做出妥协。而且由于需要切换环境上下文,在高并发量的系统上进行资源竞争时容易发生死锁等活跃性问题。我们常用它来进行数据库的“读写分离”,不需要在一段业务中同时操作多个数据源。这种动态形式并不能用spring的jta实现,而且其他实现方式(seata等)虽然可以实现,但配置复杂且实用度不高。
- 如果需要使用事务,一定记得使用分布式事务进行Spring自带事务管理的替换,否则将无法进行一致性控制。
- 写到这里本文也就结束,好久没有撰写文章很多东西考虑不是很详尽,谢谢批评指正!
项目地址
springboot2.6+mybatis
https://gitee.com/liuweiqiang12/springboot-mybatis-dynamic-datasource
springboot2.6+mybatis-plus
https://gitee.com/liuweiqiang12/springboot-mybatis-plus-dynamic-datasource