使用mysql主从复制作读写分离

本文介绍如何利用MySQL主从复制实现读写分离,并通过Java代码示例展示了如何在应用程序中实现这一功能。

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

一:背景:

主从复制,目的就是实现读写分离,大多数情况下,我们的应用对于数据库都是 “读多写少” ,对数据库读的压力是远大于写的压力,所以在一定的业务背景下(比如数据量大,读取频繁),我们就需要考虑读写分离了。

本文主要介绍两方面的内容:

1、使用mysql作主从复制,主库:也称写库,只有1个;从库:也称读库,可能会有多个,本例只有1个。

2、java读写分离的使用实例。

二:内容:

mysql主从复制:

主从复制的原理是,主库将写的动作记录到二进制文件中,然后从库会执行这些操作来达到同步。所以同步会有

一定的时间差(一般我们感觉不到)。这里我们需要准备两个mysql,主库端口是3380,从库端口是3381。

1、先安装3380,mysql的安装过程这里不作叙述。

2、安装完成后,将安装目录3380\logs目录下将所有文件删除。

3、删除3380\data目录下所有的log文件删除(不是所有文件)


  4、配置3380\data目录下的my.ini文件,保证以下几个配置开启, 并且路径正确:



5、使用navicat新建连接,创建同步数据库taotao:


6、把3380安装文件复制一份放在同一级目录下,改名为3381:

  7、同样确保logs下的文件和data下的log文件全部删除 (因为3380中已经删了,所以复制过来就没有了)


  8、配置3381\data目录下的my.ini文件:

将所有的3380改为3381,把server-id改为81(保证和上面不一样就行),注释掉 binlog-do-db=taotao(从库不需要该配置)


9、修改3381这个mysql的uuid,要保证和3380的不一样:

在3381/data/data目录中,打开auto.cnf文件,随便改一个字母就行。


10、因为3381数据库是复制的,所以需要安装到系统中:

打开cmd,进入3381的bin目录,执行如下命令:

.\mysqld.exe install MySQL-3381 --defaults-file="E:\baidu\BaiduNetdiskDownload\mysql\3381\data\my.ini"  注意改为自己的配置文件路径

提示:Service successfully installed,则安装成功,现在进入系统服务可以看到两个mysql

11、使用navicat新建连接,创建相同的数据库taotao:


  11、在系统服务中重启3380和3381两个服务,然后在navicat分别配置主库和从库:

3380主库配置:

#创建slave的同步用户slave01,密码是123456,这样从库就可以用该用户名和密码来同步主库:

grant replication slave on *.* to 'slave01'@'127.0.0.1' identified by '123456';

#刷新

flush privileges;

#查看状态:

SHOW MASTER STATUS;

圈里面的内容复制下来,从库配置时会用到(File是记录操作的二进制文件,Position就是从库复制的起始位置)


3381从库配置:

#指定主库
CHANGE MASTER TO
  master_host='127.0.0.1',
  master_user='slave01',
  master_password='123456',
  master_port=3380,
#以下两个就是上面红圈里的值
  master_log_file='mysql3306-bin.000006',   
  master_log_pos=1120;
 
#启动slave同步
START SLAVE;
 
#查看同步状态
SHOW SLAVE STATUS;

#看看以下两个字段是否为YES,是代表同步设置成功:

12、成功之后,那么你在3380中执行的任何操作,都会同步到3381中,值得注意的几点:

1、保证两个库的uuid不同。

2、保证server-id不同。

3、主库中需要指定同步数据库binlog-do-db,否则该mysql下所有的数据库都会同步;从库中不需要指定。

4、该例两个库都是新的,假如3380中已经有很多表,这个时候才有需求建立从库,那么设置同步之前,需要把3380中所有表格导入到3381中。


好了,到此mysql主从复制已完成,接下来,基于这两个库,我们在java中来实现读写分离:

读写分离(记住:主写从读):

1、配置两个数据源:

jdbc.properties:

#主库:
jdbc.master.driver=com.mysql.jdbc.Driver
jdbc.master.url=jdbc:mysql://127.0.0.1:3380/taotao?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
jdbc.master.username=root
jdbc.master.password=root

#:从库
jdbc.slave01.driver=com.mysql.jdbc.Driver
jdbc.slave01.url=jdbc:mysql://127.0.0.1:3381/taotao?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
jdbc.slave01.username=root
jdbc.slave01.password=root

spring配置文件:applicationContext.xml

	<!-- 配置主数据源连接池 -->
	<bean id="masterDataSource" class="com.jolbox.bonecp.BoneCPDataSource"
		destroy-method="close">
		<!-- 数据库驱动 -->
		<property name="driverClass" value="${jdbc.master.driver}" />
		<!-- 相应驱动的jdbcUrl -->
		<property name="jdbcUrl" value="${jdbc.master.url}" />
		<!-- 数据库的用户名 -->
		<property name="username" value="${jdbc.master.username}" />
		<!-- 数据库的密码 -->
		<property name="password" value="${jdbc.master.password}" />
		<!-- 检查数据库连接池中空闲连接的间隔时间,单位是分,默认值:240,如果要取消则设置为0 -->
		<property name="idleConnectionTestPeriod" value="60" />
		<!-- 连接池中未使用的链接最大存活时间,单位是分,默认值:60,如果要永远存活设置为0 -->
		<property name="idleMaxAge" value="30" />
		<!-- 每个分区最大的连接数 -->
		<property name="maxConnectionsPerPartition" value="150" />
		<!-- 每个分区最小的连接数 -->
		<property name="minConnectionsPerPartition" value="5" />
	</bean>
	
	<!-- 配置从数据源连接池 -->
	<bean id="slave01DataSource" class="com.jolbox.bonecp.BoneCPDataSource"
		destroy-method="close">
		<!-- 数据库驱动 -->
		<property name="driverClass" value="${jdbc.slave01.driver}" />
		<!-- 相应驱动的jdbcUrl -->
		<property name="jdbcUrl" value="${jdbc.slave01.url}" />
		<!-- 数据库的用户名 -->
		<property name="username" value="${jdbc.slave01.username}" />
		<!-- 数据库的密码 -->
		<property name="password" value="${jdbc.slave01.password}" />
		<!-- 检查数据库连接池中空闲连接的间隔时间,单位是分,默认值:240,如果要取消则设置为0 -->
		<property name="idleConnectionTestPeriod" value="60" />
		<!-- 连接池中未使用的链接最大存活时间,单位是分,默认值:60,如果要永远存活设置为0 -->
		<property name="idleMaxAge" value="30" />
		<!-- 每个分区最大的连接数 -->
		<property name="maxConnectionsPerPartition" value="150" />
		<!-- 每个分区最小的连接数 -->
		<property name="minConnectionsPerPartition" value="5" />
	</bean>
	
	
	<!-- 定义数据源,使用自己实现的数据源 -->
	<bean id="dataSource" class="com.taotao.manage.datasource.DynamicDataSource">
		<!-- 设置多个数据源 -->
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<!-- 这个key需要和程序中的key一致 -->
				<entry key="master" value-ref="masterDataSource"/>
				<entry key="slave" value-ref="slave01DataSource"/>
			</map>
		</property>
		<!-- 设置默认的数据源,这里默认走写库 -->
		<property name="defaultTargetDataSource" ref="masterDataSource"/>
	</bean>

自定义DynamicDataSource类动态获取数据源:

package com.taotao.manage.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可
 * 
 * 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
 * 
 * @author zhijun
 *
 */
public class DynamicDataSource extends AbstractRoutingDataSource{

    @Override
    protected Object determineCurrentLookupKey() {
        // 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key,
    	//根据key值去applicationContext.xml中找到对应的数据源
        return DynamicDataSourceHolder.getDataSourceKey();
    }

}

以上我们看到需要从DynamicDataSourceHolder中获取key,定义DynamicDataSourceHolder类:

package com.taotao.manage.datasource;
/**
 * 
 * 使用ThreadLocal技术来记录当前线程中的数据源的key
 * 
 * @author zhijun
 *
 */
public class DynamicDataSourceHolder {
    
    //写库对应的数据源key
    private static final String MASTER = "master";

    //读库对应的数据源key
    private static final String SLAVE = "slave";
    
    //使用ThreadLocal记录当前线程的数据源key
    private static final ThreadLocal<String> holder = new ThreadLocal<String>();

    /**
     * 设置数据源key
     * @param key
     */
    public static void putDataSourceKey(String key) {
        holder.set(key);
    }

    /**
     * 获取数据源key
     * @return
     */
    public static String getDataSourceKey() {
        return holder.get();
    }
    
    /**
     * 标记写库
     */
    public static void markMaster(){
        putDataSourceKey(MASTER);
    }
    
    /**
     * 标记读库
     */
    public static void markSlave(){
        putDataSourceKey(SLAVE);
    }

}

上面这个类中我们看到,markMaster()和markSlave()两个方法分别设置key为master和slave,然后在DynamicDataSource中获取,

然后根据配置文件找到正确的数据源,那么,这两个方法什么时候该用哪个呢?我们使用aop来配置,

在找到正确的数据源之前,先根据service执行的方法名来判断该设置哪个key。定义aop的切面类DataSourceAspect:

package com.taotao.manage.datasource;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;

/**
 * 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库
 * 
 * @author zhijun
 *
 */
public class DataSourceAspect {

    /**
     * 在进入Service方法之前执行
     * 
     * @param point 切面对象
     */
    public void before(JoinPoint point) {
        // 获取到当前执行的方法名
        String methodName = point.getSignature().getName();
        if (isSlave(methodName)) {
            // 标记为读库
            DynamicDataSourceHolder.markSlave();
        } else {
            // 标记为写库
            DynamicDataSourceHolder.markMaster();
        }
    }

    /**
     * 判断是否为读库
     * 
     * @param methodName
     * @return
     */
    private Boolean isSlave(String methodName) {
        // service中方法名以query、find、get开头的方法名走从库,否则走主库
        return StringUtils.startsWithAny(methodName, "query", "find", "get");
    }

}


从上面我们看出,service中的方法名,如果以"query" "find" "get"开头,说明是查询的操作,

我们就调用DynamicDataSourceHolder中的markSlave()方法,把数据源设置为从库的那一个,

否则设置为master那一个。(类型可以自己设置,但是一般情况下,读的操作就是以"query" "find" "get"开头)。

好了,我们定义好aop的类了,但它还只是一个普通java类,我们需要把它配置到spring的配置文件中:

事务的配置文件 applicationContext-transaction.xml :

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	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.0.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
	
	<!-- 定义事务管理器 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

	<!-- 定义事务策略 -->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<!--所有以query开头的方法都是只读的 -->
			<tx:method name="query*" read-only="true" />
			<!--其他方法使用默认事务策略 -->
			<tx:method name="*" />
		</tx:attributes>
	</tx:advice>
	<!-- 定义AOP切面处理器 -->
	<!-- 自定义AOP切面处理器,就是我们自己写的那个aop -->
	<bean class="com.taotao.manage.datasource.DataSourceAspect" id="dataSourceAspect">
		<!-- 指定事务策略 -->
		<property name="txAdvice" ref="txAdvice"/>
		<!-- 指定slave方法的前缀(非必须) -->
		<property name="slaveMethodStart" value="query,find,get"/>
	</bean>

	<aop:config>
		<!--pointcut元素定义一个切入点,execution中的第一个星号 用以匹配方法的返回类型,
			这里星号表明匹配所有返回类型。 com.abc.dao.*.*(..)表明匹配cn.itcast.mybatis.service包下的所有类的所有 
			方法 -->
		<aop:pointcut id="myPointcut" expression="execution(* com.taotao.service.*.*(..))" />
		<!--将定义好的事务处理策略应用到上述的切入点 -->
		<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut" />
		
		<!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高,因为在其他事物开启之前必须要先确定数据源 -->
		<aop:aspect ref="dataSourceAspect" order="-9999">
			<aop:before method="before" pointcut-ref="txPointcut" />
		</aop:aspect>
	</aop:config>
	
	
</beans>

通过上面的配置,那么在com.taotao.service这个包下所有service中的方法,都会被判定该走master还是slave,如果是写操作,那么会走master,写入成功后,slave中的数据马上会同步。
好了,以上就是使用mysql主从复制实现业务读写分离,,如有不对的地方,望指正。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值