在互联网开发公司中,往往伴随着业务的快速迭代,程序员可能没有过多的时间去思考技术扩展的相关问题,长久下来导致技术过于单一。为此最近在学习互联网思维,从相对简单的功能开始做总结,比如非常常见的基础数据的后台管理,那么涉及到多数据源的情况又会有哪些问题呢?
思考1:在业务中如何更加灵活方便的切换数据源呢?
思考2:多数据源之间的事务如何保证呢?
思考3:这种多数据源的分布式事务实现思路有哪些?
本篇文章的重点,也就是多数据源问题总结为以下三种方式:
准备工作:
首先AbstractRoutingDataSource是jdbc包提供的,需要引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
自定义一个类继承AbstractRoutingDataSource,重写关键方法:
@Component
@Primary
public class DynamicDatasource extends AbstractRoutingDataSource
{
public static ThreadLocal<String> dataSourceName = new ThreadLocal<>();
@Autowired
DataSource dataSource1;
@Autowired
DataSource dataSource2;
@Override
protected Object determineCurrentLookupKey()
{
return dataSourceName.get();
}
@Override
public void afterPropertiesSet()
{
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("W", dataSource1); //写数据库,主库
targetDataSources.put("R", dataSource2); //读数据库,从库
super.setTargetDataSources(targetDataSources);
super.setDefaultTargetDataSource(dataSource1);
super.afterPropertiesSet();
}
}
配置多个数据源:
@Configuration
public class DataSourceConfig
{
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1()
{
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2()
{
return DruidDataSourceBuilder.create().build();
}
@Bean
public DataSourceTransactionManager transactionManager1(DynamicDatasource dataSource)
{
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
@Bean
public DataSourceTransactionManager transactionManager2(DynamicDatasource dataSource)
{
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
application.yml参考配置:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123666
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
datasource2:
url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123666
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
min-idle,最小空闲连接,不会被销毁的数据库连接。注意:如果设置的太小,当客户端连接多的情况下,就需要新创建数据库连接,是一个比较耗时的操作,可能导致客户端连接超时。设置太大会占用系统资源
max-active:最大活跃数,官网建议配置:正在使用的数据库连接数 / 配置的这个值 = 85%
test-on-borrow:每次连接时都进行检查,生产上配置为true会影响性能,建议false,默认也是false
test-on-return:每次归还连接时进行检查,同样影响性能同上
生产上建议上面两个参数设置false,testWhileIdle设置为true,间隔一段时间检查连接是否可用:
空闲时间大于timeBetweenEvictionRunsMillis(默认1分钟)检查一次,检查发现连接失效也不会马上删除,而是空闲时间超过minEvictableIdleTimeMillis(最小空闲时间,默认30分钟)自动删除
maxEvictableIdleTimeMillis:最大空闲时间,默认7小时。空闲连接时间过长,数据库就会自动把连接关闭,Druid为了防止从连接池中拿到被数据库关闭的连接,设置了这个参数,超过时间强行关闭连接
使用测试:
方案一,直接在方法中设置数据源标识,简单实现功能,缺点也很明显
@Service
public class FriendServiceImpl implements FriendService
{
@Autowired
FriendMapper friendMapper;
@Override
public List<Friend> list()
{
DynamicDatasource.dataSourceName.set("R");
return friendMapper.list();
}
@Override
public void save(Friend friend)
{
DynamicDatasource.dataSourceName.set("W");
friendMapper.save(friend);
}
}
方案二,使用自定义注解+AOP实现,适合不同业务的多数据源场景
// 1、自定义注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR
{
String value() default "W";
}
// 2、切面配置类
@Component
@Aspect
public class DynamicDataSourceAspect
{
@Before("within(com.example.dynamicdatasource.service.impl.*) && @annotation(wr)")
public void before(JoinPoint joinPoint, WR wr)
{
String value = wr.value();
DynamicDatasource.dataSourceName.set(value);
System.out.println(value);
}
}
// 3、使用注解测试
@Service
public class FriendServiceImpl implements FriendService
{
@Autowired
FriendMapper friendMapper;
@WR("R")
@Override
public List<Friend> list()
{
// DynamicDatasource.dataSourceName.set("R");
return friendMapper.list();
}
@WR("W")
@Override
public void save(Friend friend)
{
// DynamicDatasource.dataSourceName.set("W");
friendMapper.save(friend);
}
}
方案三,使用MyBatis插件,适合相同业务读写分离的业务场景
import com.example.dynamicdatasource.DynamicDatasource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import java.util.Properties;
@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class})})
public class DynamicDataSourcePlugin implements Interceptor
{
@Override
public Object intercept(Invocation invocation)
throws Throwable
{
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement)objects[0];
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT))
{
DynamicDatasource.dataSourceName.set("R");
}
else
{
DynamicDatasource.dataSourceName.set("W");
}
return invocation.proceed();
}
@Override
public Object plugin(Object target)
{
if (target instanceof Executor)
{
return Plugin.wrap(target, this);
}
else
{
return target;
}
}
@Override
public void setProperties(Properties properties)
{
}
}
实现思路:Spring集成多个MyBatis框架,指定不同的扫描包、不同的数据源
准备工作:
读库和写库分别添加配置类,扫描不同的包路径:
@Configuration
@MapperScan(basePackages = "com.example.dynamicmybatis.mapper.r", sqlSessionFactoryRef = "rSqlSessionFactory")
public class RMyBatisConfig
{
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2()
{
return DruidDataSourceBuilder.create().build();
}
@Bean
public DataSourceTransactionManager rTransactionManager()
{
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource2());
return dataSourceTransactionManager;
}
@Bean
public SqlSessionFactory rSqlSessionFactory()
throws Exception
{
final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource2());
return sqlSessionFactoryBean.getObject();
}
@Bean
public TransactionTemplate rTransactionTemplate()
{
return new TransactionTemplate(rTransactionManager());
}
}
@Configuration
@MapperScan(basePackages = "com.example.dynamicmybatis.mapper.w", sqlSessionFactoryRef = "wSqlSessionFactory")
public class WMyBatisConfig
{
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1()
{
return DruidDataSourceBuilder.create().build();
}
@Bean
public DataSourceTransactionManager wTransactionManager()
{
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource1());
return dataSourceTransactionManager;
}
@Bean
public SqlSessionFactory wSqlSessionFactory()
throws Exception
{
final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource1());
return sqlSessionFactoryBean.getObject();
}
@Bean
public TransactionTemplate wTransactionTemplate()
{
return new TransactionTemplate(wTransactionManager());
}
}
写库和读库使用两个Mapper类,在不同的包下:
public interface RFriendMapper
{
@Select("select * from friend")
List<Friend> list();
@Insert("insert into friend(name) values(#{name})")
void save(Friend friend);
}
public interface WFriendMapper
{
@Select("select * from friend")
List<Friend> list();
@Insert("insert into friend(name) values(#{name})")
void save(Friend friend);
}
使用测试:
@Service
public class FriendServiceImpl implements FriendService
{
@Autowired
RFriendMapper rFriendMapper;
@Autowired
WFriendMapper wFriendMapper;
@Override
public List<Friend> list()
{
return rFriendMapper.list();
}
@Override
public void save(Friend friend)
{
wFriendMapper.save(friend);
}
}
思考:多数据源的事务问题
public void saveW(Friend friend)
{
friend.setName("gaoW");
wFriendMapper.save(friend);
}
public void saveR(Friend friend)
{
friend.setName("gaoR");
rFriendMapper.save(friend);
}
@Transactional
// @Transactional(transactionManager = "wTransactionManager")
@Override
public void saveAll(Friend friend)
{
saveW(friend);
saveR(friend);
int a = 1 / 0;
}
存在多个事务管理器的情况直接使用@Transactional注解是不行的,Spring不知道使用哪个事务管理器会报错。但是指定了事务管理器后,仅当前事务管理器负责的部分支持回滚,还是存在问题。
在特定场景下,直接指定事务管理器名称的方式可以生效(保证数据一致的意思):
@Transactional(transactionManager = "wTransactionManager")
@Override
public void saveAll(Friend friend)
{
saveW(friend);
saveR(friend);
int a = 1 / 0;
}
1、saveW方法内部异常,saveW发生异常事务不提交,数据一致
2、saveR方法内部异常,事务管理器回滚saveW的更新,saveR异常未提交,数据一致
3、saveW和saveR方法中间的业务发生异常,事务管理器回滚saveW的更新,saveR未提交,数据一致
4、saveW和saveR方法后面的业务发生异常,事务管理器回滚saveW的更新,saveR已提交,数据不一致
Spring提供的编程式事务解决方案:
@Service
public class FriendServiceImpl implements FriendService
{
@Autowired
RFriendMapper rFriendMapper;
@Autowired
WFriendMapper wFriendMapper;
@Autowired
TransactionTemplate rTransactionTemplate;
@Autowired
TransactionTemplate wTransactionTemplate;
public void saveW(Friend friend)
{
friend.setName("gaoW");
wFriendMapper.save(friend);
}
public void saveR(Friend friend)
{
friend.setName("gaoR");
rFriendMapper.save(friend);
}
@Override
public void saveAll2(Friend friend)
{
wTransactionTemplate.execute(wstatus -> {
rTransactionTemplate.execute(rstatus -> {
try
{
saveW(friend);
saveR(friend);
// int a = 1 / 0;
}
catch (Exception e)
{
e.printStackTrace();
wstatus.setRollbackOnly();
rstatus.setRollbackOnly();
return false;
}
return true;
});
return true;
});
}
}
Spring支持的声明式事务解决方案(分布式事务变种实现):
@Service
public class FriendServiceImpl implements FriendService
{
@Autowired
RFriendMapper rFriendMapper;
@Autowired
WFriendMapper wFriendMapper;
@Autowired
TransactionTemplate rTransactionTemplate;
@Autowired
TransactionTemplate wTransactionTemplate;
@Override
public List<Friend> list()
{
return rFriendMapper.list();
}
@Override
public void save(Friend friend)
{
wFriendMapper.save(friend);
}
public void saveW(Friend friend)
{
friend.setName("gaoW");
wFriendMapper.save(friend);
}
public void saveR(Friend friend)
{
friend.setName("gaoR");
rFriendMapper.save(friend);
}
@Transactional(transactionManager = "wTransactionManager")
@Override
public void saveAll1(Friend friend)
{
FriendService friendService = (FriendService)AopContext.currentProxy();
friendService.saveAllR(friend);
}
@Transactional(transactionManager = "rTransactionManager")
@Override
public void saveAllR(Friend friend)
{
saveW(friend);
saveR(friend);
// int a = 1 / 0;
}
}
@EnableAspectJAutoProxy(exposeProxy = true) //暴露代理对象
public class DynamicMybatisApplication {
注意:调用saveAllR方式时,需要使用代理对象,直接调用本类的其他方法事务不会生效
@Autowired自动注入自己获取代理对象,这种方式在springboot2.6以后有循环依赖报错,需要改配置,按照错误提示添加配置,设置参数为true即可
上面这两种事务的解决方式适用场景:
只涉及到两三个数据源,并且多数据源事务的场景不多,同时公司又不希望引入其他组件(安全性问题考虑),那么就可以使用这种方式实现分布式事务。当然分布式事务最好的解决方案肯定是通过第三方组件比如Seata
dynamic-datasource是属于苞米豆生态圈的
基于Springboot的多数据源组件,功能强悍,支持Seata事务
支持数据源分组,适用多库、读写分离、一主多从(实现了负载均衡,轮询/随机)等场景
提供自定义数据源方案,比如从数据库加载
提供项目启动后动态增加和删除数据源方案,可以添加管理后台页面灵活调整
提供MyBatis环境下的纯读写分离方案
提供本地多数据源事务方案
提供基于Seata的分布式事务方案,注意不能和原生spring事务混用
等等
多数据源实现方式:就是通过继承AbstractRoutingDataSource的这种方式
数据源切换:通过AOP+自定义注解实现的
使用示例:
@Service
public class FriendServiceImpl implements FriendService
{
@Autowired
FriendMapper friendMapper;
@Override
@DS("slave")
public List<Friend> list()
{
return friendMapper.list();
}
@Override
@DS("master")
@DSTransactional
public void save(Friend friend)
{
friendMapper.save(friend);
}
@DS("master")
@DSTransactional
public void saveAll()
{
// 执行多数据源的操作
}
}
application.yml参考配置:
spring:
?datasource:
? ?dynamic:
? ? ?#设置默认的数据源或者数据源组,默认值即为master
? ? ?primary: master
? ? ?#严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
? ? ?strict: false
? ? ?datasource:
? ? ? ?master:
? ? ? ? ?url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
? ? ? ? ?username: root
? ? ? ? ?password: 123666
? ? ? ? ?initial-size: 1
? ? ? ? ?min-idle: 1
? ? ? ? ?max-active: 20
? ? ? ? ?test-on-borrow: true
? ? ? ? ?driver-class-name: com.mysql.cj.jdbc.Driver
? ? ? ?slave_1:
? ? ? ? ?url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
? ? ? ? ?username: root
? ? ? ? ?password: 123666
? ? ? ? ?initial-size: 1
? ? ? ? ?min-idle: 1
? ? ? ? ?max-active: 20
? ? ? ? ?test-on-borrow: true
? ? ? ? ?driver-class-name: com.mysql.cj.jdbc.Driver