Spring + Mybatis环境实现Mysql数据库主从切换

1 篇文章 0 订阅
1 篇文章 0 订阅
一、简述:

数据库应用场景中,经常是“读多写少”,也就是对数据库读取数据压力比较大。有一个解决方案是采用数据库集群方案。
一个数据库是主库,负责写;其他为从库,负责读。实现:读写分离。
那么,对我们的要求是:
1. 读库和写库的数据一致;
2. 写数据必须写到写库;
3. 读数据必须到读库;

二、方案:

实现读写分离有两种方案:应用层解决和中间件解决;
本篇,介绍使用Spring方式,实现应用层解决方式。

三、原理:

在进入Service之前,使用AOP来做出判断,是使用写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库

说明:也可使用mybatis插件方式,通过判断语句INSERT,UPDATE,DELETE,SELECT判断;(推荐)

image
image

四、实际解决方案:
  • 方案一:Spring AOP注解方式;
  • 方案二:Spring AOP接口方法名限定方式;
  • 方案三:Mybatis 插件判断SQL语句方式;(推荐)
以下使用方案三:

参与角色:

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<String> 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<String, RouteDataSourceKeyEnum> 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文件:

<?xml version="1.0" encoding="UTF-8"?>
<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>

    <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 自动扫描mapping.xml文件 -->
        <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>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.**.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

    <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

参考资料:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值