大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够。到了数据业务层、数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢失的话,后果更是 不堪设想。这时候,我们会考虑如何减少数据库的联接,一方面采用优秀的代码框架,进行代码的优化,采用优秀的数据缓存技术如:redis,如果资金丰厚的话,必然会想到假设服务器群,来分担主数据库的压力。Ok切入今天微博主题,利用MySQL主从配置,实现读写分离,减轻数据库压力。这种方式,在如今很多网站里都有使用,也不是什么新鲜事情,今天总结一下,方便大家学习参考一下。
原理:主服务器(master)负责写操作(包括增删改),有且仅有一台;从服务器(slave)负责读操作,可以配置n台,写入数据的时候,首先master会把数据写在本地 Binary Log 文件中,然后通过I/O 写入 slave 的 relayLog 日志文件中,之后才同步数据库中,实现主从同步
在这里我使用的是两台CenterOS 6.5 的虚拟机进行配置,master IP :192.168.1.111 , Slave IP :192.168.1.112,开始了:
一: Mysql 配置主从分离
1.下载安装 Mysql 数据库
1.1 # yum list | grep mysql --我们通过命令可以查看yum上提供下载的mysql的版本信息
1.2 # yum install -y mysql-server mysql mysql-deve --运行命令开始安装直到安装完成
2. 配置Mysql数据库的 master 和 slave
2.1 首先配置 master:
# vi /etc/my.cnf -- Mysql 的配置一般都是在这,直接运行命令进行修改
在【mysqld】添加以下:
server-id=1 log-bin=master-bin log-bin-index=master-bin.index
随后开启数据库
# service mysqld start;
登录进去数据库:
# mysql -uroot -p;
进去之后创建一个用户用来主从数据库的通信
# create user manager;
授予 REPLICATION SLAVE 权限就够了
# grant replication slave on *.* to ‘manager‘@‘192.168.1.112‘ identified by ‘123456‘;
# flush privileges;
之后查看一下master日志
# show master status;
+-------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+------------------+
| master-bin.000001 | 1285 | | |
+-------------------+----------+--------------+------------------+
1 row in set (0.00 sec)
好了 ,master 配置完成了。
2,2 配置slave
和 master 一样,首先修改配置文件
# vi /etc/my.cnf
在【mysqld】添加以下:
server-id=2 relay-log-index=slave-relay-bin.index relay-log=slave-relay-bin
然后开启 mysql 服务,登录进去之后,连接master
# change master to master_host=‘192.168.1.111‘, //Master 服务器Ip
master_port=3306,
master_user=‘manager‘,
master_password=‘123456‘,
master_log_file=‘master-bin.000001‘,//Master服务器产生的日志
master_log_pos=0;
启动 slave
# start slave
查看一下slave运行有没有错误,如果没有就说明已经配置好了,主和从已经正常工作了
# show slave status \G;
二: 配置Spring 和 Mybatis
首先修改一下 jdbc.properties ,配置两条连接数据库URL
jdbc.driver=com.mysql.jdbc.Driver jdbc.slave.url=jdbc:mysql://192.168.237.111/test?useUnicode=true&characterEncoding=utf8 jdbc.master.url=jdbc:mysql://192.168.237.112/test?useUnicode=true&characterEncoding=utf8 jdbc.username=xxxxx jdbc.password=xxxxx
定义一个拦截器,实现 import org.apache.ibatis.plugin.Interceptor 接口
package com.smy.dao.split; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.support.TransactionSynchronizationManager; @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 { // 数据库操作字符串的匹配,insert,update,delete private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; private static Logger log = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { // 判断是否被事务管理 boolean synchronization = TransactionSynchronizationManager.isActualTransactionActive(); Object[] objects = invocation.getArgs(); MappedStatement ms = (MappedStatement) objects[0]; String lookupKey = DynamicDataSourceHolder.DB_MASTER;; // 判断是否被事务管理 if (!synchronization) { // 读操作 if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { // selectKey 为自增id查询主键(SELECT LAST_INSERT_ID())方法,使用主库 if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { // 如果执行到了这里说明就是没有被事务管理也没有指定主键Key,只能对sql // 语句进行匹配规则 BoundSql boundSql = ms.getBoundSql(objects[1]); String sql = boundSql.getSql().toLowerCase().replaceAll("\\t\\n\\r", " "); if (sql.matches(REGEX)) { lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { lookupKey = DynamicDataSourceHolder.DB_SLAVE; } } } } else { // 如果被事务管理说明 就是增删改,需要在 master 中操作 lookupKey = DynamicDataSourceHolder.DB_MASTER; } log.debug("设置方法[{}] use [{}] Strategy, SqlCommanType [{}]..", ms.getId(), lookupKey, ms.getSqlCommandType().name()); //设置访问数据库类型 master 或者 slave DynamicDataSourceHolder.setDbType(lookupKey); return invocation.proceed(); } @Override public Object plugin(Object target) { // 如果执行的是增删改的操作就使用本拦截,如果不是就直接返回 if (target instanceof Executor) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) { } }
定义一个类 DynamicDataSourceHolder 来管理 我们的master 和 slave 常量,也就是管理我们的数据源
package com.smy.dao.split; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DynamicDataSourceHolder { private static Logger log = LoggerFactory.getLogger(DynamicDataSourceHolder.class); private static ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static final String DB_MASTER = "master"; public static final String DB_SLAVE = "slave"; /** * 获取数据源 * @return */ public static String getDbType() { String db = contextHolder.get(); if(db==null) { db = DB_MASTER; } return db; } /** * 设置数据源 * @param dbType */ public static void setDbType(String dbType) { log.debug("所使用的数据源"+dbType); contextHolder.set(dbType); } /** * 清理数据源 */ public static void clearDbType() { contextHolder.remove(); } }
接下来 定义一个类 继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 类,因为这个类能够动态路由到数据源
package com.smy.dao.split; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDbType(); } }
实现这个类的一个抽象方法,查看AbstractRoutingDataSource 类源码有这么一个方法:
/** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */ protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); // 这个方法就确定了要使用哪个数据源,然而AbstractRoutingDataSource 类中,这个方法是抽象的,所以我们要实现这个类并实现该方法 DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
OK ,开始我们在Spring 和 Mybatis 配置文件中配置我们的拦截器 和 动态数据源Bean
Mybatis.xml 中配置添加plugin:
<plugins> <plugin interceptor="com.smy.dao.split.DynamicDataSourceInterceptor" /> </plugins>
Spring-dao.xml 中:
<context:property-placeholder location="classpath:jdbc.properties"/> <!-- 2.数据库连接池 --> <!-- 定义抽象数据源,使其它数据源 Bean 继承该数据源 --> <bean id="abstractDataSource" abstract="true" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <!-- c3p0连接池的私有属性 --> <property name="maxPoolSize" value="30" /> <property name="minPoolSize" value="10" /> <!-- 关闭连接后不自动commit --> <property name="autoCommitOnClose" value="false" /> <!-- 获取连接超时时间 --> <property name="checkoutTimeout" value="10000" /> <!-- 当获取连接失败重试次数 --> <property name="acquireRetryAttempts" value="2" /> <property name="maxStatements" value="0" /> </bean> <!-- 主数据源 master --> <bean id="master" parent="abstractDataSource"> <!-- 配置连接池属性 --> <property name="driverClass" value="${jdbc.driver}" /> <property name="jdbcUrl" value="${jdbc.master.url}" /> <property name="user" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <!-- 从数据源 slave --> <bean id="slave" parent="abstractDataSource"> <!-- 配置连接池属性 --> <property name="driverClass" value="${jdbc.driver}" /> <property name="jdbcUrl" value="${jdbc.slave.url}" /> <property name="user" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <!-- 配置动态数据源,这儿targetDataSources就是路由数据源所对应的名称 --> <bean id="dynamicDataSource" class="com.smy.dao.split.DynamicDataSource"> <property name="targetDataSources"> <map> <entry value-ref="master" key="master"></entry> <entry value-ref="slave" key="slave"></entry> </map> </property> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy"> <property name="targetDataSource"> <ref bean="dynamicDataSource" /> </property> </bean>
至于为什么这么配置, 相信大家看过源码之后就会很清楚了。。
The End 。。。。。。。。。。。。。。。。。。