前提mysql
这几天,学员们反馈但愿学习一下自定义注解,正好准备高并发课程内容里有一块涉及到使用自定义注解完成数据库切库的内容。这里单独写一篇文章记录说明一下。spring
为何会有数据库切库一说sql
首先,许多项目都有主库与从库,有的主库后面甚至会有不少个从库,主从库之间的一般同步也很快,这为数据库切库提供了一个基础,由于能够去不一样的数据库查询,获得相同的结果(若是不一样的数据库是彻底不一样的,这个不在咱们这篇文章讨论的范围以内,那个属于让项目支持多个数据源)数据库
其次,随着项目愈来愈大、操做的用户愈来愈多,对数据库的请求操做愈来愈多,很容易想到的是将读写请求分开,将写请求交给主库处理,读请求直接从某个从库读取。这样能够极大的减小大量对主库的请求,提高主库的性能。安全
接下来具体说一下如何经过自定义注解完成切库(代码使用springboot实现):springboot
第一步、定义咱们本身的切库注解类mybatis
自定义注解有几点须要注意:多线程
1)@Target 是做用的目标,接口、方法、类、字段、包等等,具体看:ElementType并发
2)@Retention 是注解存在的范围,RUNTIME表明的是注解会在class字节码文件中存在,在运行时能够经过反射获取到,具体看:RetentionPolicyapp
3)容许的变量,一般都要给定默认值,好比咱们使用一个service时,能够@Service,也能够@Service(xxxx)
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD
})
public @interface RoutingDataSource {
String value() default DataSources.MASTER_DB;
}
第二步、定义须要使用的数据库及配置
一、数据库配置:application.properties,这里要注意不一样db的前缀区别
## datasource master #
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/master?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=466420182
## datasource slave #
spring.datasourceSlave.type=com.alibaba.druid.pool.DruidDataSource
spring.datasourceSlave.driver-class-name=com.mysql.jdbc.Driver
spring.datasourceSlave.url=jdbc:mysql://localhost:3306/slave?characterEncoding=UTF-8
spring.datasourceSlave.username=root
spring.datasourceSlave.password=466420182
二、定义支持的数据源id:
public interface DataSources {
String MASTER_DB = masterDB;
String SLAVE_DB = slaveDB;
}
三、定义数据库实体类并配置为多数据源的形式
这里不要忽略了经过 MapperScan 指定须要扫描的mybatis的接口类
@Configuration
public class DatasourceConfig {
//destroy-method=close的做用是当数据库链接不使用的时候,就把该链接从新放到数据池中,方便下次使用调用.
@Bean(destroyMethod = close, name = DataSources.MASTER_DB)
@ConfigurationProperties(prefix = spring.datasource)
public DataSource dataSource() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
@Bean(destroyMethod = close, name = DataSources.SLAVE_DB)
@ConfigurationProperties(prefix = spring.datasourceSlave)
public DataSource dataSourceSlave() {
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
}
四、配置成动态数据源:
@Configuration
@MapperScan(basePackages = {com.xxx.dao}) // 这里须要替换为实际的路径
public class MybatisConfig {
@Autowired
@Qualifier(Datasources.MASTER_DB)
private DataSource masterDB;
@Autowired
@Qualifier(DataSources.SLAVE_DB)
private DataSource slaveDB;
/**
* 动态数据源
*/
@Bean(name = dynamicDataSource)
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(masterDB);
// 配置多数据源
MapdsMap = Maps.newHashMap();
dsMap.put(DataSources.MASTER_DB, masterDB);
dsMap.put(DataSources.SLAVE_DB, slaveDB);
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
@Bean
@ConfigurationProperties(prefix = mybatis)
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源,此处配置为关键配置,若是没有将 dynamicDataSource 做为数据源则不能实现切换
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}
}
第三步、使用ThreadLocal安全的管理当前进程使用的数据源链接
@Slf4j
public class DataSourceContextHolder {
/**
* 默认数据源
*/
public static final String DEFAULT_DATASOURCE = DataSources.MASTER_DB;
private static final ThreadLocalcontextHolder = new ThreadLocal();
// 设置数据源名
public static void setDB(String dbType) {
log.debug(切换到{}数据源, dbType);
contextHolder.set(dbType);
}
// 获取数据源名
public static String getDB() {
return (contextHolder.get());
}
// 清除数据源名
public static void clearDB() {
contextHolder.remove();
}
}
第四步、经过编写切面,对全部咱们自定义切库注解的方法进行拦截,动态的选择数据源
这里是为下一步提供铺垫,动态调整DataSourceContextHolder里存储的值,使用threadLocal来管理是为了不多线程之间互相影响。
自定义注解,核心的处理就是写处理这个注解的逻辑,而后经过指定的拦截方案根据当前的数据作一些动态的处理。好比Spring提供的@Controller、@Service等注解,都是须要咱们在配置文件里配置好须要扫描的路径,而后项目启动时,spring根据配置去指定路径读取这些配置,而后这些类才能够被spring进行管理。
这里不要忽略了默认数据源要选择主库,若是切库出现什么问题,好比配置错误等,能够保证访问主库来获得正确的结果;另外,请求完了不要忘记调用提供的clearDB的操做,防止threadLocal误用带来的内存泄露。
@Aspect
@Component
@Slf4j
public class DynamicDataSourceAspect {
@Before(@annotation(RoutingDataSource))
public void beforeSwitchDS(JoinPoint point){
//得到当前访问的class
Class className = point.getTarget().getClass();
//得到访问的方法名
String methodName = point.getSignature().getName();
//获得方法的参数的类型
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
String dataSource = DataSourceContextHolder.DEFAULT_DATASOURCE;
try {
// 获得访问的方法对象
Method method = className.getMethod(methodName, argClass);
// 判断是否存在@DS注解
if (method.isAnnotationPresent(RoutingDataSource.class)) {
RoutingDataSource annotation = method.getAnnotation(RoutingDataSource.class);
// 取出注解中的数据源名
dataSource = annotation.value();
}
} catch (Exception e) {
log.error(routing datasource exception, + methodName, e);
}
// 切换数据源
DataSourceContextHolder.setDB(dataSource);
}
@After(@annotation(RoutingDataSource))
public void afterSwitchDS(JoinPoint point){
DataSourceContextHolder.clearDB();
}
}
第五步、动态的取出咱们在切面里设置的数据源的字符串便可
这里须要把原理介绍一下,在链接数据库时实际上是先选择一个配置好的spring管理的datasource的id,就是咱们以前在 DatasourceConfig 类里定义的Datasource实体类的id:masterDB 和 slaveDB。而后根据id去spring的上下文选择配置,进行数据库链接。有兴趣的能够看一下源码。
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
log.debug(数据源为{}, DataSourceContextHolder.getDB());
return DataSourceContextHolder.getDB();
}
}
第六步、取消自动配置数据源,使用咱们这里定义的数据源配置
在SpringBoot启动类上一般直接使用@SpringBootApplication就能够了,这里须要调整为:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class
})
使用
如何使用呢,咱们简单演示一下:
@Service
public class DataSourceRoutingService {
@Resource
private SysUserMapper sysUserMapper;
@RoutingDataSource(DataSources.MASTER_DB) // 这个注解这时是能够省略,由于默认就是访问主库
public SysUser test1(int id) {
return sysUserMapper.selectByPrimaryKey(id);
}
@RoutingDataSource(DataSources.SLAVE_DB)
public SysUser test2(int id) {
return sysUserMapper.selectByPrimaryKey(id);
}
}
如此,数据库切库就OK了。若是你的系统已经有主库、从库之分了,那么赶忙在你的系统里利用起来吧。
扩展
这里呢,还能够支持多个扩展。好比如今一个主库后面有多个从库,在切面拿到须要切换从库时,还能够选择随机选择一个,或者根据类名、方法名或业务配置等选择某一个从库,这样不但能够分担每一个从库的压力,也能够有针对性的让指定的读请求打到指定的从库上。若是有多个主库,也能够有更多的选择~