Mysql高并发解决方案
前言
随着近些年来分布式的应用,其伴随而来的是系统的数据量也越来越大,为了可以提升系统的整体性能,我们对以Mysql
为代表的关系型数据库也提出了“分布式”的要求。
MyCat入门可参考我的博客:MyCat入门
使用HaProxy实现Mycat的高可用可参考我的博客: 使用HaProxy实现Mycat的高可用
正文
Mysql高并发解决方案
Mysql单节点的性能极限:
mysql
默认的最大连接数为151,上限为100000。- 如果借助
InnoDB
的行级锁来实现“减库存”的话:由于数据库中InnoDB
的行锁针对同一行数据的更新操作是串行执行的,那么某一个线程在未释放锁之前,其余的线程将会全部阻塞在队列中等待拿锁,并发越高时,等待的线程也就会越多,这会严重影响数据库的TPS
,从而导致RT
线性上升,最终可能引发系统出现雪崩。
由于Mysql
单节点存在性能瓶颈,所以要进一步通过增加节点的方式来提升并发量,于是可以采用主从复制进而实现读写分离,缓解单节点的IO
压力。
MySQL主从复制实现读写分离
事务的两阶段提交
Mysql
使用两阶段提交解决故障恢复、主从、主备复制时,保持了数据的一致性,即实现binlog
和InnoDB redo log
的数据一致性。
- 阶段一:
redo log
写盘,InnoDB
事务进入prepare
状态 - 阶段二:如果前面
prepare
成功,binlog
写盘,那么再继续将事务日志(redo log
)持久化到binlog
,如果持久化成功,那么InnoDB
事务 则进入commit
状态
每个事务binlog
的末尾,会记录一个 XID event
,标志着事务是否提交成功。
MySql数据库主从复制的方式
主从复制是基于日志(binlog)主要有以下四种:
- 异步复制(默认) :
MySQL
默认的复制策略,Master
处理事务过程中,将其写入Binlog
就会通知Dump thread
线程处理,然后完成事务的提交,不会关心是否成功发送到任意一个slave
中。 - 半同步复制:
Master
处理事务过程中,提交完事务后,必须等至少一个Slave
将收到的binlog
写入relay log
返回ack
(提交后才同步)才能继续执行处理用户的事务。 - 增强半同步复制:半同步的问题是因为等待
ACK
的点是Commit
之后,此时Master
已经完成数据变更,用户已经可以看到最新数据,当Binlog
还未同步到Slave
时,发生主从切换,那么此时从库是没有这个最新数据的,用户又看到老数据。增强半同步将等待ACK
的点放在提交Commit
之前(同步后再提交),此时数据还未被提交,外界看不到数据变更,此时如果发送主从切换,新库依然还是老数据,不存在数据不一致的问题。 - 组复制:
MySQL
在引擎层完成Prepare
操作写Redo
日志之后,会被MySQL
的预设Hook
拦截进入MGR
层,MGR
层将事务信息打包通过Paxos
协议发送到全部节点上,只要集群中过半节点回复ACK
(半数写成功),那么将告诉所有节点数据包同步成功,然后每个节点开始自己认证(certify
)通过就开始写Binlog
,提交事务或者写relay log
,数据同步,如果认证不通过则rollback
。
MySql主从复制的涉及三个行为:
- 数据存储:
Master
将数据改变记录到二进制日志(binary log
)中,也就是配置文件log-bin
指定的文件,这些记录叫做二进制日志事件(binary log events
); - 数据传输:
Slave
通过I/O
线程读取Master
中的binary log events
并写入到它的中继日志(relay log
); - 数据重放:
Slave
重做中继日志中的事件,把中继日志中的事件信息一条一条的在本地执行一次,完成数据在本地的存储,从而实现将改变反映到它自己的数据。
分表
水平分表的特点:
- 将一张表拆分成多张表,每张表的结构是一样(使用的是同一个
FRM
文件,但每一子表存放在在同一个磁盘中一个独立MYD
文件和一个独立的MYI
文件,MRG
文件记录分表信息); - 利用主表作为查询的接口(没有数据文件),表一表二作为存储数据的实际表单 ;
- 决定数据放在那一张实际的表,往往采用对
ID
取模(即求余)或者对业务主键hash
运算,确保有关联性的数据在同一个子表中; merge
来分表,是最简单的一种方式实现分区;
以对ID取模的方式举例:
#向表一插入数据:
insert into tb_member1(id,name,sex) select id,name,sex from member where id%2=0; //这里区分表一表二
#向表二插入数据:
insert into tb_member2(id,name,sex) select id,name,sex from member where id%2=1;
#查看一下主表的数据:
select * from tb_member;
复制代码
分区
水平分区的特点:
- 分区一张表的数据分成N多个区块,这些区块可以在同一个磁盘上,也可以在不同的磁盘上,但实际上还是同一张表,只是把
MYD
、MYI
同时切分了很多份,Par
文件会记录分区信息; - 分区使用
partition by
比较容易实现,水平分区支持range
分区、list
分区、hash
分区等方式; - 采用那种方式进行分区取决于业务,比如以权益中心为例:通常来说我们希望同一个用户的数据会存在同一个物理表中,可以采用以手机号进行
hash
的方式进行分区;
#向表插入数据,分区相对于分表是无感知的:
insert into member(id,name,sex) values (1,’luo’,’男’);
#查看一下主表的数据:
select * from tb_member;
复制代码
分表分库
分表分库是一种分区+分表结合起来的方式:
- 原本存储于一个库的数据分块存储到多个库上;
- 把原本存储于一个表的数据分块存储到多个表上;
- 分库可以解决单台服务器性能不够,或者成本过高问题;
- 分表分库可以是水平分表分库,也可以是垂直分表分库;
分表分库常见的问题
跨库join
当数据分到不同的库上,一般是禁止跨库join
的,一般会采用以下方式:
- 全局表:所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些表。比较类似我们理解的“数据字典”。为了避免跨库
join
查询,我们可以将这类表在其他每个数据库中均保存一份; - 字段冗余:比如“订单表”中保存“卖家
Id
”的同时,将卖家的“Name
”字段也冗余,这样查询订单详情的时候就不需要再去查询“卖家用户表”; - 数据同步:使用
ETL
工具(数据迁移)做表数据同步,定时A库中的tab_a
表和B库中tbl_b
有关联,可以定时将指定的表做同步;
跨库事务
由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价,所以实际上我们只会对历史数据进行分表分库拆分。
跨库事务
由于数据存储到了不同的库上,数据库事务管理出现了困难。跨库事务的核心是保障一致性问题,主要有两种方式:
- 分布式事务:分布式事务能最大限度保证了数据库操作的原子性,但是成本过高,一般不采用。
- 最终一致性:采用本地事务,本地落地,补偿发送。
如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价,所以实际上我们只会对历史数据进行分表分库拆分。
跨库分页、排序、函数问题
跨节点多库进行查询时,会出现limit
分页、order by
排序等问题:
- 当排序字段就是分片字段时:通过分片规则就比较容易定位到指定的分片;
- 当排序字段非分片字段时:需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。
唯一全局主键问题
在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID
无法保证全局唯一,常见有两种方案:
- UUID:主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于
UUID
非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB
下,UUID
的无序性会引起数据位置频繁变动,导致分页。 - snowflake雪花算法:解决了分布式系统生成全局
ID
的需求,生成64位的Long
型数字,生成的ID
整体上按时间趋势递增;
分表分库的业务场景
对数据库进行分表分库时可参考一下原则:
- 能不切分尽量不要切分:不到万不得已不用轻易使用分表分库,避免“过度设计”和“过早优化”。
- 数据量过大,正常运维影响业务访问:单表太大,备份时需要大量的磁盘
IO
和网络IO
等。 - 业务发展,需要对某些字段垂直拆分:比如我们有时候会把用户登陆这部分从用户表独立出来,做成登陆表。
- 数据量快速增长:如果由于数据量的增长,性能接近瓶颈,就需要考虑水平切分,要根据业务选择合适的规则切分,如对
ID
取模或者对手机号做hash
运算等。 - 安全型和可用性:将数据切分到不同的数据库可以降低数据库宕机对业务的影响。
MyCat
实现Mysql
的读写分离本质上其实就是分表分库的实践:具体可参考我的博客:MyCat入门
近期评论