读写分离

读写分离解决的问题

1.可以使读操作分布到多个服务器,实现对读密集型应用的优化,并且使对数据库的负载均衡更加简单。

2.对备份来说,复制是一项很有意义的技术补充,不过复制不能取代备份。

3.可以实现高可用性,在发生故障需要切换的时候,读机器可以更快的切换到数据使系统能够显著的缩短宕机时间。

4.也可以用来做一些功能的升级的时候拿来做测试。

读写分离的原理

那么复制是如何工作的呢,主要是通过以下三个步骤:

1.在主库把数据更改记录到二进制日志(俗称 bin-log)中。

2.备库将主库上的日志复制到自己的中继日志(relay-log)中。

3.备库读取中继日志中的时间,并将其重放到备库的数据库上。

流程如下图:
mysql-duxie-fenli

读写分离的数据库配置

1.首先在主库上建立账号,以方便备库建立一个普通的客户端连接来启动主库的二进制转储线程(最好在备库上也建立一个这个账号,方便出现问题交换主备状态)。

GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO username@'ip' IDENTIFIED BY 'password';

2.配置主库的my.cnf,配置完后重启

log-bin                     = mysql-bin
binlog-ignore-db            = mysql  # 不同步的库
binlog-ignore-db            = information_schema 
binlog-ignore-db            = performance_schema
server-id                   = 105150 #要避免重复

3.配置从库的my.cnf,配置完后重启

server_id=206162
relay_log=/var/lib/mysql/mysql-relay-bin #中继日志
log_slave_updates=1 #允许备库将重放事件记录到自身日志
log_bin=mysql-bin 
replicate-do-db=xiaomai #需要同步的库
replicate-ignore-db=mysql #不需要同步的库
replicate-ignore-db=information_schema
replicate-ignore-db=performance_schema

4.启动复制,通过语句不通过配置,可以在无需重启备库的情况下指向主库

change master to master_host='ip', master_user='username', master_password='password', master_log_file='mysql-bin.000002', master_log_pos=58731;

执行完后,可以直接开启复制。

start slave;

通过show slave status,查看状态:

show slave status
==============================================
 **************** 1. row ******************* 
Slave_IO_State: Master_Host: IP
Master_User: username
 Master_Port: 3306
 Connect_Retry: 60 
Master_Log_File: mysql-bin.000002 
Read_Master_Log_Pos: 58731 
Relay_Log_File: localhost-relay-bin.000001
Relay_Log_Pos: 561 
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: YES 
Slave_SQL_Running: YES 
Replicate_Do_DB: ……………省略若干…………… 
Master_Server_Id: 1 
1 row in set (0.01 sec) 
==============================================

当Slave_IO_Running和Slave_SQL_Running都为yes时,同步就已经开始了。

Mycat实现分片分表

数据切分

数据切分就是指通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主 机)上面,以达到分散单台设备负载的效果,数据切分的方式:

1.垂直切分:就是指通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主 机)上面,以达到分散单台设备负载的效果。垂直切分最大特点就是规则简单,实施也更为方便,尤其适合各业务之间的耦合度非常低,相互影响很 小,业务逻辑非常清晰的系统。在这种系统中,可以很容易做到将不同业务模块所使用的表分拆到不同的数据库 中。根据不同的表来进行拆分,对应用程序的影响也更小,拆分规则也会比较简单清晰。

优点:
1.拆分后业务清晰,拆分规则明确。
2.系统之间整合或扩展容易。
3.数据维护简单。
缺点:
1.部分业务表无法 join,只能通过接口方式解决,提高了系统复杂度。
2.受每种业务不同的限制存在单库性能瓶颈,不易数据扩展跟性能提高。
3.事务处理复杂。

2.水平切分:根据 表中的数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库(主机)上面。水平切分于垂直切分相比,相对来说稍微复杂一些。因为要将同一个表中的不同数据拆分到不同的数据库中,对于应用程序来说,拆分规则本身就较根据表名来拆分更为复杂,后期的数据维护也会更为复杂一些。

优点:
1.拆分规则抽象好,join 操作基本可以数据库做。
2.不存在单库大数据,高并发的性能瓶颈。
3.应用端改造较少。
4.提高了系统的稳定性跟负载能力。
缺点:
1.拆分规则难以抽象。
2.分片事务一致性难以解决。
3.数据多次扩展难度跟维护量极大。
4.跨库 join 性能较差。

两种方式共同的缺点如下:

引入分布式事务的问题。
跨节点 Join 的问题。
跨节点合并排序分页问题。
多数据源管理问题。

为什么使用Mycat

MYCAT是基于Java的一个分布式数据库系统中间层,为高并发下数据库的分布式提供解决方案。其优点如下:

1.遵守Mysql原生协议,跨语言,跨平台,跨数据库
2.基于心跳的自动故障切换,支持读写分离,支持MySQL主从,以及galera cluster集群。
3.基于Nio实现,有效管理线程,解决高并发问题。
4.支持单库内部任意join,支持跨库2表join,甚至基于caltlet的多表join,支持通过全局表,ER关系的分片策略,实现了高效的多表join查询。
5.持分布式事务(弱xa),支持XA分布式事务。
6.支持全局序列号,解决分布式下的主键生成问题。
7.使用的项目较多,证明其适用于生产环境,而且代码开源,使用java编写,在遇到特殊情况时可以通过自己修改编译来解决。

当然,Mycat也存在以下缺点:

1.mycat中的路由结果是通过分片字段和分片方法来确定的。当使用Mycat时有非分片字段查询,Mycat无法计算路由,便发送到所有节点上执行该SQL,会极大消耗Mycat和MySQL数据库资源。
2.当使用mycat全分片查询,有分页排序时,也会对内存和CPU资源的消耗。
3.当使用Mycat时如果要进行表JOIN操作,要确保两个表的关联字段具有相同的数据分布,否则会查不到结果。
4.Mycat并没有根据二阶段提交协议实现 XA事务,而是只保证 prepare 阶段数据一致性的 弱XA事务。如果有分布式事务,得先看是否得保证事务得强一致性。

不过,对于我们现在的业务来说,Mycat是比较适合的,我们可以通过水平切分来进行分片,在大部分情况下已经不会出现跨库join的问题以及跨库事务的问题,在大部分情况下的查询都能够通过分库的规则准确路由到真正需要查询的库。

Mycat配置

mycat的配置方面最主要的就是对于schema的配置,举例说明:
conf/schema.xml

<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100">
	<table name="travelrecord" primaryKey="record_id" dataNode="dn1,dn2" autoIncrement="true" rule="auto-sharding-long" ></table> 
</schema>
<dataNode name="dn1" dataHost="localhost1" database="db1" />
<dataNode name="dn2" dataHost="localhost1" database="db2" />
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0"
		  writeType="0" dbType="mysql" dbDriver="native" switchType="1"  slaveThreshold="100">
	<heartbeat>select user()</heartbeat>
	<!-- can have multi write hosts -->
	<writeHost host="hostM1" url="localhost:3306" user="root"
			   password="123456">
		<!-- can have multi read hosts -->
		<readHost host="hostS2" url="192.168.1.200:3306" user="root" password="xxx" />
	</writeHost>
	<writeHost host="hostS1" url="localhost:3316" user="root"
			   password="123456" />
</dataHost>

这里主要说明一下datanode的属性:

maxCon: 指定每个读写实例连接池的最大连接。即不管工程对mycat的链接有多少,他到MySQL的链接最多就是指定的该数值。
minCon:指定每个读写实例连接池的最小连接,初始化连接池的大小。即不管工程对mycat的有没有链接,他到MySQL的链接最少就是指定的该数值。
balance: 
负载均衡类型,目前的取值有 3 种:
	1. balance="0", 不开启读写分离机制,所有读操作都发送到当前可用的 writeHost 上。
	2. balance="1",全部的 readHost 与 stand by writeHost 参与 select 语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且 M1 与 M2 互为主备),正常情况下,M2,S1,S2 都参与 select 语句的负载 均衡。
	3. balance="2",所有读操作都随机的在 writeHost、readhost 上分发。
	4. balance="3",所有读请求随机的分发到 wiriterHost 对应的 readhost 执行,writerHost 不负担读压力。

writeType:负载均衡类型,目前的取值有 3 种:
	1. writeType="0", 所有写操作发送到配置的第一个 writeHost,第一个挂了切到还生存的第二个 writeHost,重新启动后已切换后的为准,切换记录在配置文件中:dnindex.properties .
	2. writeType="1",所有写操作都随机的发送到配置的 writeHost,1.5 以后废弃不推荐。

switchType 属性
	-1 表示不自动切换
	1 默认值,自动切换
	2 基于MySQL主从同步的状态决定是否切换 心跳语句为 show slave status
	3 基于MySQLgalarycluster的切换机制(适合集群)(1.4.1) 心跳语句为 show status like ‘wsrep%’.

因为上面的表中分配了分片规则,所以需要到conf/rule.xml中配置相应的分片规则:

<tableRule name="auto-sharding-long">
	<rule>
		<columns>id</columns>
		<algorithm>rang-long</algorithm>
	</rule>
</tableRule>
<function name="rang-long"
	class="io.mycat.route.function.AutoPartitionByLong">
	<property name="mapFile">autopartition-long.txt</property>
</function>

其中function标签中提到的autopartition-long.txt,内容如下:

2000001-4000000=1
0-2000000=0
4000001-8000000=2

所以很明确了,该分片规则就是通过字段id的值来分片的,0-2000000是第一个,2000001-4000000,4000001-8000000依次是第二个和第三个。

而我们对于表中设置了autoIncrement为true,所以表的主键是自增的,而对于不同库的id自增则需要全局序列号,对于自增的全局序列号我们通过数据库方式来得到,首先在serve.xml中配置使用数据库方式生成sequence:

<system><property name="sequnceHandlerType">1</property></system>

接下来在dn1的数据库中增加表MYCAT_SEQUENCE:

– name sequence 名称
– current_value 当前 value
– increment 增长步长! 可理解为 mycat 在数据库中一次读取多少个 sequence. 当这些用完后, 下次再从数据库中读取.

增加了表后,添加第一条数据,上述的表的数据添加如下:

INSERT INTO `MYCAT_SEQUENCE` (`name`, `current_value`, `increment`) VALUES ('travelrecord', '1', '1')

然后再dn1的数据库中增加三个存取当前sequence值的方法,在添加方法时如果出现错误

ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)

则配置my.cnf中的mysqld项:

log-bin-trust-function-creators=1

配置的三个函数如下:

1.获取当前sequence值的函数
DELIMITER $$
CREATE FUNCTION mycat_seq_currval(seq_name 	VARCHAR(50))RETURNS VARCHAR(64) CHARSET 'utf8'
BEGIN
DECLARE retval VARCHAR(64);
SET retval='-999999999,NULL';
SELECT CONCAT(CAST(current_value AS CHAR),',',CAST(increment AS CHAR)) INTO retval FROM
MYCAT_SEQUENCE WHERE NAME = seq_name;
RETURN retval;
END$$
DELIMITER ;

2.设置sequence的值:
DROP FUNCTION IF EXISTS mycat_seq_setval;
DELIMITER $$
CREATE FUNCTION mycat_seq_setval(seq_name VARCHAR(50),VALUE INTEGER) RETURNS VARCHAR(64) CHARSET 'utf8'
BEGIN
   UPDATE MYCAT_SEQUENCE SET current_value = VALUE    WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END$$
DELIMITER 

3.获取下一个值
DROP FUNCTION IF EXISTS mycat_seq_nextval;
DELIMITER $$
CREATE FUNCTION mycat_seq_nextval(seq_name VARCHAR(50)) RETURNS VARCHAR(64) CHARSET 'utf8'
BEGIN
UPDATE MYCAT_SEQUENCE SET current_value = current_value + increment 
WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END$$
DELIMITER ;

最后需要配置conf/sequence_db_conf.properties文件,如下:

TRAVELRECORD=dn1
注:MYCAT_SEQUENCE 表和以上的 3 个 function,需要放在同一个节点上。字段的名字为表名的大写

Mycat存在的问题

mycat本身虽然在生产环境在应用,但是对于我们sequelize orm生成的语句有时候会出现不兼容的情况,对于一些语句会出现报错的情况,需要充分的测试以及来解决。解决的方案主要分为两种:

1.对sequelize的一些生成语句的函数进行重写并且修改。

2.对mycat本身的代码进行修改并且重新编译。

在未来的一段时间,希望大家多多观察这方面的问题,然后一起修改掉这些问题,但是数据库本身的架构是肯定会往这个分库分表读写分离的方向发展的,中间肯定会遇到一些问题,还希望大家能耐心的去解决。

引用资料:
1.《HIGH PERFORMMANCE MYSQL》
2.《Mycat_V1.6.0》