一、使用场景
1,一个应用链接多个数据库
2,读写分离:
二、如何实现多数据源-自己实现
1,利用SringJDBC模块提供的AbstractRoutingDataSource 基本的图示:
优先自己实现
==>
核心思想就是Spring框架在获取数据源的时候走到我们自己实现的数据源BeanZ中,然后根据key的不同获取到不同的Database。
1,我们只需创建AbstractRoutingDataSource实现类DynamicDataSource然后 始化targetDataSources和key为 数据源标识(可以是字符串、枚举、都行,因为标识是Object)、defaultTargetDataSource即可
2.后续当调用AbstractRoutingDataSource.getConnection 会接着调用提供的模板方法: determineTargetDataSource
3.通过determineTargetDataSource该方法返回的数据库标识 从resolvedDataSources 中拿到对应的数据源 4.我们只需DynamicDataSource中实现determineTargetDataSource为其提供一个数据库标识
数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://127.0.0.1:3306/database1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: root
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/database2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: root
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
数据源配置注入
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
}
}
将多个数据源进行注入,设置默认的key值。
@Component
@Primary // 将该Bean设置为主要注入Bean,
public class DynamicDataSource implements DataSource, InitializingBean {
// 当前使用的数据源标识
public static ThreadLocal<String> name=new ThreadLocal<>();
// 写
@Autowired
DataSource dataSource1;
// 读
@Autowired
DataSource dataSource2;
@Override
public Connection getConnection() throws SQLException {
if(name.get().equals("W")){
return dataSource1.getConnection();
}else{
return dataSource2.getConnection();
}
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
if(name.get().equals("W")){
return dataSource1.getConnection();
}else{
return dataSource2.getConnection();
}
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
/**
* InitializingBean 实现此接口需要重写afterPropertiesSet,初始化的过程。
* 其实也可以在构造函数中进行初始化。
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
//todo ..初始化
name.set("W");
}
具体的使用
@RestController
@RequestMapping("frend")
@Slf4j
public class FrendController {
@Autowired
private FrendService frendService;
@GetMapping(value = "select")
public List<Frend> select(){
DynamicDataSource.name.set("R");
return frendService.list();
}
@GetMapping(value = "selectA")
public String selectA(){
return "你正在访问A资源";
}
@GetMapping(value = "insert")
public void in(){
Frend frend = new Frend();
frend.setName("徐庶");
DynamicDataSource.name.set("W");
frendService.save(frend);
}
}
三、使用Spring提供的AbstractRoutingDataSource
-
此项一般结合及自定义注解使用
-
自定义注解 + Spring提供的AbstractRoutingDataSource 避免了自己写的实现类【DynamicDataSource】中一些没有实现的方法
数据源注入
@Component
@Primary // 将该Bean设置为主要注入Bean,
public class DynamicDataSource extends AbstractRoutingDataSource {
// 当前使用的数据源标识
public static ThreadLocal<String> name=new ThreadLocal<>();
// 写
@Autowired
DataSource firstDataSource;
// 读
@Autowired
DataSource secondDataSource;
/**
* 返回当前数据源标识
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
@Override
public void afterPropertiesSet() {
//1,为targetDataSource初始化所有数据源
Map<Object,Object> targetDataSources= new HashMap<>();
targetDataSources.put(DataSourceMark.W.name(),firstDataSource);
targetDataSources.put(DataSourceMark.R.name(),secondDataSource);
super.setTargetDataSources(targetDataSources);
//2,为defaultDataSource设置默认的数据源
super.setDefaultTargetDataSource(firstDataSource);
super.afterPropertiesSet();
}
}
自定义注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
String value() default "W";
}
注解在具体类的使用
---------【FrendReadServiceImpl】读使用--------------
@Service
@WR("R")
public class FrendReadServiceImpl implements FrendReadService {
@Autowired
FrendMapper frendMapper;
@Override
public List<Frend> list() {
return frendMapper.list();
}
}
---------------【FrendWriteServiceImpl】写使用------------------------------
@Service
@WR("W")
public class FrendWriteServiceImpl implements FrendWriteService {
@Autowired
FrendMapper frendMapper;
@Override
public void save(Frend frend) {
frendMapper.save(frend);
}
}
切点切面中对于数据源选择的使用
使用前置通知(优先推荐):
在获取数据源之前设置获取数据源的key.
@Before("execution(* com.tuling.dynamic.datasource.service.impl.*.*(..))")
public void dynamicDataSource(JoinPoint joinPoint) {
WR annotation = joinPoint.getTarget().getClass().getAnnotation(WR.class);
if(annotation!=null){
String value = annotation.value();
DynamicDataSource.name.set(value);
log.info("使用的数源:"+value);
}
}
或者使用环绕通知亦可以。
@Component
@EnableAspectJAutoProxy //启动AOP
@Aspect
@Slf4j
public class DynamicAnnotation {
//切点
@Pointcut("execution(* com.tuling.dynamic.datasource.service.*.*(..))")
public void pointMethod() {
}
;
//切面
@Around("pointMethod()")
public Object dynamicDataSource(ProceedingJoinPoint joinPoint) {
Object obj=new Object();
try {
WR annotation = joinPoint.getTarget().getClass().getAnnotation(WR.class);
if (annotation != null) {
String value = annotation.value();
System.out.println("类上的注解" + value);
//获取方法的注解
System.out.println("方法:" + joinPoint.getSignature().getName() + " 使用的数据源: " + value);
DynamicDataSource.name.set(value);//设置使用的数据源
obj = joinPoint.proceed();
log.info("方法执行完成之后处理..");
}
} catch (Throwable e) {
e.printStackTrace();
}
return obj;
}
}
拓展:切换数据源的自定义注解在具体方法中的使用:
@RestController
@RequestMapping("frend")
@Slf4j
public class FrendController {
@Autowired
private FrendService frendService;
/**
* 注解切换
* */
@WR("R")
@GetMapping(value = "selectR")
public List<Frend> selectR(){
return frendService.list();
}
@GetMapping(value = "insertW")
@WR("W")
public void insertW(){
Frend frend = new Frend();
frend.setName("徐庶");
frendService.save(frend);
}
}
@Component
@EnableAspectJAutoProxy //启动AOP
@Aspect
@Slf4j
public class DynamicAnnotation {
@Pointcut("execution(* com.tuling.dynamic.datasource..*.*(..))")
public void pointMethod(){
};
@Before("execution(* com.tuling.dynamic.datasource..*.*(..)) && @annotation(wr)")
public void dynamicDataSource(JoinPoint joinPoint, WR wr){
//获取方法的注解
String value = wr.value();
System.out.println("方法:"+joinPoint.getSignature().getName()+ " 使用的数据源: "+value);
DynamicDataSource.name.set(value);//设置使用的数据源
}
}
五、使用Mybatis插件框架集成多个数据源,用于读写分离的场景。
- 手动编写代码侵入性太高了。
- 不同的业务数据库则比方案不太适用。
- 此框架只使用与mybatis数据源,hebanat防范则不适用此方案。
原理:
扩展mybatis的sql执行器Executor,如果执行的是query的查询方法则设备数据源的key=R, 如果是update则设置key=W写操作 注意:插件有四大对象都可以被增强。
设置mybatis插件的代理
@Intercepts注解可以配置需要增加的类,本案例中实现了update和query方法,也可以增加insert等方法
@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.name.set(DataSourceMark.R.getName());
}else {
DynamicDataSource.name.set(DataSourceMark.W.getName());
}
//修改当前线程要选择的数据源的key
return invocation.proceed();
}
}
将自己实现的DynamicDataSourcePlugin注入到myBatis中
在配置类中增加interceptor即可。
@Bean
public Interceptor dynamicDataSourcePlugin(){
return new DynamicDataSourcePlugin();
}
mybatis注入Interceptor 的源码如下图截屏:
六、集成多个Mybatis框架
数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource-w:
url: jdbc:mysql://127.0.0.1:3306/database1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: root
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
datasource-r:
url: jdbc:mysql://127.0.0.1:3306/database2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: root
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
设置两套mapper
public interface RFrendMapper {
@Select("SELECT * FROM friend")
List<Frend> list();
@Insert("INSERT INTO friend(`name`) VALUES (#{name})")
void save(Frend frend);
}
设置写数据源
@Configuration
// 继承mybatis:
// 1. 指定扫描的mapper接口包(主库)
// 2. 指定使用sqlSessionFactory是哪个(主库)
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.w",sqlSessionFactoryRef = "wSqlSessionFactory")
public class dataSourceConfigW {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource-w")
public DataSource wDataSource() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public SqlSessionFactory wSqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(wDataSource());
// 指定主库对应的mapper.xml文件
/*sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/order/*.xml"));
如果是使用註解的方式實現则使用@MapperScan來指定mappper即可。
*/
return sessionFactory.getObject();
}
}
设置读数据源
@Configuration
// 继承mybatis:
// 1. 指定扫描的mapper接口包(主库)
// 2. 指定使用sqlSessionFactory是哪个(主库)
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.r",
sqlSessionFactoryRef = "rSqlSessionFactory")
public class dataSourceConfigR {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource-r")
public DataSource rDataSource() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public SqlSessionFactory rSqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(rDataSource());
return sessionFactory.getObject();
}
}
具体使用: 在service中注入不同的mapper
@Service
public class FrendServiceImpl implements FrendService {
@Autowired
private WFrendMapper wFrendMapper;
@Autowired
private RFrendMapper rFrendMapper;
@Override
public List<Frend> list() {
return rFrendMapper.list();
}
@Override
public void save(Frend frend) {
wFrendMapper.save(frend);
}
}
附录:
关于切面的配置
- 前置通知@Before的源码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Before {
String value();
String argNames() default "";
}
所以如下的使用异常:
@Before("within(com.tuling.dynamic.datasource.service.impl..*) && @annotation(wr)")
public void dynamicDataSource(JoinPoint joinPoint,WR wr) {
String value = wr.value();
log.info("自定义注解值:"+value);
DynamicDataSource.name.set(value);
}
- 环绕通知范围要注意范围,最好指定到具体的包或者类,防止不必要的性能消耗。如果整个项目使用环绕通知,则会有异常,比如:读取数据源的配置文件时没有实现接口则环绕通知会失败。