【spring系列】之15:spring实现数据源动态选择

现在一般的数据库读写分离都有很多现成的工具去做了,但是有时候,在业务量不是特别特别大,又需要做读写分离的时候,我们可以自己动手一个基于AOP切面的数据源动态选择。

既然提到多数据源了,那就会涉及到数据源的选择时机问题。在自己动手做的时候,也现在网速百度了下,基本有以下几种情况出现:

1.mapper接口定义两套

2.在mapper层做动态选择

3.在serivce层做动态选择

 

先简单比较一下三种方法:

第一种,定义两套接口的方式,可以实现功能,而且强保证增删改走写库,查走读库,若系统很小,又不会其他的方式实现,可以尝试这样用,但是开发的工作量会很大,后期的接口维护也会令你头大的,因此,此种方法要慎重。

 

第二种,在mapper层做数据源选择,和第一种相比,开发量会减少,写的好点的代码,也能强保证增删改走写库,查走读库,但是事务方面,因为夸库了,所以不能保证事务的正确性。可能有同学会说,写操作已经在同一个库实现了,怎么会出现事务问题?

那就得这样想了:主从的数据库,数据同步会有延迟,如果在某些特性条件下,需要是查询结果来判断是否需要回滚,那是不是就会出现错误的事务了。这是其一;其二:如果有两个写库呢,你在哪执行事务?因此这个方式也不推荐。

 

第三种,也就是我们推荐的方式了,在service层选择数据源,我们可以就在一个事务中执行代码了,不会存在夸库的情况。但是这个也不是十分完美,或者说是百分百读写分离的。因为在同一个代码块或者事务中,是可能有读也有写的。

下面就来具体实现啦:

步驟一:創建我們的DataSource注解,包含一个DataSourceTypeEnum类型的属性。默认为MASTER

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {

	DataSourceTypeEnum value() default DataSourceTypeEnum.MASTER;
}
/**
 * 
 * MASTER:WRITE<BR>
 * SLAVE:READ
 * 
 * @author licw
 *
 * @version 1.0.0
 */
public enum DataSourceTypeEnum {
	MASTER, SLAVE;
}

用法:在我们的service层的方法,添加我们自定义的数据源注解,MASTER表示写库,SLAVE表示读库。

步骤二:继承spring中的AbstractRoutingDataSource(数据源路由器)并重写determineCurrentLookupKey方法

public class DynamicDataSource extends AbstractRoutingDataSource {

	/**
	 * 获取与数据源相关的key. <br>
	 * 此key是Map<String,DataSource> resolvedDataSources 中与数据源绑定的key值<br>
	 * 在通过determineTargetDataSource获取目标数据源时使用
	 */
	@Override
	protected Object determineCurrentLookupKey() {

		return HandleDataSource.getDataSource();
	}

}

看AbstractRoutingDataSource的源码可以发现,多数据源是存放在一个map中的,我们设置的key就是对象map中数据源的key

源码如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

	@Nullable
	private Map<Object, Object> targetDataSources;

	@Nullable
	private Object defaultTargetDataSource;

	private boolean lenientFallback = true;

	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

	@Nullable
	private Map<Object, DataSource> resolvedDataSources;

	@Nullable
	private DataSource resolvedDefaultDataSource;

         。。。。
	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}
}

所有配置的数据源,均是保存在Map<Object, DataSource> resolvedDataSources中,

determineTargetDataSource方法返回map的key,后面我们就是通过key获取的具体数据源。

接下来再看一下HandleDataSource是个什么鬼?

/**
 * 保存当前数据源的key
 * 
 * @author licw
 *
 * @version 1.0.0
 */
public class HandleDataSource {
	public static final ThreadLocal<String> holder = new ThreadLocal<String>();

	/**
	 * 获取当前线程的数据源
	 * 
	 * @param dataSourceType      读写类型判断
	 */
	public static void putDataSource(DataSourceTypeEnum dataSourceType) {

		if (DataSourceTypeEnum.MASTER.equals(dataSourceType)) {
			holder.set("write01");
		} else {
			holder.set("read01");
		}

	}

	/**
	 * 获取当前线程的数据源路由的key
	 * 
	 * @return
	 */
	public static String getDataSource() {
		return holder.get();
	}

}

发现这个是一个用ThreadLocal<String>保存我们map中DataSource对应的key,包含两个静态方法:

putDataSource(DataSourceTypeEnum dataSourceType)和getDataSource(),

putDataSource是设置我们当前线程的key,getDataSource是获取我们当前线程的key。获取的我们已经在上面的代码里看到了,那设置是在哪里设置的呢?(很多人知道ThreadLocal却从来没有用过,今天是不是用到了哈)

 

步骤三:通过切面拦截,设置我们的DataSource的key

先看代码:

public class DataSourceAspect {

	private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

	/**
	 * 在dao层方法之前获取datasource对象之前在切面中指定当前线程数据源路由的key
	 */
	public void before(JoinPoint point) {
		// 获取切点
		Object target = point.getTarget();
		if (logger.isDebugEnabled()) {
			logger.info("start to select datasource.");
		}

		// 获取方法的名字
		String method = point.getSignature().getName();
		Class<?> classz = target.getClass();
		// 获取方法上的参数
		Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
		try {
			// 获取方法
			Method m = classz.getMethod(method, parameterTypes);
			// 判断方法是否存在,并且判断是否有DataSource这个注释。
			if (m != null && m.isAnnotationPresent(DataSource.class)) {
				// 获取注解
				DataSource data = m.getAnnotation(DataSource.class);
				if (logger.isDebugEnabled()) {
					logger.info("target class is {},method is {},selected datasource type is {}", target.toString(),
							m.getName(), data.value());
				}

				// 设置数据源
				HandleDataSource.putDataSource(data.value());
			}
		} catch (Exception e) {
			e.printStackTrace();
			logger.error("target class is {},method is {},selected datasource throw a exception:{} ", target.toString(),
					method, e);
		}
	}

}

大概的意思也就是先获得我们拦截的方法,继而获得方法上的@DataSource注解的值,然后调用HandleDataSource的putDataSource方法,设置当前线程要选择的具体数据源的key。

步骤四:配置我们的读写数据源

这个就直接上配置了

	<bean id="dataSource" class="com.ddc.mcn.service.util.DynamicDataSource">
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<!-- write -->
				<entry key="write01" value-ref="write01DataSource" />
				<!--  
				<entry key="write02" value-ref="write02DataSource" />
				-->
				<!-- read -->
				<entry key="read01" value-ref="read01DataSource" />
				<!-- 
				<entry key="read02" value-ref="read02DataSource" />
				 -->
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="write01DataSource" />
	</bean>

DynamicDataSource继承了AbstractRoutingDataSource,targetDataSources是我们提供的多个数据源,defaultTargetDataSource是我们默认走的数据源。

上面的代码说了,数据源是保存在resolvedDataSources属性中的,这里怎么又成了targetDataSources属性?我们可看一下源码,里面有这样一段:

	@Override
	public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
		this.targetDataSources.forEach((key, value) -> {
			Object lookupKey = resolveSpecifiedLookupKey(key);
			DataSource dataSource = resolveSpecifiedDataSource(value);
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}

由于AbstractRoutingDataSource实现了InitializingBean接口,在spring容器其中时,在初始化的过程中就会调用afterPropertiesSet方法,而这个方法,就是将配置的targetDataSources,循环设置到了resolvedDataSources中,将

defaultTargetDataSource设置到resolvedDefaultDataSource中。

下面具体数据源的配置样例:

	<!-- 读数据源:基于Druid数据库链接池的数据源配置 -->
	<bean id="read01DataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
		<!-- 基本属性driverClassName、 url、user、password -->
		<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
		<property name="url" value="${apollo.dataSource.read01.jdbc.url}" />
		<property name="username" value="${apollo.dataSource.read01.jdbc.username}" />
		<property name="password" value="${apollo.dataSource.read01.jdbc.password}" />
		<!-- 配置初始化大小、最小、最大 -->
		<!-- 通常来说,只需要修改initialSize、minIdle、maxActive -->
		<property name="initialSize" value="5" />
		<property name="maxActive" value="50" />
		<property name="minIdle" value="5" />
		<!-- 配置获取连接等待超时的时间 -->
		<property name="maxWait" value="60000" />
		<property name="poolPreparedStatements" value="false" />
		<property name="maxPoolPreparedStatementPerConnectionSize" value="0" />
		<property name="validationQuery" value="SELECT 'x'" />
		<property name="validationQueryTimeout" value="5" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />
		<property name="testWhileIdle" value="true" />
		<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
		<property name="minEvictableIdleTimeMillis" value="300000" />
		<property name="maxEvictableIdleTimeMillis" value="1800000" />
		<property name="removeAbandoned" value="true" />
		<property name="filters" value="stat" />
		<!-- 解密密码必须要配置的项 
		<property name="filters" value="config" /> 
		<property name="connectionProperties" value="config.decrypt=true" /> 
		-->
	</bean>

最后,我们只要将我们的切面加入到容器中就可以了。

  <!-- 为业务逻辑层的方法解析@DataSource注解  为当前线程的routeholder注入数据源key --> 
  <bean id="dataSourceAspect" class="com.ddc.mcn.service.aop.DataSourceAspect" />



	<aop:config proxy-target-class="true" >
		<aop:aspect ref="dataSourceAspect" order="1">
		    <aop:pointcut expression="execution(* com.ddc.mcn.service..*.*(..)) || @annotation(com.ddc.mcn.service.annotation.DataSource)" id="tx"/>
		    <aop:before method="before" pointcut-ref="tx"/>
		</aop:aspect>
	</aop:config> 

到此,我们手写的数据源动态选择功能就完成了。

	@Override
	@DataSource(DataSourceTypeEnum.SLAVE)
	public List<MenuResponseDTO> list() {
		List<MenuResponseDTO> res = new ArrayList<MenuResponseDTO>();
		List<MenuDO> menus = menuMapper.list(new HashMap<String, Object>(16));
		if (menus != null && menus.size() != 0) {
			for (MenuDO m : menus) {
				MenuResponseDTO t = new MenuResponseDTO();
				BeanUtils.copyProperties(m, t);
				res.add(t);
			}
		}
		return res;
	}

在我们的方法上加上我们自定义注解,就ok了。

 

总结:

实现这个功能需要以下几个小技能

1.线程相关的ThreadLocal使用

2.spring的切面

3.要了解AbstractRoutingDataSource抽象类

4.自定义注解

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值