随着数据量和并发量的增长,数据库会到达瓶颈。这时就需要根据实际情况,对数据库进行读写分离或分库分表,并在运行时能够根据请求动态的切换数据源。
- SpringBoot 2.0.3.RELEASE
- MyBatis
- Druid 1.0.16
- AOP
- Lombok
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>
<dependencies>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.16</version>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependency>
AbstractRoutingDataSource类是Spring提供的抽象类,通过和类DataSourceHolder配合实现动态的切换数据源。
AbstractRoutingDataSource类的成员变量:
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
- targetDataSources 保存key和数据源的映射关系
- defaultTargetDataSource 表示默认的连接
下面继承AbstractRoutingDataSource类,实现自己的选择数据库逻辑
@Getter
@Setter
@ConfigurationProperties(prefix = DynamicDataSource.PREFIX)
public class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware {
public static final String PREFIX = "indi.zqc.dynamic.datasource";
private ApplicationContext applicationContext;
private List<String> routes;
@Override
public void afterPropertiesSet() {
initDataSource();
super.afterPropertiesSet();
}
/**
* 初始化所有数据源和并指定默认数据源
*/
private void initDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
Object defaultTargetDataSource = null;
if (!CollectionUtils.isEmpty(routes)) {
Map<String, DataSource> dataSources = applicationContext.getBeansOfType(DataSource.class);
for (String route : routes) {
DataSource dataSource = dataSources.get(route);
if (dataSource != null) {
targetDataSources.put(route, dataSource);
if (defaultTargetDataSource == null) {
// routes的第一个为默认数据源
defaultTargetDataSource = dataSource;
}
}
}
}
setTargetDataSources(targetDataSources);
setDefaultTargetDataSource(defaultTargetDataSource);
}
/**
* 在访问数据源时,会调用当前方法来判断访问哪个数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.get();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
DataSourceHolder的实现
@Slf4j
public class DataSourceHolder {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public static void set(String dataSource) {
log.error("切换到数据源:{}", dataSource);
context.set(dataSource);
}
public static String get() {
log.error("操作数据源:{}", context.get());
return context.get();
}
public static void clear() {
context.remove();
}
}
如果每次在数据库操作前后分别调用DataSourceHolder.set(dataSource)和DataSourceHolder.get()方法也能够实现切换数据源,但这就很麻烦。所以这里借助注解和Spring的切面解决这个问题。
自定义注解DataSource
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value();
}
增加切面,定义切面时需要指定Order,使该切面在事务@Transactional前执行
@Order(2)
@Aspect
@EnableAspectJAutoProxy
public class DynamicDataSourceAspect {
@Pointcut("@annotation(indi.zqc.dynamic.datasource.annotation.DataSource)")
private void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint point) {
String dataSource = null;
// 获取方法上的注解信息
Signature signature = point.getSignature();
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
DataSource annotation = methodSignature.getMethod().getAnnotation(DataSource.class);
if (annotation != null && StringUtils.isNotBlank(annotation.value())) {
dataSource = annotation.value();
}
}
// 设置数据源
DataSourceHolder.set(dataSource);
}
@After("pointcut()")
public void after(JoinPoint point) {
DataSourceHolder.clear();
}
}
public class DynamicDataSourceConfiguration {
@Bean(name = "db1")
@ConfigurationProperties(prefix = DynamicDataSource.PREFIX + ".db1")
public DataSource dataSource1() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = DynamicDataSource.PREFIX + ".db2")
public DataSource dataSource2() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
@Bean
@Primary
public DynamicDataSource dynamicDataSource() {
return new DynamicDataSource();
}
@Bean
public DynamicDataSourceAspect dynamicDataSourceAspect() {
return new DynamicDataSourceAspect();
}
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
indi.zqc.dynamic.datasource.routes=db1,db2
indi.zqc.dynamic.datasource.db1.db-type=mysql
indi.zqc.dynamic.datasource.db1.driver-class-name=com.mysql.jdbc.Driver
indi.zqc.dynamic.datasource.db1.url=jdbc:mysql://******/db1?useSSL=true
indi.zqc.dynamic.datasource.db1.username=******
indi.zqc.dynamic.datasource.db1.password=******
indi.zqc.dynamic.datasource.db2.db-type=mysql
indi.zqc.dynamic.datasource.db2.driver-class-name=com.mysql.jdbc.Driver
indi.zqc.dynamic.datasource.db2.url=jdbc:mysql://******/db2?useSSL=true
indi.zqc.dynamic.datasource.db2.username=******
indi.zqc.dynamic.datasource.db2.password=******
- indi.zqc.dynamic.datasource.routes指定生效的数据源,第一个为默认数据源
@Transactional
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 注解方式访问数据源db1
*/
@DataSource("db1")
@Override
public User getUser(int id) {
return userDao.getUser(id);
}
/**
* 注解方式访问数据源db2
*/
@DataSource("db1")
@Override
public void insertUser(User user) {
userDao.insertUser(user);
}
/**
* 注解方式访问数据源db2
*/
@DataSource("db2")
@Override
public void updateUser(User user) {
userDao.updateUser(user);
}
}