数据库应用场景中,经常是“读多写少”,也就是对数据库读取数据压力比较大。有一个解决方案是采用数据库集群方案。
一个数据库是主库,负责写;其他为从库,负责读。实现:读写分离。
那么,对我们的要求是:
1. 读库和写库的数据一致;
2. 写数据必须写到写库;
3. 读数据必须到读库;
实现读写分离有两种方案:应用层解决和中间件解决;
本篇,介绍使用Spring方式,实现应用层解决方式。
在进入Service之前,使用AOP来做出判断,是使用写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库
说明:也可使用mybatis插件方式,通过判断语句INSERT,UPDATE,DELETE,SELECT判断;(推荐)
参与角色:
1、RouteDataSourceKeyEnum 主从数据库枚举
2、RouteDataSource 动态切换数据源
3、RouteDataSourcePlugin MyBtais拦截插件
4、spring-mybatis.xml 数据源,切换类配置文件
1、RouteDataSourceKeyEnum.java文件:
/**
* @author devin
* @date 2017/11/1
*/
public enum RouteDataSourceKeyEnum {
MASTER, SLAVE
}
2、RouteDataSource.java文件:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author devin
* @date 2017/10/23
*/
public class RouteDataSource extends AbstractRoutingDataSource {
/**
* dbKey线程安全容器
*/
private static ThreadLocal holder = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return holder.get();
}
/**
* 设置dbKey
*
* @param dbKey
*/
public static void setDbKey(RouteDataSourceKeyEnum dbKey) {
holder.set(dbKey.name());
}
}
3、RouteDataSourcePlugin.java文件:
import com.framework.datasource.DynamicDataSourceGlobal;
import com.framework.datasource.DynamicDataSourceHolder;
import com.framework.util.GlobalKeys;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author devin
* @date 2017/10/23
* 在DefaultSqlSession的insert,delete方法也是调用了update方法
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class})})
public class RouteDataSourcePlugin implements Interceptor {
protected static final Logger logger = LoggerFactory.getLogger(RouteDataSourcePlugin.class);
private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
private static final Map cacheMap = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
//常量切换 生产 测试库
boolean gamma = Boolean.parseBoolean(GlobalKeys.getString("db.source.switch"));
if (gamma) {
RouteDataSource.setDbKey(RouteDataSourceKeyEnum.MASTER);
return invocation.proceed();
} else if (!gamma) {
RouteDataSource.setDbKey(RouteDataSourceKeyEnum.SLAVE);
return invocation.proceed();
}
//它首先查看当前是否存在事务管理上下文,并尝试从事务管理上下文获取连接,如果获取失败,直接从数据源中获取连接。
//在获取连接后,如果当前拥有事务上下文,则将连接绑定到事务上下文中。(此处直接继续下一过程)
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if (!synchronizationActive) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
RouteDataSourceKeyEnum routeDataSourceKeyEnum = null;
if ((routeDataSourceKeyEnum = cacheMap.get(ms.getId())) == null) {
//读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
//!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
routeDataSourceKeyEnum = RouteDataSourceKeyEnum.MASTER;
} else {
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
if (sql.matches(REGEX)) {
routeDataSourceKeyEnum = RouteDataSourceKeyEnum.MASTER;
} else {
routeDataSourceKeyEnum = RouteDataSourceKeyEnum.SLAVE;
}
}
} else {
routeDataSourceKeyEnum = RouteDataSourceKeyEnum.MASTER;
}
logger.warn("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), routeDataSourceKeyEnum.name(), ms.getSqlCommandType().name());
cacheMap.put(ms.getId(), routeDataSourceKeyEnum);
}
RouteDataSource.setDbKey(routeDataSourceKeyEnum);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
//
}
}
4、spring-mybatis.xml文件:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:jdbc.properties"/>
bean>
<bean id="masterDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="initialSize" value="${initialSize}">property>
<property name="maxActive" value="${maxActive}">property>
<property name="maxIdle" value="${maxIdle}">property>
<property name="minIdle" value="${minIdle}">property>
<property name="maxWait" value="${maxWait}">property>
bean>
<bean id="slaveDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url_slave}"/>
<property name="username" value="${username_slave}"/>
<property name="password" value="${password_slave}"/>
<property name="initialSize" value="${initialSize}">property>
<property name="maxActive" value="${maxActive}">property>
<property name="maxIdle" value="${maxIdle}">property>
<property name="minIdle" value="${minIdle}">property>
<property name="maxWait" value="${maxWait}">property>
bean>
<bean id="dataSource" class="com.framework.routedb.RouteDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="MASTER" value-ref="masterDataSource"/>
<entry key="SLAVE" value-ref="slaveDataSource"/>
map>
property>
<property name="defaultTargetDataSource" ref="masterDataSource">property>
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/**/*.xml">property>
<property name="configLocation" value="classpath:mybatis-config.xml">property>
<property name="plugins">
<list>
<bean class="com.huxin.assets.framework.routedb.RouteDataSourcePlugin">bean>
list>
property>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.**.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory">property>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
beans>
参考资料: