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

来源:互联网 发布:强直性脊柱炎 知乎 编辑:程序博客网 时间:2024/06/14 17:37

一、简述:

数据库应用场景中,经常是“读多写少”,也就是对数据库读取数据压力比较大。有一个解决方案是采用数据库集群方案。
一个数据库是主库,负责写;其他为从库,负责读。实现:读写分离。
那么,对我们的要求是:
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>

参考资料:

  • 使用Spring实现读写分离( MySQL实现主从复制)
  • Spring+MyBatis实现数据库读写分离方案
  • spring+spring mvc +mybatis+druid 实现数据库主从分离
阅读全文
0 0
原创粉丝点击