Java利用注解和AOP实现动态切换数据源(详解)
前言:
我们先介绍一下主从复制的概念以及主从复制的优点,扩展一下知识面。
1.什么是主从复制?
主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库。在赋值过程中,一个服务器充当主服务器,而另外一台服务器充当从服务器。
当一台从服务器连接到主服务器时,从服务器会通知主服务器获取主服务器的日志文件中读取最后一次成功更新的位置。然后从服务器会接收从哪个时刻起发生的任何更新,然后锁住并等到主服务器通知新的更新。
2.主从复制的优点?
- 数据安全性高:做了数据的备份,发送单台服务器宕机也不会丢失数据。
- 性能提升:一主多从的模式,主机负责写数据,从机负责读数据,提高性能。
- 扩展性高:在QPS(Queries Per Second)大的情况下,可以通过简单的增加从服务器,提高系统稳定性。
知识贴纸:
QPS全称为Queries Per Second,即每秒钟处理的请求数量。对于一个高并发应用来说,QPS是非常重要的性能指标,它反映了应用处理请求的能力。在实际应用中,QPS的大小取决于应用的负载和应用本身的性能。
举个例子,假设有一个电商网站,这个网站每天需要处理100万个订单请求,那么每秒钟需要处理的订单数量就是1000000 / 86400 ≈ 11.57。因此,这个网站的QPS应该至少达到11.57。
我们为什么要去实现多数据源配置?
- 在一个项目中,我们可能访问的数据库资源并不止一个,可能是多个甚至更多,这个时候我们就需要去搭建多个数据源,进行动态的切换数据源进行一系列的增删改查操作。
- 在QPS大的情况下,利用主从复制动态切换数据源,可以提高系统的稳定性和性能。
- 在随着业务增长,数据量变大的情况下,我们可能会去分库分表,需要去查询多个数据库。
实现方式:
我们应该如何优雅的通过Java代码实现动态切换数据呢?
- 注解
- AOP(面向切面编程)
没错,我们只需掌握这两项技术就可以实现动态切换数据源,去提高系统的稳定性和性能。
代码实现:
0.准备环节: 配置一下我们的yml文件和pom文件
项目所需的依赖jar包:
<!--引入mysql依赖jar-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--引入jdbc驱动依赖jar-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--引入druid数据连接池依赖jar-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.11</version>
</dependency>
多数据源配置的yml文件:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://localhost:3306/test
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/test_slave
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
1.通过接口定义数据源的名称
public interface DynamicDataSourceId {
/**
* 主库名称
*/
String MASTER = "master";
/**
* 从库名称
*/
String SLAVE = "slave";
}
注:接口中定义的变量都是常量,防止被改变,影响我们的程序(隐式默认 : public static final)
2.定义一个注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DataSourceDynamicSwitch {
String dataSourceId() default DynamicDataSourceId.MASTER;
}
3.定义一个类通过上下文设置指定数据源
利用ThreadLocal的技术,保证线程切换数据源的安全性,不会去影响其他线程甚至整个程序的数据源调整。
public class DynamicDataSourceContextHolder {
/**
* 每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置当前线程需要使用的数据源id(和当前线程绑定)
*/
public static void setDataSourceId(final String dataSourceId){
CONTEXT_HOLDER.set(dataSourceId);
}
/**
* 获取当前线程使用的数据源id
*/
public static String getDataSourceId(){
return CONTEXT_HOLDER.get();
}
/**
* 清空当前线程使用的数据源id
*/
public static void clearDataSourceId(){
CONTEXT_HOLDER.remove();
}
}
4.直接定义一个类去实现spring-boot-starter-jdbc包下的数据源切换方法
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceId();
}
}
5.定义一个配置类去注册数据源的配置
@Configuration
@ConditionalOnClass(SqlSessionFactory.class)
public class DataSourceConfig {
/**
* 获取yml中主库数据源信息
*/
@Value("${spring.datasource.druid.master.password}")
private String masterPassword;
@Value("${spring.datasource.druid.master.username}")
private String masterUserName;
@Value("${spring.datasource.druid.master.url}")
private String masterUrl;
@Value("${spring.datasource.druid.master.driver-class-name}")
private String masterDriverClassName;
public DataSource masterDataSource() {
DruidDataSource masterDruidDataSource = new DruidDataSource();
masterDruidDataSource.setUrl(masterUrl);
masterDruidDataSource.setUsername(masterUserName);
masterDruidDataSource.setPassword(masterPassword);
masterDruidDataSource.setDriverClassName(masterDriverClassName);
return masterDruidDataSource;
}
/**
* 获取yml中从库数据源信息
*/
@Value("${spring.datasource.druid.slave.password}")
private String slavePassword;
@Value("${spring.datasource.druid.slave.username}")
private String slaveUserName;
@Value("${spring.datasource.druid.slave.url}")
private String slaveUrl;
@Value("${spring.datasource.druid.slave.driver-class-name}")
private String slaveDriverClassName;
public DataSource slaveDataSource() {
DruidDataSource slaveDruidDataSource = new DruidDataSource();
slaveDruidDataSource.setUrl(slaveUrl);
slaveDruidDataSource.setUsername(slaveUserName);
slaveDruidDataSource.setPassword(slavePassword);
slaveDruidDataSource.setDriverClassName(slaveDriverClassName);
return slaveDruidDataSource;
}
/**
* 配置数据源交给spring管理
*/
@Bean
public DataSource dataSource() {
DynamicDataSource dataSource = new DynamicDataSource();
final DataSource masterDataSource = masterDataSource();
final DataSource slaveDataSource = slaveDataSource();
Map<Object,Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DynamicDataSourceId.MASTER, masterDataSource);
dataSourceMap.put(DynamicDataSourceId.SLAVE, slaveDataSource);
dataSource.setTargetDataSources(dataSourceMap);
//设置默认数据源
dataSource.setDefaultTargetDataSource(masterDataSource);
return dataSource;
}
/**
* 创建JdbcTemplate交给spring管理
*/
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
}
6.通过AOP实现切面类,使DataSourceDynamicSwitch注解生效
@Aspect
@Component
public class DynamicDataSourceSwitchAspect {
@Pointcut("@annotation(com.annotation.DataSourceDynamicSwitch)")
public void pointcut() {
}
/**
* 通过切面切换数据源 通过threadlocal 一个方法会绑定一个线程 数据源存储在此线程里面
**/
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//先获取方法上注解
DataSourceDynamicSwitch annotation = method.getAnnotation(DataSourceDynamicSwitch.class);
if (annotation == null) {
//方法上没有,再获取类上的注解
annotation = method.getClass().getClass().getAnnotation(DataSourceDynamicSwitch.class);
if (annotation == null) return;
}
//获取注解上指定切换的数据源
String dataSourceId = annotation.dataSourceId();
//通过DynamicDataSourceContextHolder上下文对象设置数据源ID
DynamicDataSourceContextHolder.setDataSourceId(dataSourceId);
}
/**
* 清除掉当前线程数据源,下次线程进来防止干扰默认数据源
**/
@After("pointcut()")
public void doAfter(){
DynamicDataSourceContextHolder.clearDataSourceId();
}
}
!到此通过AOP+注解的方式实现动态切换数据源完成 !
代码测试:
这里就不描述Mapper和Dao层的写法了,大家可以自己写两个方法,再方法上使用我们定义的注解并配置数据源名称,进行测试。
@Test
void dynamicDataSource(){
System.out.println("查询主数据库");
userDao.find1("'李华'",13);
System.out.println("查询从数据库");
userDao.find2("李华",13);
}
打印结果:
如有描述理解错误,请大家指出交流!
最后的最后,觉得文章对你有用的小伙伴,留个关注和点赞吧,谢谢支持!