【数据库设计】分库,分表,主从,读写分离

Mysql性能优化

一 数据库设计合理性

1.1范式

         为了建立冗余较小、结构合理的数据库,设计数据库时必须遵循一定的规则。在关系型数据库中这种规则就称为范式。范式是符合某一种设计要求的总结。要想设计一个结构合理的关系型数据库,必须满足一定的范式。

 

1.2 遵循三范式的反三范式设计

第一范式:1NF是对属性的原子性约束,要求属性(列)具有原子性,不可再分解;(只要是关系型数据库都满足1NF)

第二范式:2NF是对记录的惟一性约束,表中的记录是唯一的, 就满足2NF, 通常我们设计一个主键来实现,主键不能包含业务逻辑。

第三范式:3NF是对字段冗余性的约束,它要求字段没有冗余。 没有冗余的数据库设计可以做到。但是,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。具体做法是: 在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,允许冗余。

二 分表分库技术(取模分表,水平分割,垂直分割)

2.1 垂直拆分

2.1.1 垂直分库

         垂直拆分就是要把表按模块划分到不同数据库中。比如大型的电商项目平台是由不同的子项目模块构成的如订单系统,账户系统,商品管理系统等等,这些个独立的模块都可以拆分成独立的服务与独立的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务治理"的做法相似,每个微服务使用单独的一个数据库。如下图所示一个大型的电商应用拆分成一个个小应用于单独的库。

2.1.2 垂直分表

如上图标所示原本是一个大表的表结构,被拆垂直拆分成了两个很小的子表。当然我们的原则是能不分表就不分表,除非这张表中字段比较多,我们可以将不经常用,不经常查询的字段或者大的文本字段拆分到垂直扩展的表中。把字段多的大表拆分成小表。不仅便于开发与维护,当然也可以避免跨页的问题。Mysql底层是按页的方式来存储的,字段相对较少就可以避免跨页访问,减少系统的开销。字段相对比较少,访问频率高,内存加载更多的数据,提高查询的命中率,减少磁盘的IO,提高系统的性能。垂直分表最好是根据业务来进行划分,如果单表字段过大,且大部分字段不参与业务维度的统计,就没必要把这一部分数据查询出来了。垂直分表是1v1的关系。所以主表与扩展表是一对一的映射关系。

2.1.3 垂直拆分优缺点

(1)优点:

解决业务系统层面的耦合,业务清晰

与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等

并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈

(2)缺点:

部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度

分布式事务处理复杂

依然存在单表数据量过大的问题(需要水平切分)

 

2.2 水平拆分

当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。

 

2.2.1 库内分表

对于在一个数据库里面存储的表,如果数据量过大的情况下,对大表的查询或者按照不同的业务维度对单表进行水平的分表可以对业务的查询有一定的帮助,可以提高查询性能。当然也不是随意对表进行拆分的,前提是确实有这样的业务并且有这样的需求。

 

如上图所示是库内水平分表的示意图,这样进行数据库的分表,有助于根据指定的维护进行查询,提高查询的性能。具体怎么进行水平分表,当然也是根据业务规则来进行拆分的。比如本人最近参与的一个项目10000个经销商,每个经销商可以投放n多的广告。那么按照3-5年的广告投放量来算的话。一个经销商投放1000条广告也就是1千万的数据量了。一个表容量.这么来说放到一个表里就不太合适了,这个时候可以按经销商 ID根据具体的业务规则放到不同的表中。减轻根据单个经销商查询的压力。

2.2.2 分库分表

         库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。

如上图所示是分库分表的示意图,如图我们把单个库中某个表,分别分到下面3个库中的表中,按照指定的数据库分库分表规则与字段分配策略进行拆分。如上图所示通过分库分表的规则就可以解决数据库IO瓶颈,数据库连接压力,CPU,网络,内存等等的瓶颈问题。这个是数据库瓶颈问题的解决方案。

 

2.2.3 分表策略

2.2.3.1 按范围拆分

按照时间区间或ID区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。

优点:

单表大小可控

天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移

使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。

缺点:

热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询

2.2.3.2 根据字段值取模

 

如上图所示是取key%n的运算进行分表的,要分多少张表就进行模多少的运算。实际上我们可以把这个取模的运算看做是一个hash函数,只是这个函数是一个取模的函数罢了总的一句话这种方式就是 hash分表。当然对于水平拆分如何创建一个唯一的自增Id呢,这个时候我们可以采用一张辅助的表,来形成一个唯一的Id值

一般采用hash取模mod的切分方式,例如:将 Customer 表根据 key 字段切分到3个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据会分散到同一个库中,如果查询条件带有key字段,则可明确定位到相应库去查询。

 

优点:

数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈

缺点:

后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题)容易面临跨分片查询的复杂问题。

2.2.3.2 根据时间拆分

    

如上图所示我们可以按时间去拆分,只是这个拆分周期就会作为表的后缀名,比如当前时间是18年的数据往18表存,19的往19表存,根据不同的时间粒度可以把表拆分成为年表,月表,天表,小时表,分钟表这样对于大量的按时间做维度的数据统计来说就能够快速的定位到表并得出报表。比如移动或者联动的实时的流量,攻击的次数这些按时间统计的数据就可以按日期来做分表操作。

 

优点:

单表大小可控

天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移

使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。

 

缺点:

热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询,联表查询困难。

注意!  如上所示说明的分表的方式不仅仅只局限于数据Id的范围或者时间,实际上分表的规则是来自于业务场景的,它可以按照你的任何字段来分表。可以对不同的字段做数字的hash() 再通过数值做hash()这样也可以分表。具体还是看对于真正的用户来说,它怎么方便查询,要怎么操作查询才合适。还有就是并不是分表就是最佳的性能实践。分表以后也会造成一些分表查询的问题。所以最好站在业务的高度上去分表,分库。所有的技术都是为业务服务的,要不然就是耍流氓。

 

 

2.2.4 分表实践  

2.2.4.1 创建表

create table user0(

id int unsigned primary key ,

name varchar(32) not null default '',

pwd  varchar(32) not null default '')

engine=myisam charset utf8;


create table user1(

id int unsigned primary key ,

name varchar(32) not null default '',

pwd  varchar(32) not null default '')

engine=myisam charset utf8;


create table user2(

id int unsigned primary key ,

name varchar(32) not null default '',

pwd  varchar(32) not null default '')

engine=myisam charset utf8;



create table uuid(

id int unsigned primary key auto_increment)engine=myisam charset utf8;

如上所示的sql语句创建三张user表,其中一张辅助生成表,辅助生成Id用。

2.2.4.2 业务逻辑代码

@Service

public class UserService {


     @Autowired

     private JdbcTemplate jdbcTemplate;


     public String regit(String name, String pwd) {

         // 1.先获取到 自定增长ID

         String idInsertSQL = "INSERT INTO uuid VALUES (NULL);";

         jdbcTemplate.update(idInsertSQL);

         Long insertId = jdbcTemplate.queryForObject("select last_insert_id()", Long.class);

         // 2.判断存储表名称

         String tableName = "user" + insertId % 3;

         // 3.注册数据

         String insertUserSql = "INSERT INTO " + tableName + " VALUES ('" + insertId + "','" + name + "','" + pwd

                   + "');";

         System.out.println("insertUserSql:" + insertUserSql);

         jdbcTemplate.update(insertUserSql);

         return "success";

     }


     public String get(Long id) {

         String tableName = "user" + id % 3;

         String sql = "select name from " + tableName + "  where id="+id;

         System.out.println("SQL:" + sql);

         String name = jdbcTemplate.queryForObject(sql, String.class);

         return name;

     }

}

        

如上所示是一个业务层代码的实践案例,如上代码所示,它就是通过一个空表进行主键生成,又按主键来进行分表的一种方式。

2.3 主从复制

        

2.3.1 主从复制示意

    影响MySQL-A数据库的操作,在数据库执行后,都会写入本地的日志系统A中。

 假设,实时的将变化了的日志系统中的数据库事件操作,在MYSQL-A的3306端口,通过网络发给MYSQL-B。

 MYSQL-B收到后,写入本地日志系统B,然后一条条的将数据库事件在数据库中完成。

 那么,MYSQL-A的变化,MYSQL-B也会变化,这样就是所谓的MYSQL的复制,即MYSQL replication。

 在上面的模型中,MYSQL-A就是主服务器,即master,MYSQL-B就是从服务器,即slave。

 日志系统A,其实它是MYSQL的日志类型中的二进制日志,也就是专门用来保存修改数据库表的所有动作,即bin log。【注意MYSQL会在执行语句之后,释放锁之前,写入二进制日志,确保事务安全】

 日志系统B,并不是二进制日志,由于它是从MYSQL-A的二进制日志复制过来的,并不是自己的数据库变化产生的,有点接力的感觉,称为中继日志,即relay log。

 可以发现,通过上面的机制,可以保证MYSQL-A和MYSQL-B的数据库数据一致,但是时间上肯定有延迟,即MYSQL-B的数据是滞后的。

【即便不考虑什么网络的因素,MYSQL-A的数据库操作是可以并发的执行的,但是MYSQL-B只能从relay log中读一条,执行下。因此MYSQL-A的写操作很频繁,MYSQL-B很可能跟不上。】

 

2.3.2 配置主从

2.3.2.1 配置主服务器

(1) 首先准备两个安装好mysqlserver的服务器

(2)  修改主服务器(master)

vim /etc/my.cnf  新增以下内容

server_id=177  ###服务器id

log-bin=mysql-bin   ###开启日志文件

 (3) 重新启动一下(master)

Service mysqld stop

Service mysqld start

 (4) 查询一下检测一下server_id是否配置正确

SHOW VARIABLES LIKE 'server_id'

(5)通过如下所示的脚本,查询一下日志文件

SHOW MASTER STATUS;

 (6) 查询窗口中给从服务器授权

  

GRANT REPLICATION SLAVE ON *.* to 'mysync'@'%' identified by 'q123456';

2.3.2.2 配置从服务器

(1)/etc/my.cnf 下配置服务器id

server_id=178

log-bin=mysql-bin

binlog_do_db=test

   (2)重启mysql服务器

     service mysqld restart

(3)同上检查id等

4查询窗口执行同步脚本

STOP SLAVE

CHANGE MASTER TO MASTER_HOST='192.168.232.128',MASTER_USER='admin',MASTER_PASSWORD='admin',

         MASTER_LOG_FILE='mysql-bin.000001',MASTER_LOG_POS=316;

START SLAVE

中间的master_host 主库

 Master_user=admin

 Master_password=admin

Master_log_file=通过这个脚本SHOW MASTER STATUS;master查询相关的日志文件信息与位置端口号,这样主从就建好了

   5 SHOW SLAVE STATUS 查看是否已经做了同步

  

 如上图所示 我们已经看到已经做了主从了。这样主数据库的数据变更就会同步到从数据库了。

2.4 读写分离

        

     在数据库集群架构中,让主库负责处理事务性查询,而从库只负责处理select查询,让两者分工明确达到提高数据库整体读写性能。当然,主数据库另外一个功能就是负责将事务性查询导致的数据变更同步到从库中,也就是写操作。这就是数据库的读写分离。

2.4.1 优点

 1)分摊服务器压力,提高机器的系统处理效率

  2)增加冗余,提高服务可用性,当一台数据库服务器宕机后可以调整另外一台从库以最快速度恢复服务

 

2.5 mycat中间件

是一个开源的分布式数据库系统,但是因为数据库一般都有自己的数据库引擎,而Mycat并没有属于自己的独有数据库引擎,所有严格意义上说并不能算是一个完整的数据库系统,只能说是一个在应用和数据库之间起桥梁作用的中间件。

在Mycat中间件出现之前,MySQL主从复制集群,如果要实现读写分离,一般是在程序段实现,这样就带来了一个问题,即数据段和程序的耦合度太高,如果数据库的地址发生了改变,那么我的程序也要进行相应的修改,如果数据库不小心挂掉了,则同时也意味着程序的不可用,而对于很多应用来说,并不能接受;

    引入Mycat中间件能很好地对程序和数据库进行解耦,这样,程序只需关注数据库中间件的地址,而无需知晓底层数据库是如何提供服务的,大量的通用数据聚合、事务、数据源切换等工作都由中间件来处理;

    Mycat中间件的原理是对数据进行分片处理,从原有的一个库,被切分为多个分片数据库,所有的分片数据库集群构成完成的数据库存储,有点类似磁盘阵列中的RAID0.

 2.5.1配置mycat

(1)配置server.xml文件

         <!-- 添加user -->    <

user name="mycat">

                   <property name="password">mycat</property>

                   <property name="schemas">mycat</property>

    </user>

        

         <!-- 添加user -->

   <user name="mycat_red">

                   <property name="password">mycat_red</property>

                   <property name="schemas">mycat</property>

                   <property name="readOnly">true</property>

</user>

mycat的安装目录下面配置可读可写的访问方式。

(2) 配置schema文件

         <?xml version="1.0"?>

<!DOCTYPE mycat:schema SYSTEM "schema.dtd">

<mycat:schema xmlns:mycat="http://org.opencloudb/">

    <!-- 与server.xml中user的schemas名一致 -->

    <schema name="mycat" checkSQLschema="true" sqlMaxLimit="100">

        <table name="t_users" primaryKey="user_id" dataNode="dn1" rule="rule1"/>

        <table name="t_message" type="global" primaryKey="messages_id" dataNode="dn1" />

    </schema>

<dataNode name="dn1" dataHost="jdbchost" database="weibo_simple

" />

  

    <dataHost name="jdbchost" maxCon="1000" minCon="10" balance="1"

                writeType="0" dbType="mysql" dbDriver="native" switchType="1"

                slaveThreshold="100">

         <heartbeat>select user()</heartbeat> 

        <writeHost host="hostMaster" url="192.168.232.128:3306" user="root" password="root">

        </writeHost>

        <writeHost host="hostSlave" url="192.168.232.129:3306" user="root" password="root"/>

    </dataHost>

</mycat:schema>

如上所示指定了主库与从裤

(3)配置rule.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<!-- - - Licensed under the Apache License, Version 2.0 (the "License");

         - you may not use this file except in compliance with the License. - You

         may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0

         - - Unless required by applicable law or agreed to in writing, software -

         distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT

         WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the

         License for the specific language governing permissions and - limitations

         under the License. -->

<!DOCTYPE mycat:rule SYSTEM "rule.dtd">

<mycat:rule xmlns:mycat="http://org.opencloudb/">

          <tableRule name="rule1">

        <rule>

            <columns>user_id</columns>

            <algorithm>func1</algorithm>

        </rule>

    </tableRule>

    <function name="func1" class="org.opencloudb.route.function.AutoPartitionByLong">

       <property name="mapFile">autopartition-long.txt</property>

    </function>

</mycat:rule>

(4) log4j.xml配置

<level value=”debug” />

(5)启动mycat

(6)通过不同的server.xml配置的账户进行连接操作检测读写分离。

猜你喜欢

转载自blog.csdn.net/worn_xiao/article/details/83963932