通过Spring实现应用程序的读写分离,实现原理是采用AOP。在方法执行之前选择对应的数据源,而在Spring中刚好有对应的动态数据源抽象类:
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
配置数据源
#读库配置
jdbc.read.driver=com.mysql.cj.jdbc.Driver
jdbc.read.url=${jdbc.read.url}
jdbc.read.username=${jdbc.read.username}
jdbc.read.password=${jdbc.read.password}
#写库配置
jdbc.write.driver=com.mysql.cj.jdbc.Driver
jdbc.write.url=${jdbc.write.url}
jdbc.write.username=${jdbc.write.username}
jdbc.write.password=${jdbc.write.password}
applicationContext.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<description>Spring公共配置</description>
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<!-- 使用annotation定义事务 -->
<tx:annotation-driven proxy-target-class="true"/>
<!-- 数据源配置, 使用alibaba druid连接池 -->
<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- Connection Info -->
<property name="driverClassName" value="${jdbc.write.driver}"/>
<property name="url" value="${jdbc.write.url}"/>
<property name="username" value="${jdbc.write.username}"/>
<property name="password" value="${jdbc.write.password}"/>
</bean>
<bean id="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- Connection Info -->
<property name="driverClassName" value="${jdbc.read.driver}"/>
<property name="url" value="${jdbc.read.url}"/>
<property name="username" value="${jdbc.read.username}"/>
<property name="password" value="${jdbc.read.password}"/>
</bean>
<!-- 动态数据源,根据service接口上的注解来决定取哪个数据源 -->
<bean id="dataSource" class="com.snail.config.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- master -->
<entry key="master" value-ref="slaveDataSource"/>
<!-- slave -->
<entry key="slave" value-ref="slaveDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="master"/>
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 声明式开启 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" order="2"/>
<!-- 为业务逻辑层的方法解析@DataSource注解 为当前线程的HandleDataSource注入数据源 -->
<bean id="dataSourceAspect" class="com.snail.config.DataSourceAspect"/>
<aop:config proxy-target-class="true">
<aop:aspect id="dataSourceAspect" ref="dataSourceAspect" order="1">
<aop:pointcut id="tx" expression="@annotation(com.snail.config.DataSource) "/>
<aop:before pointcut-ref="tx" method="before"/>
</aop:aspect>
</aop:config>
</beans>
如果我们有仔细看applicationContext.xml文件,我们会发现有以下三个类
com.snail.config.DynamicDataSource
com.snail.config.DataSourceAspect
com.snail.config.DataSource
DynamicDataSource.java
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 获取与数据源相关的key 此key是Map<String,DataSource> resolvedDataSources 中与数据源绑定的key值
* 在通过determineTargetDataSource获取目标数据源时使用
*/
@Override
protected Object determineCurrentLookupKey() {
String dataSourceKet = HandleDataSource.getDataSource();
log.debug("使用数据源:{}", dataSourceKet);
return dataSourceKet;
}
}
DataSourceAspect.java
@Slf4j
public class DataSourceAspect {
/**
* 在service层方法获取datasource对象之前,在切面中指定当前线程数据源
*/
public void before(JoinPoint point) {
//获取实现类方法上的注解消息
Method method = ((MethodSignature) point.getSignature()).getMethod();
try {
if (method != null && method.isAnnotationPresent(DataSource.class)) {
DataSource data = method.getAnnotation(DataSource.class);
log.debug("数据库切换类型:{}:{},{}", point.getTarget().getClass().getName(), method.getName(), data.value());
// 数据源放到当前线程中
HandleDataSource.putDataSource(data.value().toString());
}
} catch (Exception e) {
log.debug("异常时使用数据库类型:{}:{},{}", point.getTarget().getClass().getName(), method.getName(), DBTarget.write.toString());
HandleDataSource.putDataSource("master");
}
}
}
DataSource.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
String value();
}
HandleDataSource.java
public class HandleDataSource {
public static final ThreadLocal<String> holder = new ThreadLocal<>();
/**
* 绑定当前线程数据源
*
* @param datasource
*/
public static void putDataSource(String datasource) {
holder.set(datasource);
}
/**
* 获取当前线程的数据源
*
* @return
*/
public static String getDataSource() {
return holder.get();
}
public static void clear() {
holder.remove();
}
}
在service服务上指定数据源:
public class SnailService {
/**
* 通过id查询信息
* @param snailId 蜗牛ID
*/
@DataSource("slave")
Snail querySnail(Long snailId){
}
/**
* 存储蜗牛信息
* @param snail 蜗牛信息
*/
@DataSource("master")
Snail saveSnail(Snail snail){
}
}
以上通过Spring Aop方式完成了数据库的读写分离操作,需要注意的是事务和读写分离在Aop的织入顺序问题。
要在事务开始之前完成对读写数据源的选择,否则在事务开始后,则读写分离不起作用,此时可以利用order关键字控制Aop的织入顺序。