什么是数据库读写分离
在业务量比较大时,我们经常会采用主从数据库读写分离的技术来降低我们主服务器的压力,降低服务器崩溃与数据丢失的可能性。下面来讲解主从数据库的核心理论知识。在Master主数据库中有一个BinaryLog日志文件,它会将我们所有的对数据库的操作记录下来,并且主数据库在日志数据更新后会通知从数据库来更新日志记录。从数据库中会开启两个线程,一个线程平时处于睡眠状态,在接到主数据库的通知后会唤醒,并去读取主数据库中的日志文件将其同步到从数据库中的RelayLog日志文件中,另一个线程负责SQL语句的执行。这两个线程构成了生产者与消费者模式,RelayLog就相当于其中的消息队列。这样从数据库就可以随时与主数据库保持一致的状态。继而我们可以将所有只读的事务分发到从数据库中去执行,将所有涉及写的操作分发到主数据库中去执行。
主从数据库分离的MySQL配置
主数据库配置
1.更改主数据库mysqld配置
1 2 3 4 5 6
| vim /etc/my.cnf
server-id = 1
log-bin-index=master-bin.index log-bin = master-bin
|
2.创建用于主从数据库用于通信的用户
1 2 3 4 5 6
| CREATE USER repl
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'123.123.0.1(从服务器IP)' IDENTIFIED BY '12345(用户密码)';
flush privileges;
|
配置完成后我们可以在主数据库中输入show master status来获取我们主数据库中binaryLog文件的相关信息,日志文件名,日志开始位置的等等。
3.更改从数据库mysqld配置
与第一步主数据库做的配置相似
1 2 3 4 5 6
| vim /etc/my.cnf
server-id=2
relay-log-index=slave-relay-bin.index relay-log=slave-relay-bin
|
4.连接主从数据库
在从数据库中建立到主数据库的连接
其中:
master_host为主服务器的IP地址
master_port为主服务器mysql的端口号
master_user与master_password为主服务器中创建的用于通信的的用户名与密码
master_log_file要读取的主服务器中的日志文件名称,可以通过第二步中的show master status来查看
master_log_pos要读取的日志文件的开始位置,同样也可以通过show master status来查看,如果是首次连接可以直接写0,如果遇到服务器挂掉的情况,我们可以通过记录中的挂掉的日志位置来重新使主从服务器保持一致
1 2 3
| change master to master_host='123.123.12.12',master_port='3306', master_user='repl',master_password='12345', master_log_file='master-bin.000001',master_log_pos=0;
|
从服务器配置完成后,我们可以通过show slave status来查看配置是否成功,如果失败我们也可以从中get到报错信息.
主从数据库的代码实现
我们的核心思想就是,写一个拦截器。在每次需要调用数据库去执行事务的时候,我们用这个拦截器去判断事务的类型,如果是只读事务,那么我们就去将事务分发到从数据库中去执行,否则就交给主数据库执行。
Java中给我们提供了一个抽象类来进行数据库分离,AbstractRoutingDataSource,该类通过LookupKey来返回对应的数据源,在该类中有两个核心成员一个是Map<Object, Object> targetDataSources,其中存入的为LookupKey与DataSource的键值对,还有一个就是Object determineCurrentLookupKey(),我们需要重写该方法,该方法内部需要做到的事情为:返回我们需要的LookupKey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class DynamicDataSource extends AbstractRoutingDataSource{ private static ThreadLocal<String> LookupKeyContext = new ThreadLocal<String>(); }
@override public static Object determineCurrentLookupKey(){ String db = LookupKeyContext.get(); if(db == null){ return "master"; } return db; } public static void setDataSource(String db){ LookupKeyContext.set(db); } public static void clearDataSource(){ LookupKeyContext.clear(); }
|
我们通过MyBatis的拦截器拦截下所有的数据库操作请求,判断其是否为只读事务,然后通过我们上面写的那个类设定对应的LookupKey。之后再根据LookupKey去调用对应的DataSource.
下面我们来实现MyBatis的拦截器
1 2 3 4 5 6 7 8 9 10
| public Object intercept(Invocation invaocation) throws Throwable{ boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); if(isReadOnly){ DynamicDataSourc.set("slave"); }else{ DynamicDataSourc.set("master"); } }
|
DataSource数据源配置的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <bean id="abstractDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/> <property name="initialPoolSize" value="${jdbc.initPoolSize}"/> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/> <property name="initialPoolSize" value="${jdbc.initPoolSize}"/> <property name="autoCommitOnClose" value="false"/> <property name="acquireRetryAttempts" value="2"/> <property name="checkoutTimeout" value="10000"/> </bean>
<bean id="master" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"/> <property name="password" value="${jdbc.password}"/> <property name="jdbcUrl" value="${jdbc.master.url}"/> <property name="driverClass" value="${jdbc.driver}"/> </bean>
<bean id="slave" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"/> <property name="password" value="${jdbc.password}"/> <property name="jdbcUrl" value="${jdbc.master.url}"/> <property name="driverClass" value="${jdbc.driver}"/> </bean> <bean id="dynamicDataSource" class="刚刚我们自己定义的类">
<property name="targetDataSources"> <map> <entry key="master" value_ref="master" /> <entry key="slave" value_ref="slave" /> </map> </property> </bean>
|
最后再在MyBatis配置文件中加入主从数据库支持即可