当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
- 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。
- 优化SQL和索引
- 缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑使用应用级别的缓存如Redis;
- 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
- MySQL自带的分区表。无需更改代码,但是sql语句是需要针对分区表做优化的,sql条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区
- 垂直分区,其实就是根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统;
- 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的
sharding key
,为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表;
【1】读写分离架构
读写分离是依赖于主从复制,而主从复制又是为读写分离服务的。因为主从复制要求slave不能写只能(如果对slave执行写操作,那么show slave status
将会呈现Slave_SQL_Running=NO
,此时你需要手动同步一下slave)。
读写分离架构能提升整体负载,但是不能解决单表数据量过大的问题。
① 使用mysql-proxy代理
优点:直接实现读写分离和负载均衡,不用修改代码,master和slave用一样的帐号,mysql官方不建议实际生产中使用
缺点:降低性能, 不支持事务
② 指定数据源
使用AbstractRoutingDataSource+aop+annotation在dao层决定数据源。如果采用了mybatis, 可以将读写分离放在ORM层,比如mybatis可以通过mybatis plugin拦截sql语句,所有的insert/update/delete都访问master库,所有的select 都访问salve
库,这样对于dao层都是透明。
plugin实现时可以通过注解或者分析语句是读写方法来选定主从库。不过这样依然有一个问题, 也就是不支持事务, 所以我们还需要重写一下DataSourceTransactionManager, 将read-only的事务扔进读库, 其余的有读有写的扔进写库。
当然可以使用AbstractRoutingDataSource+aop+annotation在service层决定数据源,可以支持事务。这种方式的缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。
【2】表分区
MySQL在5.1版引入的分区是一种简单的水平拆分,用户需要在建表的时候加上分区参数,对应用是透明的无需修改代码。
对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成,实现分区的代码实际上是通过对一组底层表的对象封装,但对SQL层来说是一个完全封装底层的黑盒子。MySQL实现分区的方式也意味着索引也是按照分区的子表定义,没有全局索引。
用户的SQL语句是需要针对分区表做优化,SQL条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区,可以通过EXPLAIN PARTITIONS来查看某条SQL语句会落在那些分区上,从而进行SQL优化。
分区的好处
- 可以让单表存储更多的数据
- 分区表的数据更容易维护,可以通过清除整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作
- 部分查询能够从查询条件确定只落在少数分区上,速度会很快
- 分区表的数据还可以分布在不同的物理设备上,从而高效利用多个硬件设备
- 可以使用分区表来避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争
- 可以备份和恢复单个分区
分区的限制和缺点
- 一个表最多只能有1024个分区
- 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
- 分区表无法使用外键约束
- NULL值会使分区过滤无效
- 所有分区必须使用相同的存储引擎
分区的类型
- RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区。
- LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择。
- HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式。
- KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值。
分区适合的场景
最适合的场景数据的时间序列性比较强,则可以按时间来分区,如下所示:
CREATE TABLE members ( firstname VARCHAR(25) NOT NULL, lastname VARCHAR(25) NOT NULL, username VARCHAR(16) NOT NULL, email VARCHAR(35), joined DATE NOT NULL ) PARTITION BY RANGE( YEAR(joined) ) ( PARTITION p0 VALUES LESS THAN (1960), PARTITION p1 VALUES LESS THAN (1970), PARTITION p2 VALUES LESS THAN (1980), PARTITION p3 VALUES LESS THAN (1990), PARTITION p4 VALUES LESS THAN MAXVALUE );
查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容的批量删除。
如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存
另外MySQL有一种早期的简单的分区实现 – 合并表(merge table),限制较多且缺乏优化,不建议使用,应该用新的分区机制来替代。
【3】垂直拆分和水平拆分
① 垂直拆分(分表、分库)
垂直分库
是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。或者 用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆 分成两个单独的表
。
- 如果数据库中的数据表过多,可以采用 垂直分库 的方式,将关联的数据表部署在同一个数据库上。
- 如果数据表中的列过多,可以采用 垂直分表 的方式,将一张数据表分拆成多张数据表,把经常一起使用的列放到同一张表里。
垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联,比如原始的用户表是:
直拆分后是:
简单来说垂直分表是指数据表列的拆分,把一张列比较多的表拆分为多张表。 把主键和一些列放在一个表,然后把主键和另外的列放在另一个表中。
垂直拆分的优点
- 可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)
- 可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
- 简化表的结构,数据维护简单
垂直拆分的缺点
主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂。垂直拆分并不能解决单表数据量过大的问题
,如果需要解决单表数据量过大还需要进行水平拆分。
如果需要解决单台服务器性能瓶颈,还需要根据模块进行垂直拆分表放到不同服务器数据库,比如订单表和台账表存放到两个数据库服务器上。这种称之为垂直分库
。
有些分表的策略基于应用层的逻辑算法,一旦逻辑算法改变,整个分表逻辑都会改变,扩展性较差 对于应用层来说,逻辑算法增加开发成本管理冗余列。
② 水平分表(分库)
水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表。
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。常见的策略有Hash、取模、根据实际维度进行分片等。
库内分表,仅仅是单纯的解决了单一表数据过大的问题,由于没有把表的数据分布到不同的机器上,因此对于减轻MySQL服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的IO、CPU、网络,这个就要通过分库来解决。
水品拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。
前面垂直拆分的用户表如果进行水平拆分,结果是:
实际情况中往往会是垂直拆分和水平拆分的结合,即将Users_A_M和Users_N_Z再拆成Users和UserExtras,这样一共四张表。
水平拆分的优点
- 不存在单库大数据和高并发的性能瓶颈
- 应用端改造较少
- 提高了系统的稳定性和负载能力
水平拆分的缺点
- 分片事务一致性难以解决
- 跨节点Join性能差,逻辑复杂
- 数据多次扩展难度跟维护量极大
水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。给应用增加复杂度,通常查询时需要多个表名,查询所有数据都需UNION操作
尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。
③ 分库分表后面临的问题
① 事务支持
分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
② 跨库join
只要是进行切分,跨节点Join的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。
解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。
③ ID问题
一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由。
常见的主键生成策略有UUID和Snowflake 。
UUID
使用UUID作主键是 简单的方案,但是缺点也是非常明显的。由于UUID非常的长,除占用大量存储空间外, 主要的问题是在索引上,在建立索引和基于索引进行查询时都存在性能问题。
最好避免随机的(不连续且值的分别范围非常大)聚簇索引,特别是对于IO密集型的应用。例如,从性能的角度考虑,使用UUID来作为聚簇索引则会很糟糕:它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
Twitter的分布式自增ID算法Snowflake
在分布式系统中,需要生成全局UID的场合还是比较多的,twitter 的snowflake解决了这种需求,实现也还是很简单的,除去配置信息,核心代码就是毫秒级时间41位 机器ID 10位 毫秒内序列12位。
sequenc
在postgreSQL、oracle、db2数据库中有一个特殊的特性—sequence。 任何时候数据库可以根据当前表中的记录数大小和步长来获取到该表下一条记录数。然而,MySQL是没有这种序列对象的。
MySQL可以这样做,对于插入也就是insert操作,首先就是获取唯一的id了,就需要一个表来专门创建id,插入一条记录,并获取最后插入的ID。也就是说,当我们需要插入数据的时候,必须由这个表来产生id值。
这种方法效果很好,但是在高并发情况下,MySQL的AUTO_INCREMENT将导致整个数据库慢。如果存在自增字段,MySQL会维护一个自增锁,innodb会在内存里保存一个计数器来记录auto_increment值,当插入一个新行数据时,就会用一个表锁来锁住这个计数器,直到插入结束。如果是一行一行的插入是没有问题的,但是在高并发情况下,那就悲催了,表锁会引起SQL阻塞,极大的影响性能,还可能会达到max_connections值。
innodb_autoinc_lock_mode:可以设定3个值:0、1、2
- 0:traditonal (每次都会产生表锁)
- 1:consecutive (默认,可预判行数时使用新方式,不可时使用表锁,对于simple insert会获得批量的锁,保证连续插入)
- 2:interleaved (不会锁表,来一个处理一个,并发最高)
对于myisam表引擎是traditional,每次都会进行表锁的。
通过redis生成ID
这种方法将id的生成工作交给了redis,能保证高可用、唯一性。
④ 跨分片的排序分页
一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。
为了 终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的
结果集进行汇总和再次排序, 后再返回给用户。
③ 分片原则
能不分就不分,参考单表优化。
分片数量尽量少,分片尽量均匀分布在多个数据结点上。因为一个查询SQL跨分片越多,则总体性能越差。虽然要好于所有数据在一个分片的结果,只在必要的时候进行扩容,增加分片数量。
分片规则需要慎重选择做好提前规划,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题,最近的分片策略为范围分片,枚举分片,一致性Hash分片,这几种分片都有利于扩容。
尽量不要在一个事务中的SQL跨越多个分片,分布式事务一直是个不好处理的问题。
查询条件尽量优化,尽量避免Select *
的方式,大量数据结果集下,会消耗大量带宽和CPU资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。
过数据冗余和表分区赖降低跨库Join的可能。
这里特别强调一下分片规则的选择问题,如果某个表的数据有明显的时间特征,比如订单、交易记录等,则他们通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤,比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。
总体上来说,分片的选择是取决于最频繁的查询SQL的条件,因为不带任何Where语句的查询SQL,会遍历所有的分片,性能相对最差,因此这种SQL越多,对系统的影响越大,所以我们要尽量避免这种SQL的产生。
【5】数据库分片方案
由于水平拆分牵涉的逻辑比较复杂,当前也有了不少比较成熟的解决方案。这些方案分为两大类:客户端架构和代理架构。
① 客户端架构
通过修改数据访问层,如JDBC、Data Source、MyBatis,通过配置来管理多个数据源,直连数据库,并在模块内完成数据的分片整合,一般以Jar包的方式呈现
这是一个客户端架构的例子:
可以看到分片的实现是和应用服务器在一起的,通过修改Spring JDBC层来实现。
客户端架构的优点是:
- 应用直连数据库,降低外围系统依赖所带来的宕机风险。
- 集成成本低,无需额外运维的组件。
缺点是:
- 限于只能在数据库访问层上做文章,扩展性一般,对于比较复杂的系统可能会力不从心。
- 将分片逻辑的压力放在应用服务器上,造成额外风险。
② 代理架构
通过独立的中间件来统一管理所有数据源和数据分片整合,后端数据库集群对前端应用程序透明,需要独立部署和运维代理组件。
这是一个代理架构的例子:
代理组件为了分流和防止单点,一般以集群形式存在,同时可能需要Zookeeper之类的服务组件来管理。
代理架构的优点是:
- 能够处理非常复杂的需求,不受数据库访问层原来实现的限制,扩展性强
- 对于应用服务器透明且没有增加任何额外负载
缺点是:
- 需部署和运维独立的代理中间件,成本高
- 应用需经过代理来连接数据库,网络上多了一跳,性能有损失且有额外风险
③ 各方案比较
如此多的方案,如何进行选择?可以按以下思路来考虑:
确定是使用代理架构还是客户端架构。中小型规模或是比较简单的场景倾向于选择客户端架构,复杂场景或大规模系统倾向选择代理架构。
具体功能是否满足,比如需要跨节点ORDER BY,那么支持该功能的优先考虑。不考虑一年内没有更新的产品,说明开发停滞,甚至无人维护和技术支持。最好按大公司->社区->小公司->个人这样的出品方顺序来选择。选择口碑较好的,比如github星数、使用者数量质量和使用者反馈。开源的优先,往往项目有特殊需求可能需要改动源代码。
按照上述思路,推荐以下选择:
- 客户端架构:ShardingJDBC
- 代理架构:MyCat或者Atlas
④ 兼容MySQL且可水平扩展的数据库
目前也有一些开源数据库兼容MySQL协议,如:TiDB,Cubrid。
但其工业品质和MySQL尚有差距,且需要较大的运维投入,如果想将原始的MySQL迁移到可水平扩展的新数据库中,可以考虑一些云数据库:
- 阿里云PetaData
- 阿里云OceanBase
- 腾讯云DCDB
⑤ NoSQL
在MySQL上做Sharding是一种戴着镣铐的跳舞,事实上很多大表本身对MySQL这种RDBMS的需求并不大,并不要求ACID,可以考虑将这些表迁移到NoSQL,彻底解决水平扩展问题,例如:
- 日志类、监控类、统计类数据
- 非结构化或弱结构化数据
- 对事务要求不强,且无太多关联操作的数据