当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锁竞争
-
可以备份和恢复单个分区
分区的限制和缺点
分区的类型
-
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】垂直拆分和水平拆分
① 垂直拆分(分表、分库)
垂直分库
是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。或者 用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆
分成两个单独的表
。
垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联,比如原始的用户表是:
垂直拆分后是:
简单来说垂直分表是指数据表列的拆分,把一张列比较多的表拆分为多张表。把主键和一些列放在一个表,然后把主键和另外的列放在另一个表中。
垂直拆分的优点
垂直拆分的缺点
主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂。垂直拆分并不能解决单表数据量过大的问题,如果需要解决单表数据量过大还需要进行水平拆分。
如果需要解决单台服务器性能瓶颈,还需要根据模块进行垂直拆分表放到不同服务器数据库,比如订单表和台账表存放到两个数据库服务器上。这种称之为垂直分库。
有些分表的策略基于应用层的逻辑算法,一旦逻辑算法改变,整个分表逻辑都会改变,扩展性较差 对于应用层来说,逻辑算法增加开发成本管理冗余列。
② 水平分表(分库)
水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表。