spring +mybatis实现数据库主从复制

本文介绍如何在SSM框架中实现动态数据源切换,包括动态数据源配置、线程安全的数据源持有者和拦截器实现,以及Spring配置文件和MyBatis插件的设置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ssm搭建  略

需要三个java文件

1.DynamicDataSource.java

package com.sykj.plugin;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import javax.sql.DataSource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.ReflectionUtils;

/**
 * 动态数据源设置
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Integer slaveCount;

    // 轮询计数,初始为-1,AtomicInteger是线程安全的
    private AtomicInteger counter = new AtomicInteger(-1);

    // 记录读库的key
    private List<Object> slaveDataSources = new ArrayList<Object>(0);


    @Override
    protected Object determineCurrentLookupKey() {
        //如果使用主库,则直接返回
        if (DynamicDataSourceHolder.isMaster()) {
            return DynamicDataSourceHolder.getDbType();
        }
        //如果不是主库进行轮询选择从库
        return getSlaveKey();
    }

    /**
     * 该方法会在Spring Bean 加载初始化的时候执行,功能和 bean 标签的属性 init-method 一样
     * 把所有的slave库放到slaveDataSources里
     */
    @SuppressWarnings("unchecked")
    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();

        // 由于父类的resolvedDataSources属性是私有的子类获取不到,需要使用反射获取
        Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources");
        field.setAccessible(true); // 设置可访问

        try {
            Map<Object, DataSource> resolvedDataSources = (Map<Object, DataSource>) field.get(this);
            // 读库的数据量等于数据源总数减去写库的数量
            this.slaveCount = resolvedDataSources.size() - 1;
            for (Map.Entry<Object, DataSource> entry : resolvedDataSources.entrySet()) {
                if (DynamicDataSourceHolder.DB_MASTER.equals(entry.getKey())) {
                    continue;
                }
                slaveDataSources.add(entry.getKey());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 轮询算法实现
     *
     * @return
     */
    public Object getSlaveKey() {
        // 得到的下标为:0、1、2、3……
        Integer index = counter.incrementAndGet() % slaveCount;
        if (counter.get() > 9999) { // 以免超出Integer范围
            counter.set(-1); // 还原
        }
        return slaveDataSources.get(index);
    }
}

2.DynamicDataSourceHolder.java

package com.sykj.plugin;

/**
 * 动态数据源类持有者,线程安全
 * 持有动态数据源的信息,以及修改清理数据源
 */
public class DynamicDataSourceHolder {
    //线程安全
    private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    public static final String DB_MASTER = "master";
    public static final String DB_SLAVE = "slave";


    /**
     * 0
     * 获取线程的dbType
     *
     * @return
     */
    public static String getDbType() {
        String db = contextHolder.get();
        if (db == null)
            db = DB_MASTER;
        System.out.println("所使用的数据源为:" + db);
        return db;
    }

    /**
     * 设置线程的dbType
     *
     * @param datasource 数据源类型
     */
    public static void setDbType(String datasource) {

        contextHolder.set(datasource);
    }

    /**
     * 清理连接类型
     */
    public static void clearDbType() {
        contextHolder.remove();
    }

    //判断是否是使用主库,提高部分使用
    public static boolean isMaster() {
        return DB_MASTER.equals(getDbType());
    }
}

3.DynamicDataSourceInterceptor.java

package com.sykj.plugin;

import java.util.Locale;
import java.util.Properties;

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.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * 对mybatis中操作进行拦截
 * 增删改使用master,查询使用slave
 */
@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 DynamicDataSourceInterceptor implements Interceptor {
    //sql匹配规则
    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
    /**
     * 进行拦截操作,增删改和事务操作使用master,查询使用slave,里面具体的一下实现代码,感兴趣可以学习mybatis源码去理解
     *你也可以根据自己的实际业务逻辑去控制
     * @param invocation
     * @return
     * @throws Throwable
     */
    public Object intercept(Invocation invocation) throws Throwable {

        //是否是用事务管理
        boolean syschronizationActive = TransactionSynchronizationManager.isActualTransactionActive();
        Object[] objects = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) objects[0];
        String lookupKey = DynamicDataSourceHolder.DB_MASTER;

        if (syschronizationActive != true) {

            //读方法
            if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                //如果selectKey为自增id查询主键(SELECT LAST INSERT_ID)方法,使用主库
                if (mappedStatement.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                    lookupKey = DynamicDataSourceHolder.DB_MASTER;
                } else {
                    BoundSql boundSql = mappedStatement.getSqlSource().getBoundSql(objects[1]);
                    String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                    if (sql.matches(REGEX)) {
                        lookupKey = DynamicDataSourceHolder.DB_MASTER;
                    } else {
                        lookupKey = DynamicDataSourceHolder.DB_SLAVE;
                    }
                }

            }
        } else {
            lookupKey = DynamicDataSourceHolder.DB_MASTER;
        }
        System.out.println("设置方法:"+mappedStatement.getId()+"; use:"+lookupKey+"; Strategy,SqlCommanType:"+mappedStatement.getSqlCommandType().name());
        DynamicDataSourceHolder.setDbType(lookupKey);
        return invocation.proceed();
    }

    /**
     * 设置拦截对象
     * Executor在mybatis中是用来增删改查的,进行拦截
     *
     * @param target 拦截的对象
     * @return
     */
    public Object plugin(Object target) {
        if (target instanceof Executor)
            return Plugin.wrap(target, this);
        else
            return target;
    }
    public void setProperties(Properties properties) {}
}

最后修改spring配置文件,添加多个数据源

<!-- 抽象数据源,提供继承使用 -->
    <bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
       <!-- 配置初始化大小、最小、最大 -->
		<property name="initialSize" value="10" />
		<property name="minIdle" value="10" />
		<property name="maxActive" value="50" />
		<!-- 配置获取连接等待超时的时间 -->
		<property name="maxWait" value="60000" />
		<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
		<property name="minEvictableIdleTimeMillis" value="300000" />
		<property name="validationQuery" value="SELECT 'x'" />
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />
		<!-- 打开PSCache,并且指定每个连接上PSCache的大小 如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。 -->
		<property name="poolPreparedStatements" value="false" />
		<property name="maxPoolPreparedStatementPerConnectionSize"
			value="20" />
		<!-- 配置监控统计拦截的filters -->
		<property name="filters" value="wall,stat" />
    </bean>
 <bean id="master" parent="abstractDataSource">
        <!-- 基本属性 url、user、password -->
		<property name="driverClassName" value="${jdbc_driverClassName}" />
		<property name="url" value="${jdbc_url}" />
		<property name="username" value="${jdbc_username}" />
		<property name="password" value="${jdbc_password}" />
    </bean>

    <bean id="slave" parent="abstractDataSource">
       <!-- 基本属性 url、user、password -->
		<property name="driverClassName" value="${jdbc_driverClassName}" />
		<property name="url" value="${jdbc_slave_url}" />
		<property name="username" value="${jdbc_username}" />
		<property name="password" value="${jdbc_password}" />
    </bean>
 <!-- 设置我们编写的动态数据源 -->
    <bean id="dynamicDataSource" class="com.sykj.plugin.DynamicDataSource">
        <property name="targetDataSources">
            <map>
                <entry value-ref="master" key="master"></entry>
                <entry value-ref="slave" key="slave"></entry>
            </map>
        </property>
    </bean>
 <!-- 如果我们的数据源是使用了 LazyConnectionDataSourceProxy 则在执行 Connection#prepareStatement 
    之前,spring 是不会向数据库连接池获取数据库链接的,以防在我们拦截sql判断数据源的同时,他拿到数据源,拿到了就不能更改了 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
        <property name="targetDataSource">
            <ref bean="dynamicDataSource"></ref>
        </property>
    </bean>

添加mybatis插件

<!-- spring和MyBatisPlus完美整合,不需要mybatis的配置映射文件 -->
	<bean id="sqlSessionFactory"
		class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="typeAliasesPackage" value="com.sykj.bean" />
		<!-- 自动扫描mapping.xml文件 -->
		<property name="mapperLocations" value="classpath:com/sykj/mapper/*.xml"></property>
		<!-- <property name="configLocation" value="classpath:mybatis-config.xml"></property> -->
		<property name="plugins">
			<array>
				<!-- 注册分页插件 -->
				<bean class="com.github.pagehelper.PageInterceptor">
					<property name="properties">
						<value>
							helperDialect=mysql
						</value>
					</property>
				</bean>
				<bean class="com.sykj.plugin.DynamicDataSourceInterceptor"></bean>
			</array>
		</property>
	</bean>
	<!-- DAO接口所在包名,Spring会自动查找其下的类 ,自动扫描了所有的XxxxMapper.xml对应的mapper接口文件,只要Mapper接口类和Mapper映射文件对应起来就可以了 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.sykj.mapper" />
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
	</bean>

大功告成

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值