前言
也是偶然的机会,跟一个小伙伴聊了下多数据源、灵活切换数据源的问题。那么,就想起来了,好久前使用过的一款开源的快速开发框架renren框架。其中,正好有一个动态数据源模块。个人,觉得设计的还是不错,采用了Spring的AOP切面,来切入数据源配置。实现原理比较简单,但是很易用。今天,就来跟大家解读下,其内部的实现。
配置
首先,我们简单粗暴地看一下使用该组件,如何配置。
pom文件,引入对应的依赖模块,就完成了对于切面的引入
<dependency> <groupId>io.renren</groupId> <artifactId>renren-dynamic-datasource</artifactId> <version>4.0.0</version> </dependency>
在需要进入多数据源的项目配置文件中,配置数据源内容
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/renren?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: cs123456 initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 #Oracle需要打开注释 #validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* #login-username: admin #login-password: admin filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: false wall: config: multi-statement-allow: true ##多数据源的配置,需要引用renren-dynamic-datasource dynamic: datasource: # 从数据源名称 slave1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/renren1?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true username: iicp_opt_app password: ds#123456 slave2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/renren2?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true username: iicp_opt_app password: ds#123456
主数据源,可以详细配置连接池信息
从数据,无需配置连接池信息,默认已经实现,采用DruidDataSource
上述,内容完成了,完整的配置。
应用
该动态数据源模块,设计为对于确切的服务,实现切面,切换服务内,数据源的灵活使用。所以落脚点,在服务类上。
默认数据源实现如下
@Service public class TestServiceImpl implements TestService { // bussiness code }
动态数据源实现如下
@Service @DataSource("slave1") public class TestServiceImpl implements TestService { // bussiness code }
经过注解@DataSource("数据源名称") 来实现服务内数据源绑定。
从以上示例看,使用非常简单。
本文,主要来剖析实现,因此,我们不再演示实现效果,感兴趣的可以自行测试下。
实现原理剖析
数据源模块包括如下内容:
首先解析下切面的实现,DataSourceAspect
@Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class DataSourceAspect { protected Logger logger = LoggerFactory.getLogger(getClass()); @Pointcut("@annotation(io.renren.commons.dynamic.datasource.annotation.DataSource) " + "|| @within(io.renren.commons.dynamic.datasource.annotation.DataSource)") public void dataSourcePointCut() { } @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { // 完成注解内容获取 MethodSignature signature = (MethodSignature) point.getSignature(); Class targetClass = point.getTarget().getClass(); Method method = signature.getMethod(); DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class); DataSource methodDataSource = method.getAnnotation(DataSource.class); // 方法注解为最小粒度,确定数据源内容 if(targetDataSource != null || methodDataSource != null){ String value; if(methodDataSource != null){ value = methodDataSource.value(); }else { value = targetDataSource.value(); } // 存储数据源 DynamicContextHolder.push(value); logger.debug("set datasource is {}", value); } try { return point.proceed(); } finally { DynamicContextHolder.poll(); logger.debug("clean datasource"); } } }
观察DynamicContextHolder 对于数据源存储的实现
public class DynamicContextHolder { @SuppressWarnings("unchecked") private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() { @Override protected Object initialValue() { return new ArrayDeque(); } }; /** * 获得当前线程数据源 * * @return 数据源名称 */ public static String peek() { return CONTEXT_HOLDER.get().peek(); } /** * 设置当前线程数据源 * * @param dataSource 数据源名称 */ public static void push(String dataSource) { CONTEXT_HOLDER.get().push(dataSource); } /** * 清空当前线程数据源 */ public static void poll() { Deque<String> deque = CONTEXT_HOLDER.get(); deque.poll(); if (deque.isEmpty()) { CONTEXT_HOLDER.remove(); } } }
从上述代码,我们可以看到,采用了ThreadLocal,线程本地变量,来实现存储当前线程数据源内容,以及做了及时的清空。
对于,多数据源实际实现,采用了Spring框架的AbstractRoutingDataSource
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicContextHolder.peek(); } }
那么,有了数据源,我们需要数据源工厂进行相关的引入和配置
@Configuration @EnableConfigurationProperties(DynamicDataSourceProperties.class) public class DynamicDataSourceConfig { @Autowired private DynamicDataSourceProperties properties; @Bean @ConfigurationProperties(prefix = "spring.datasource.druid") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } @Bean public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) { DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(getDynamicDataSource()); //默认数据源 DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties); dynamicDataSource.setDefaultTargetDataSource(defaultDataSource); return dynamicDataSource; } private Map<Object, Object> getDynamicDataSource(){ Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource(); Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size()); dataSourcePropertiesMap.forEach((k, v) -> { DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v); targetDataSources.put(k, druidDataSource); }); return targetDataSources; } }
public class DynamicDataSourceFactory { public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName(properties.getDriverClassName()); druidDataSource.setUrl(properties.getUrl()); druidDataSource.setUsername(properties.getUsername()); druidDataSource.setPassword(properties.getPassword()); druidDataSource.setInitialSize(properties.getInitialSize()); druidDataSource.setMaxActive(properties.getMaxActive()); druidDataSource.setMinIdle(properties.getMinIdle()); druidDataSource.setMaxWait(properties.getMaxWait()); druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis()); druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis()); druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis()); druidDataSource.setValidationQuery(properties.getValidationQuery()); druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout()); druidDataSource.setTestOnBorrow(properties.isTestOnBorrow()); druidDataSource.setTestOnReturn(properties.isTestOnReturn()); druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements()); druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements()); druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements()); try { druidDataSource.setFilters(properties.getFilters()); druidDataSource.init(); } catch (SQLException e) { e.printStackTrace(); } return druidDataSource; } }
关于具体的属性,我们不再细看了。
以上,就是该数据源模块的简单实现。涉及到的内容不是很复杂,但是对于重复的轮子来说,很有参考价值;对于有使用需求的小伙伴,也是很易用。
对于有更复杂的需求,我们可以自行扩展使用。
总结
简单易用的特点,我觉得挺有思考和使用价值,故而分析一下,给大家所分享。作为快速开发框架,有一定的优越性,也值得我们去学习。
