1. 背景
我们一般应用对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,有一个思路就是说采用数据库集群的方案,
其中一个是主库,负责写入数据,我们称之为:写库;
其它都是从库,负责读取数据,我们称之为:读库;
那么,对我们的要求是:
1、读库和写库的数据一致;
2、写数据必须写到写库;
3、读数据必须到读库;
2. 实现方案
解决读写分离的方案有两种:应用层解决和中间件解决。
1.应用层
目前的一些解决方案需要在程序中手动指定数据源,比较麻烦,后边我会通过AOP思想来解决这个问题
2,中间件
MySQL-proxy:http://hi.baidu.com/geshuai2008/item/0ded5389c685645f850fab07Amoeba for MySQL
此处我们介绍一种在应用层的解决方案,通过spring动态数据源和AOP来解决数据库的读写分离。
该方案目前已经在一个互联网项目中使用了,而且可以很好的工作。
该方案目前支持
一读多写;当写时默认读操作到写库、当写时强制读操作到读库。
考虑未来支持
读库负载均衡、读库故障转移等。
使用场景
不想引入中间件,想在应用层解决读写分离,可以考虑这个方案;
建议数据访问层使用jdbc、ibatis,不建议hibernate。
优势
应用层解决,不引入额外中间件;
在应用层支持『当写时默认读操作到写库』,这样如果我们采用这种方案,在写操作后读数据直接从写库拿,不会产生数据复制的延迟问题;
应用层解决读写分离,理论支持任意数据库。
缺点
1、不支持@Transactional注解事务,此方案要求所有读方法必须是read-only=true,因此如果是@Transactional,这样就要求在每一个读方法头上加@Transactional 且readOnly属性=true,相当麻烦。 :oops:
2、必须按照配置约定进行配置,不够灵活
2.1. 应用层解决
优缺点:
优点:
1、源程序不需要做任何改动就可以实现读写分离;
2、动态添加数据源不需要重启程序;
缺点:
1、程序依赖于中间件,会导致切换数据库变得困难;
2、由中间件做了中转代理,性能有所下降;
相关中间件产品使用:
MySQL-proxy:http://hi.baidu.com/geshuai2008/item/0ded5389c685645f850fab07
Amoeba for MySQL:http://www.iteye.com/topic/188598和http://www.iteye.com/topic/1113437
3. 使用Spring基于应用层实现
3.1. 原理
在进入Service之前,使用AOP来做出判断,是使用写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库
3.2. DynamicDataSource
动态改变数据源
在介绍实现方式之前,我们先准备一些必要的知识,spring 的AbstractRoutingDataSource 类
AbstractRoutingDataSource这个类 是spring2.0以后增加的,我们先来看下AbstractRoutingDataSource的定义:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {}
AbstractRoutingDataSource继承了AbstractDataSource ,而AbstractDataSource 又是DataSource 的子类。DataSource
3.3. DynamicDataSourceHolder
使用ThreadLocal技术来记录当前线程中的数据源的key
5. 一主多从的实现
很多实际使用场景下都是采用“一主多从”的架构的,所有我们现在对这种架构做支持,目前只需要修改DynamicDataSource即可。
6. MySQL主从复制
6.1. 原理
mysql主(称master)从(称slave)复制的原理:
1、master将数据改变记录到二进制日志(binarylog)中,也即是配置文件log-bin指定的文件(这些记录叫做二进制日志事件,binary log events)
2、slave将master的binary logevents拷贝到它的中继日志(relay log)
3、slave重做中继日志中的事件,将改变反映它自己的数据(数据重演)
6.2. 主从配置需要注意的地方
1、主DB server和从DB server数据库的版本一致
2、主DB server和从DB server数据库数据一致[ 这里就会可以把主的备份在从上还原,也可以直接将主的数据目录拷贝到从的相应数据目录]
3、主DB server开启二进制日志,主DB server和从DB server的server_id都必须唯一
6.3. 主库配置(windows,Linux下也类似)
第一步,主服务器创建用户并清空日志
mysql> show privileges;
mysql> grant replication client, replication slave on *.* to 'larry'@'192.168.1.%' identified by 'larry';
mysql> show binary logs;
mysql> reset master;
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 107 |
+------------------+-----------+
1 row in set (0.00 sec)
第二步,修改从服务器的server-id,把server-id改为2然后重启mysql
[root@serv08 ~]# cat /etc/my.cnf | grep server-id
server-id = 1
#server-id = 2
[root@serv08 ~]# vim /etc/my.cnf
[root@serv08 ~]# cat /etc/my.cnf | grep server-id
server-id = 2
#server-id = 2
[root@serv08 ~]# /etc/init.d/mysqld restart
第三步,从服务器清空日志
mysql> show binary logs;
mysql> reset master;
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 107 |
+------------------+-----------+
1 row in set (0.00 sec)
mysql> show slave status;
Empty set (0.00 sec)
第四步,从服务器通过change master to命令修改设置
mysql> change master to
-> master_host='192.168.1.11',
-> master_user='larry',
-> master_password='larry',
-> master_port=3306,
-> master_log_file='mysql-bin.000001',
-> master_log_pos=107;
Query OK, 0 rows affected (0.01 sec)
第五步,开启slave。
mysql> slave start;
mysql>show slave status ;
mysql>show slave status \G;
第六步,从服务器查看是否和主服务器通信成功。如果出现 Slave_IO_Running和Slave_SQL_Running都是yes,则证明配置成功
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.1.11
Master_User: larry
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 107
Relay_Log_File: serv08-relay-bin.000002
Relay_Log_Pos: 253
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 107
Relay_Log_Space: 410
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
1 row in set (0.00 sec)
ERROR:
No query specified
第八步,测试
--slave查看数据库
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| test |
+--------------------+
4 rows in set (0.02 sec)
--master创建数据库
mysql> create database larrydb;
Query OK, 1 row affected (0.00 sec)
--master查看数据库
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| larrydb |
| mysql |
| performance_schema |
| test |
+--------------------+
5 rows in set (0.01 sec)
--slave查看数据库,发现已经同步
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| larrydb |
| mysql |
| performance_schema |
| test |
+--------------------+
5 rows in set (0.00 sec)
--master创建表 插入数据
mysql> use larrydb;
Database changed
mysql> create table test(id int(11));
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(1);
Query OK, 1 row affected (0.00 sec)
--slave查看数据是否同步成功,发现数据已经同步
mysql> use larrydb;
Database changed
mysql> show tables;
+-------------------+
| Tables_in_larrydb |
+-------------------+
| test |
+-------------------+
1 row in set (0.00 sec)
mysql> select * from test;
+------+
| id |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
第九步,查看进程状态
--master查看进程状态
mysql> show processlist;
+----+-------+--------------------+---------+-------------+------+-----------------------------------------------------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-------+--------------------+---------+-------------+------+-----------------------------------------------------------------------+------------------+
| 1 | root | localhost | larrydb | Query | 0 | NULL | show processlist |
| 2 | larry | 192.168.1.18:41393 | NULL | Binlog Dump | 854 | Master has sent all binlog to slave; waiting for binlog to be updated | NULL |
+----+-------+--------------------+---------+-------------+------+-----------------------------------------------------------------------+------------------+
2 rows in set (0.00 sec)
--slave查看进程状态
mysql> show processlist;
+----+-------------+-----------+---------+---------+------+-----------------------------------------------------------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-------------+-----------+---------+---------+------+-----------------------------------------------------------------------------+------------------+
| 1 | root | localhost | larrydb | Query | 0 | NULL | show processlist |
| 2 | system user | | NULL | Connect | 880 | Waiting for master to send event | NULL |
| 3 | system user | | NULL | Connect | 65 | Slave has read all relay log; waiting for the slave I/O thread to update it | NULL |
+----+-------------+-----------+---------+---------+------+-----------------------------------------------------------------------------+------------------+
3 rows in set (0.00 sec)
备注:如果需要制定同步的数据库,需要修改slave从库 /etc/my.cnf 这个mysql配置文件。
# vim /etc/my.cnf
增加两端语句
binlog-do-db = 同步的数据
binlog-ignore-db = mysql,information_schema
需要注意的地方
1、两个数据库的版本和数据应该保持一致
2、对应的端口,防火墙应该开启
Spring项目源码 https://github.com/jiafuweiJava/MasterSlave