1. 简介

  1. 项目中本身具有非常多的数据库表
  2. 随着项目的开发,数据库表和数据量也都会增多
  • 对数据库进行性能优化

1.1 复制集

  • 复制集(Replication

    • 数据库中数据相同,起到备份作用
    • 高可用 High Available HA

1.2 分布式

  • 分布式(Distribution

    • 数据库中数据不同,共同组成完整的数据集合
    • 通常每个节点被称为一个分片(shard)
    • 高吞吐 High Throughput
  • 复制集与分布式可以单独使用,也可以组合使用(即每个分片都组建一个复制集)

1.3 主从

  • 关于主(Master)从(Slave)

    • 这个概念是从使用的角度来阐述问题的
    • 主节点 -> 表示程序在这个节点上最先更新数据
    • 从节点 -> 表示这个节点的数据是要通过复制主节点而来
    • 复制集 可选 主从、主主、主主从从
    • 分布式 每个分片都是主,组合使用复制集的时候,复制集的是从

2. 复制

1.1 简介

1. 定义

也叫主从同步,数据备份,是一个异步的复制过程

  1. 在两台数据库服务器的基础上实现了 读写分离,把两台数据库服务器分为一台主服务器(master)和一台从服务器(slave),一台主服务器对应一台从服务器。
  2. master只负责写入(write)数据,从服务器只负责 同步 主服务器的数据,并让外部程序读取(read)数据也可以让外部程序读取数据(master也可以让外部程序读取数据)

2. 本质

slave从master获取Binary log,然后再在自己身上完全顺序的执行日志中所记录的各种操作
MySQL服务器之间的主从同步是基于二进制日志机制,主服务器使用二进制日志来记录数据库的变动情况,从服务器通过读取和执行该日志文件来保持和主服务器的数据一致。

3. 原理

  1. 当master一有数据写入,slave的I/O thread连接上master,并请求读取指定日志文件(Binary log)的指定位置之后的日志内容
  2. master接收来自slave的IO thread的请求后,让负责复制的I/O thread通过,根据请求信息读取日志信息(Binary log),返回给slaveI/O thread
  3. slave的IO thread接收到信息后,将接收到的日志内容(数据写入的操作)依次写入slave的Relay log
  4. slave的SQL thread检测到Relaylog新增加内容后,会马上解析该文件的内容,并在自身执行原始SQL语句(数据写入的操作)

复制分成三步:

  1. master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);
  2. slave将master的binary log events拷贝到它的中继日志(relay log);
  3. slave重做中继日志中的事件,将改变反映它自己的数据。
    下图描述了这一过程:

  • 该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。

  • 下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。

  • SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。

  • 此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。

  • 利用主从在达到高可用的同时,也可以通过读写分离提供吞吐量

  • 读写分离对事务是否有影响

对于写操作包括开启事务和提交或回滚要在一台机器上执行,分散到多台master执行后数据库原生的单机事务就失效了。
对于事务中同时包含读写操作,与事务隔离级别设置有关,如果事务隔离级别为read-uncommitted 或者 read-committed,读写分离没影响,如果隔离级别为repeatable-read、serializable,读写分离就有影响,因为在slave上会看到新数据,而正在事务中的master看不到新数据。

3. 作用

  1. 对数据进行备份,也就是主从同步后, 当主服务器宕机后,可以从从服务器中选一台当主服务器,提高可用性;当从服务器宕机后,不会有任何影响,体现了 高可用,数据安全
    可以增加从服务器来提高数据库的读取性能

  2. 读写分离实现后,有两台服务器,分摊了读取数据库服务器的压力,提高了吞吐量,实现了高性能

5. 常用架构

5.1 主从架构
1. 简介

在多加几台数据库服务器的基础上实现了读写分离,把多态数据库服务器分为一台主服务器(master)和多台从服务器(slave),master负责write操作,slave负责read操作,一台主服务器对应多台从服务器。

2. 原理

2. 优缺点

优点:

  1. 一主多从,从库高可用HA,数据安全
  2. 读写分离,提高了吞吐量,实现了高性能
    缺点:
  3. 主库单点,没有实现高可用HA一旦挂了,无法写入
3. 应用场景

微博:微博写微博和读微博的人比例大概是1:10

5.2 主备架构
1. 简介

实质就是开多个数据库服务器,都是master,都可以writer和read,一旦主库挂了,就启用备库

2. 原理

3. 优缺点

优点:

  • 高可用,数据安全
    缺点:
  • 单库读写,性能一般
4. 应用场景

阿里云,美团大企业,性能可以通过多个服务器来解决

5. 问题

既然主备互为备份,为什么不采用双主方案,提供两台Master进行负载均衡

  • 因为有延迟,会出现脏数据,数据不一致
  • 虽然两边执行的修改有先后顺序,但由于 Replication 是异步的实现机制,同样可能导致晚做的修改被做的修改所覆盖
  • 不仅B库数据错误,且A&B库数据不一致
  • 主备架构搭建除了配置双主同步,还需要配置第三故障转移/高可用方案
5.3 高可用复合架构
1. 简介

在主从架构的基础上,进行主库的备份:主从架构+主备架构

2. 原理

3. 优缺点
  • 读写分离,提高吞吐量
  • 主从库实现了高可用HA:主库宕机后,去找从库,同理,从库宕机,去找主库
  • 提高了吞吐量

A库宕机的情况:

2. 读写分离

2.1 Django实现MySQL读写分离

1. Docker安装运行MySQL从机

提示:

  • 搭建一主一从的主从同步。
  • 主服务器:ubuntu操作系统中的MySQL。
  • 从服务器:Docker容器中的MySQL。

1.获取MySQL镜像

  • 主从同步尽量保证多台MySQL的版本相同或相近。
$ sudo docker image pull mysql:5.7.22
或
$ sudo docker load -i 文件路径/mysql_docker_5722.tar

2.指定MySQL从机配置文件

  • 在使用Docker安装运行MySQL从机之前,需要准备好从机的配置文件。
  • 为了快速准备从机的配置文件,直接把主机的配置文件拷贝到从机中。
$ cd ~
$ mkdir mysql_slave
$ cd mysql_slave
$ mkdir data
$ cp -r  /etc/mysql/mysql.conf.d  ./

3.修改MySQL从机配置文件

  • 编辑 ~/mysql_slave/mysql.conf.d/mysqld.cnf文件。
  • 由于主从机都在同一个电脑中,所以选择使用不同的端口号区分主从机,从机端口号是8306。
 # 从机端口号
port = 8306# 关闭日志
general_log = 0# 从机唯一编号
server-id = 2

4.Docker 安装运行 MySQL 从机

  • MYSQL_ROOT_PASSWORD:创建 root 用户的密码为 123456。
$ sudo docker run --name mysql-slave -e MYSQL_ROOT_PASSWORD=123456 -d --network=host -v /home/ubuntu/mysql_slave/data:/var/lib/mysql -v /home/ubuntu/mysql_slave/mysql.conf.d:/etc/mysql/mysql.conf.d mysql:5.7.22

5.测试从机是否创建成功

$ mysql -uroot -p123456 -h127.0.0.1 --port=8306
2. 主从同步实现

1.配置主机( ubuntu 中 MySQL)

  • 配置文件如有修改,需要重启主机。
    sudo service mysql restart
    首先, 进入主机的配置文件所在地:
    cd /etc/mysql/mysql.conf.d/
    进入后找到 mysqld.cnf 文件, 对其进行修改:
sudo vim mysqld.cnf

修改内容如下所示:

 # 开启日志: 把下面的代码注释去掉
general_log_file = /var/log/mysql/mysql.log
general_log = 1# 主机唯一编号
server-id = 1# 二进制日志文件
log_bin = /var/log/mysql/mysql-bin.log

2.从机备份主机原有数据

  • 在做主从同步时,如果从机需要主机上原有数据,就要先复制一份到从机。
# 1. 收集主机原有数据
$ mysqldump -uroot -pmysql --all-databases --lock-all-tables > ~/master_db.sql
# 2. 从机复制主机原有数据
$ mysql -uroot -p123456 -h127.0.0.1 --port=8306 < ~/master_db.sql
3.主从同步实现

1.创建用于从服务器同步数据的帐号

 # 登录到主机
$ mysql –uroot –pmysql# 创建从机账号
$ GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%' identified by 'slave';# 刷新权限
$ FLUSH PRIVILEGES;

2.展示 ubuntu 中 MySQL 主机的二进制日志信息

$ SHOW MASTER STATUS;

3.Docker 中 MySQL 从机连接 ubuntu 中 MySQL 主机

 # 登录到从机
$ mysql -uroot -p123456 -h 127.0.0.1 --port=8306# 从机连接到主机
$ change master to master_host='127.0.0.1', master_user='slave', master_password='slave',master_log_file='mysql-bin.000250', master_log_pos=990250;# 开启从机服务
$ start slave;# 展示从机服务状态
$ show slave status \G;

测试:
在主机中新建一个数据库后,直接在从机查看是否存在。

  1. 增加slave数据库的配置
DATABASES = {'default': { # 写(主机)'ENGINE': 'django.db.backends.mysql', # 数据库引擎'HOST': '172.16.238.128', # 数据库主机'PORT': 3306, # 数据库端口'USER': 'root', # 数据库用户名'PASSWORD': 'mysql', # 数据库用户密码'NAME': 'project' # 数据库名字},'slave': { # 读(从机)'ENGINE': 'django.db.backends.mysql','HOST': '172.16.238.128','PORT': 8306,'USER': 'root','PASSWORD': '123456','NAME': 'project'}
}
  1. 创建和配置数据库读写路由
  1. 创建数据库读写路由
  • 在mall.utils.db_router.py中实现读写路由
class MasterSlaveDBRouter(object):"""数据库读写路由"""def db_for_read(self, model, **hints):"""读所使用的服务器:"""return "slave"def db_for_write(self, model, **hints):"""写所使用的服务器:"""return "default"def allow_relation(self, obj1, obj2, **hints):"""是否运行关联操作"""return True
  1. 配置数据库读写路由
  • 在 dev.py 文件中配置如下参数
DATABASE_ROUTERS = ['meiduo_mall.utils.db_router.MasterSlaveDBRouter']

2.2 falsk实现读写分离

  • 需求分析:
    sqlchemy并没有像django-orm一样内置完善的读写分离方案,但是提供了可以自定义的接口:我们可以借此对flask-sqlchemy进行二次开发,实现读写分离
  • 思路分析:
    • 实现自定义的session类(SignallingSession),继承SignllingSession

      • 重写`get_bind方法,根据读写需求选择对应的数据库地址
    • 实现自定义的SQLAlchemy类,继承与SQLAlchemy
      • 重写create_session方法,在内部实现自定义的Session
  • 虚拟机搭建好Mysql主从,可以直接用于测试使用
    • 主数据库端口 3306
    • 从数据库接口 8306

2.2 项目集成

  • 将工具包routting_db导入common/models中,其中的`routing_sqlchemy.py文件实现了读写分离
import random
from flask import Flask
from flask_sqlalchemy import SQLAlchemy, SignallingSession, get_state
import pymysql
from sqlalchemy import ormpymysql.install_as_MySQLdb()app = Flask(__name__)# 单数据库
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:mysql@192.168.243.151:3306/test30"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# 多数据库-主从
app.config["SQLALCHEMY_BINDS"] = {"master": "mysql://root:mysql@192.168.243.151:3306/test30","slave1": "mysql://root:mysql@192.168.243.151:8306/test30","slave2": "mysql://root:mysql@192.168.243.151:3306/test30",
}# 1. 自定义Session类, 继承SignallingSession, 并重写get_bind方法
class RoutingSession(SignallingSession):def __init__(self, *args, **kwargs):super(RoutingSession, self).__init__(*args, **kwargs)def get_bind(self, mapper=None, clause=None):"""每次数据库操作(增删改查及事务操作)都会调用该方法, 来获取对应的数据库引擎(访问的数据库)"""state = get_state(self.app)if mapper is not None:try:# SA >= 1.3persist_selectable = mapper.persist_selectableexcept AttributeError:# SA < 1.3persist_selectable = mapper.mapped_table# 如果项目中指明了特定数据库,就获取到bind_key指明的数据库,进行数据库绑定info = getattr(persist_selectable, 'info', {})bind_key = info.get('bind_key')if bind_key is not None:return state.db.get_engine(self.app, bind=bind_key)# 使用默认的主数据库# return SessionBase.get_bind(self, mapper, clause)from sqlalchemy.sql.dml import UpdateBase# 如果模型类未指定数据库, 判断是否为写操作# delete和update不会触发_flushing# isinstance(clause, UpdateBase) 判断数据库操作行为,clause如果是增删改查都是属于UpdateBase子类if self._flushing or isinstance(clause, UpdateBase):# 写操作--主数据库print("写操作--主数据库")return state.db.get_engine(self.app, bind="master")else:# 读操作--从数据库slave_key = random.choice(["slave1", "slave2"])print("读操作--从数据库: ", slave_key)return state.db.get_engine(self.app, bind=slave_key)# 2. 自定义SQLALchemy类, 重写create_session方法
class RoutingSQLAlchemy(SQLAlchemy):def create_session(self, options):# 继承-拓展SQLAlchemy的功能,封装一个RoutingSession类实现读写分离return orm.sessionmaker(class_=RoutingSession, db=self, **options)# 自定义RoutingSQLAlchemy类创建数据库对象
db = RoutingSQLAlchemy(app)# 构建模型类
class User(db.Model):__tablename__ = 't_user'id = db.Column(db.Integer, primary_key=True)name = db.Column(db.String(20), unique=True)age = db.Column(db.Integer, default=0, index=True)@app.route('/')
def index():"""增加数据"""# read()# write()# read()# update()return "index"def read():print('---读-----------')users = User.query.all()print(users)for user in users:print(user.id, user.name, user.age)def write():print('---写-----------')user1 = User(name='james', age=20)db.session.add(user1)db.session.commit()def update():print("---更新写---")User.query.filter(User.name == 'xiaoming').update({"name": "Uzi"})db.session.commit()if __name__ == '__main__':# 重置所有继承自db.Model的表# 如果模型类没有设置__bind_ky__属性(指定对应的数据库), 则DDL操作 根据SQLALCHEMY_DATABASE_URI 指定的数据库进行处理# db.drop_all()# db.create_all()app.run(debug=True, port=8888)
  • app/settings/config.py文件中设计值主从数据库的URL地址
# app/settings/config.pyclass DefaultConfig:"""默认配置"""SQLALCHEMY_BINDS = {  # 主从数据库的URI"master": 'mysql://root:mysql@192.168.105.140:3306/hm_topnews',"slave1": 'mysql://root:mysql@192.168.105.140:3306/hm_topnews',"slave2": 'mysql://root:mysql@192.168.105.140:8306/hm_topnews'}
  • app/__init__.py文件中使用自定义SQLAchemy类
# app/__init__.py# from flask_sqlalchemy import SQLAlchemy
# db = SQLAlchemy()from models.routing_db.routing_sqlalchemy import RoutingSQLAlchemy# mysql数据库操作对象
db = RoutingSQLAlchemy()

2. 分片(sharding)

2.1 简介

1. 分库分表前的问题

任何问题都是太大或者太小的问题,这里面对的数据量太大的问题。

  • 用户请求量太大

    因为单服务器TPS,内存,IO都是有限的。 解决方法:分散请求到多个服务器上; 其实用户请求和执行一个sql查询是本质是一样的,都是请求一个资源,只是用户请求还会经过网关,路由,http服务器等。

  • 单库太大

    单个数据库处理能力有限;单库所在服务器上磁盘空间不足;单库上操作的IO瓶颈 解决方法:切分成更多更小的库

  • 单表太大

    CRUD都成问题;索引膨胀,查询超时 解决方法:切分成多个数据集更小的表。

2. 分库分表的方式方法

  • 一般就是垂直切分和水平切分,这是一种结果集描述的切分方式,是物理空间上的切分。 从面临的问题,开始解决,阐述: 首先是用户请求量太大,就堆机器搞定

  • 然后是单个库太大,这时要看是因为表多而导致数据多,还是因为单张表里面的数据多。 如果是因为表多而数据多,使用垂直切分,根据业务切分成不同的库。

  • 如果是因为单张表的数据量太大,这时要用水平切分,即把表的数据按某种规则切分成多张表,甚至多个库上的多张表。 分库分表的顺序应该是先垂直分,后水平分。 因为垂直分更简单,更符合处理现实世界问题的方式。

3. 分片简介

  • 需求分析:

    • 用户请求量太大,会导致web应用无法及时响应->分布式服务器(分散请求到多个服务器上)
    • 表单太大,会导致CRUD都成问题,索引膨胀,查询超时->拆分表
    • 单库太大,会导致单库磁盘空间不足:处理能力有限,出现IO瓶颈->拆分库
  • 作用

    • 分片也成为数据拆分(Shareding),其主要工作就是对单库单表进行拆分,多苦多表共同组成完整的数据集合
    • 分片可以提高吞吐量,同一时间数据的读写完成量更多,扩充单机存储量的容量/读写速度上限
  • 分类

    • 垂直拆分:字段太多
    • 水平拆分
      • 使用频率(常用字段/不常用字段)
      • 垂直分库,分表–blind–key–来实现,修改数据库
      • HASH取模 离散化

    去用户id,然后hash取模,飞陪到不同的数据库上,遮掩

  • 注意点

  • 不要轻易分库分表,因为分片会带来 诸多分布式问题, 让应用的复杂度大量增加

  • 应避免"过度设计"和"过早优化", 先尽力去做其他优化,例如:升级硬件、升级网络、读写分离、索引优化、缓存设计等等。

  • 当数据量达到单表瓶颈时候(参考值: 单表记录1000W+/硬盘100G+),再考虑分库分表

  • 如果需要进行分库分表, 优先考虑垂直拆分

    * 地理区域
    分布式问题* 分布式事务
    * 跨Join/排序/分页方案一:* 不需要分方案二:* 二阶段事务session_options = {“twoparse}* begin:xl* prepare:二阶段预提交* commit:真正提交方案三:* ebay* 状态字段* 1. 表中定义状态字段2. 两个系统定义 ***消息接口***3. 
  • 分库访问

    • flask-sqlalchemy通过
  • 水平拆分:记录太多

3. 垂直拆分

3.1 垂直分表

1. 简介

  • 也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。
  • 字段 将一张表拆分成多张表
  • 对于字段较多的表, 每条记录占用的空间也会较多, 导致每次从硬盘中读取的记录以及查询缓存可缓存的记录数量较少, 影响查询查询效率
  • 针对字段多的表就可以采用垂直分表来进行拆分, 这样可以减少表体积, 提高查询效率

2. 拆分规则

  • 相关性

    • 可以将字段根据 业务逻辑 和 使用的相关性 进行分表划分
    • 如: 用户名和密码经常配合使用, 将其分到用户认证表, 生日和邮箱等个人信息经常一起访问, 将其分到用户信息表
  • 使用频率
    • 可以将字段根据 常用 和 不常用 进行划分, 并进行分表处理
    • 如: 原始用户表中包含了多个字段, 其中有常用的昵称、手机号等字段, 也包含不常用的邮箱、生日等字段, 可以根据使用频率将其分为两张表: 用户基础信息表 和 用户其他信息表
  • 项目中的应用
    • 用户数据垂直分表 user_basic& user_profile
    • 文章数据垂直分表 article_basic & article_content (文章内容较长且只在详情页才需要)

3.2 垂直分库

1. 简介

  • 垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上。为什么? 我们想象一下,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。 所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。

  • 数据库业务层面的拆分,和服务的“治理”,“降级”机制类似,也能对不同业务的数据分别的进行管理,维护,监控,扩展等。 数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。 数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈。

  • 将一个数据库中的多张表拆分到多个数据库(服务器节点)中
  • 注意点:
    • 由于 本地事务不支持跨库操作, 所以应该将 有相关联性的表放在同一个库中
    • 如: 如果后续项目垂直分库, 将用户相关的放在数据库1, 文章相关的放在数据库2
 # 默认
数据库   t_user  t_article   # 垂直分表
数据库   t_user_basic    t_user_detail      t_article_basic    t_article_detail   # 垂直分库
数据库1   t_user_basic          t_user_detail
数据库2   t_article_detail      t_article_basic

3.3 分库访问

  • flask-sqlalchemy 通过配置 SQLALCHEMY_BINDS允许设置多个数据库URI, 并且每个模型类可以 __bind_key__属性 设置自己对应访问的数据库

  • 示例场景如下: 项目进行了分库处理, 包含两个库 db1 和 db2, 用户表t_user存储在db1中, 而地址表t_adr存储在db2中

from flask import Flask
from flask_sqlalchemy import SQLAlchemyapp = Flask(__name__)# 设置多个数据库地址  (用于数据操作)
app.config['SQLALCHEMY_BINDS'] = {'db1': 'mysql://root:mysql@192.168.105.140:3306/db1','db2': 'mysql://root:mysql@192.168.105.140:3306/db2'
}# 其他配置
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_ECHO'] = True# 创建组件对象
db = SQLAlchemy(app)# 用户表  存储在db1中
class User(db.Model):__tablename__ = 't_user'__bind_key__ = 'db1'  # 设置表所在的数据库URIid = db.Column(db.Integer, primary_key=True)name = db.Column(db.String(20))# 地址表   存储在db2中
class Address(db.Model):__tablename__ = 't_adr'__bind_key__ = 'db2'  # 设置表所在的数据库URIid = db.Column(db.Integer, primary_key=True)detail = db.Column(db.String(20), unique=True)user_id = db.Column(db.Integer)@app.route('/')
def index():"""添加数据"""user1 = User(name='张三')db.session.add(user1)db.session.flush()adr1 = Address(detail='中关村3号', user_id=user1.id)adr2 = Address(detail='华强北5号', user_id=user1.id)db.session.add_all([adr1, adr2])db.session.commit()  # 虽然只调用一次commit, 但由于需要到两个数据库进行操作, 其实是两个数据库分别创建一个事务并提交return "index"@app.route('/demo1')
def demo1():"""查询多表数据"""user1 = User.query.filter_by(name='张三').first()adrs = Address.query.filter_by(user_id=user1.id).all()for adr in adrs:print(adr.detail)return 'demo1'if __name__ == '__main__':# 重置所有继承自db.Model的表db.drop_all()db.create_all()app.run(debug=True)

4. 水平拆分

4.1 水平分表

  • 一张表的记录 拆分到多张表中
  • 对于记录较多的表, 会出现 索引膨胀, 查询超时 等问题, 影响用户体验
  • 针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。

4.2 水平分库分表

  • 水平分表后, 将分表分散放在多个数据库节点中
  • 将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。

4.3 水平分库分表拆分规则

  • ID范围/RANGE

    • 从0到10000一个表,10001到20000一个表;
    • 从 0 到 100W 一个表,100W+1 到 200W 一个表。
  • HASH取模 离散化

    • 取用户id,然后hash取模,分配到不同的数据库上。这样可以同时向多个表中插入数据, 提高并发能力, 同时由于用户id进行了离散处理, 不会出现ID冲突的问题
    • 一个商场系统,一般都是将用户,订单作为主表,然后将和它们相关的作为附表,这样不会造成跨库事务之类的问题。 取用户id,然后hash取模,分配到不同的数据库上。
  • 业务

    • 按照业务将数据进行分类并拆分, 如文章包含金融、科技等多个分类, 可以每个分类的数据拆分到一张表中。
  • 地理区域

    比如按照华东,华南,华北这样来区分业务,七牛云等云服务应该就是如此。

  • 时间

    按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。

4.4 数据库定向查询

  • 如果进行了水平拆分, 在没有精确过滤条件的情况下, 可能需要到多个数据库中依次查询目标数据
    *可以对 RoutingSession 进行二次开发, 提供方法进行 数据库定向查询
  • 应用场景如下: 对用户表进行水平分库分表, 用户数据分别保存在 db1.t_userdb2.t_user 中, 项目的其他数据保存在数据库 test
import random
from flask import Flask
from flask_sqlalchemy import SQLAlchemy, SignallingSession, get_state
from sqlalchemy import ormapp = Flask(__name__)# 设置单个数据库URI (用于建表并添加测试数据)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@192.168.105.140:3306/db1'# 设置多个数据库的URI (用于数据操作)
app.config['SQLALCHEMY_BINDS'] = {'db1': 'mysql://root:mysql@192.168.105.140:3306/db1','db2': 'mysql://root:mysql@192.168.105.140:3306/db2','master': 'mysql://root:mysql@192.168.105.140:3306/test','slave': 'mysql://root:mysql@192.168.105.140:3306/test'
}# 其他配置
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True# 1. 自定义Session类, 继承SignallingSession, 并重写get_bind方法
class RoutingSession(SignallingSession):def get_bind(self, mapper=None, clause=None):"""每次数据库操作(增删改查及事务操作)都会调用该方法, 来获取对应的数据库引擎(访问的数据库)"""state = get_state(self.app)if self._bind:  # 如果查询指定了访问的数据库, 则使用指定的数据库print('查询数据库:', self._bind)return state.db.get_engine(self.app, bind=self._bind)elif mapper is not None:  # 如果模型类已指定数据库, 使用指定的数据库info = getattr(mapper.mapped_table, 'info', {})bind_key = info.get('bind_key')if bind_key is not None:return state.db.get_engine(self.app, bind=bind_key)if self._flushing:  # 如果模型类未指定数据库, 判断是否为写操作print('写操作')return state.db.get_engine(self.app, bind='master')else:print('读操作')return state.db.get_engine(self.app, bind='slave')_bind = None  # 定义类属性记录要访问的数据库def using_bind(self, bind):"""指定要访问的数据库"""self._bind = bindreturn self# 2. 自定义SQLALchemy类, 重写create_session方法
class RoutingSQLAlchemy(SQLAlchemy):def create_session(self, options):return orm.sessionmaker(class_=RoutingSession, db=self, **options)# 创建组件对象
db = RoutingSQLAlchemy(app)# 构建模型类
class User(db.Model):__tablename__ = 't_user'id = db.Column(db.Integer, primary_key=True)name = db.Column('username', db.String(20), unique=True)age = db.Column(db.Integer, default=0, index=True)@app.route('/')
def index():for db_bind in ['db1', 'db2']:  # 遍历各数据库节点, 查询用户数据user = db.session().using_bind(db_bind).query(User).filter(User.name == 'zs').first()print(user)if user:print(user.id, user.name, user.age)return "index"if __name__ == '__main__':# 重置所有继承自db.Model的表db.drop_all()db.create_all()# 添加测试数据  需要分别往db1和db2中添加一条数据user1 = User(name='zs', age=20)db.session.add(user1)db.session.commit()app.run(debug=True)

5. 分布式问题

5.1 分布式失去问题

  • 事务支持

    • 分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;
    • 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
  • 本地事务不支持跨库操作
  • 解决办法从简单到复杂有三种

1. 方案一

  • 将有关联的表放在一个数据库中
  • 同库操作可以使用一个事务
  • 如用户表&用户频道表, 文章基本信息表&文章内容表放在一起

2. 方案二

  • Mysql从5.6开始支持分布式事务

  • 核心是二阶段提交协议(简称 2PC协议 / XA协议)

  • 分布式事务会提供一个 事务管理器 来对 各数据库的本地事务进行统一管理, 只有各本地事务都向管理器 预提交 成功后, 事务管理器才会统一执行提交处理, 否则统一进行回滚处理

  • sqlalchemy 也支持分布式事务

    • 只需要在创建 SQLAlchemy对象时, 设置参数 session_options={'twophase': True}即可
  • 设置后, 整个session的所有操作会被放入到一个分布式事务中, 并在整个分布式事务范围内保证原子性


from flask import Flask
from flask_sqlalchemy import SQLAlchemyapp = Flask(__name__)# 设置多个数据库地址
app.config['SQLALCHEMY_BINDS'] = {'db1': 'mysql://root:mysql@192.168.105.140:3306/db1','db2': 'mysql://root:mysql@192.168.105.140:3306/db2'}app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_ECHO'] = True# 创建组件对象  设置二阶段提交
db = SQLAlchemy(app, session_options={'twophase': True})# 用户表
class User(db.Model):__tablename__ = 't_user'__bind_key__ = 'db1'  # 设置数据库db1id = db.Column(db.Integer, primary_key=True)name = db.Column(db.String(20))# 地址表
class Address(db.Model):__tablename__ = 't_adr'__bind_key__ = 'db2'  # 设置数据库db2id = db.Column(db.Integer, primary_key=True)detail = db.Column(db.String(20), unique=True)user_id = db.Column(db.Integer)@app.route('/')
def index():"""添加数据"""user1 = User(name='张三')db.session.add(user1)db.session.flush()adr1 = Address(detail='中关村3号', user_id=user1.id)adr2 = Address(detail='华强北5号', user_id=user1.id)db.session.add_all([adr1, adr2])db.session.flush()db.session.commit()  # 由于采用了分布式事务, 整个session的操作会被放入到一个分布式事务中, 并实现事务的原子性return "index"@app.route('/demo1')
def demo1():"""查询多表数据  需求: 查询姓名为"张三"的所有地址信息"""# 先根据姓名查找用户主键user1 = User.query.filter_by(name='张三').first()# 再根据主键到从表查询关联地址adrs = Address.query.filter_by(user_id=user1.id).all()for adr in adrs:print(adr.detail)return 'demo1'if __name__ == '__main__':# 删除所有继承自db.Model的表db.drop_all()# 创建所有继承自db.Model的表db.create_all()app.run(debug=True)
  • 注意点:

    • 分布式事务要在所有事务都"提交成功"的情况下才会正式提交, 如果参与的部分节点卡顿, 会影响整个事务的性能

3. 方案三

  • 基于状态/消息的最终一致性方案

    • 对于 包含多个子系统的大型项目, 需要保证子系统之间的数据一致性
    • 单个子系统往往不会操作所有数据库, 但是 每个子系统可以通过定义字段来记录操作的状态, 每完成一个阶段则更新相应的状态
    • 如下单-付款流程中, 应用A的下单事务完成后更新订单状态为 已下单, 应用B付款事务完成后, 再通过 支付回调接口 通知应用A 更新订单状态
    • 应用B还需要提供一个 支付查询接口, 以便在用户查询或者订单超时的情况下, 让应用A可以查询订单的支付情况
  • ebay 提出的方案, 理论叫做 BASE

5.2 跨节点 Join/排序/分页

  • 跨库join
  • 不支持的跨库操作包括join/分组/聚合/排序

1. 方案一

  • 分两次查询进行, 在应用端合并

2. 方案二

  • 使用一些第三方方案(数据库中间件)
  • 开源框架除了Mycat, 功能较少
  • 需要一定学习成本, 二次开发需要公司具有一定技术实力
  • 以下为推荐的开源框架:

    MySQL Router:https://github.com/mysql/mysql-router
    Atlas:https://github.com/Qihoo360/Atlas
    Mycat:https://github.com/MyCATApache/Mycat-Server
  • 付费框架推荐: 阿里DRDS
    • 功能: 分库分表、分布式JOIN/聚合/排序、分布式事务、平滑扩容、读写分离, 全局唯一ID
  • 基础版: 14000+/年
  • 一个字: 强!
    分库分表后表之间的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 粗略的解决方法: 全局表:基础数据,所有库都拷贝一份。 字段冗余:这样有些字段就不用join去查询了。 系统层组装:分别查询出所有,然后组装起来,较复杂。

3. 方案三

  • 分库分表方案产品

  • 目前市面上的分库分表中间件相对较多,其中基于代理方式的有MySQL Proxy和Amoeba, 基于Hibernate框架的是Hibernate Shards,基于jdbc的有当当sharding-jdbc, 基于mybatis的类似maven插件式的有蘑菇街的蘑菇街TSharding, 通过重写spring的ibatis template类的Cobar Client。

  • 还有一些大公司的开源产品:

5.3 多库结果集合并(group by,order by)

6. 项目应用

  • 主从

  • 垂直分表

CREATE TABLE `user_basic` (`user_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',`account` varchar(20) COMMENT '账号',`email` varchar(20) COMMENT '邮箱',`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态,是否可用,0-不可用,1-可用',`mobile` char(11) NOT NULL COMMENT '手机号',`password` varchar(93) NULL COMMENT '密码',`user_name` varchar(32) NOT NULL COMMENT '昵称',`profile_photo` varchar(128) NULL COMMENT '头像',`last_login` datetime NULL COMMENT '最后登录时间',`is_media` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否是自媒体,0-不是,1-是',`is_verified` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否实名认证,0-不是,1-是',`introduction` varchar(50) NULL COMMENT '简介',`certificate` varchar(30) NULL COMMENT '认证',`article_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '发文章数',`following_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关注的人数',`fans_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '被关注的人数',`like_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '累计点赞人数',`read_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '累计阅读人数',PRIMARY KEY (`user_id`),UNIQUE KEY `mobile` (`mobile`),UNIQUE KEY `user_name` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户基本信息表';CREATE TABLE `user_profile` (`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',`gender` tinyint(1) NOT NULL DEFAULT '0' COMMENT '性别,0-男,1-女',`birthday` date NULL COMMENT '生日',`real_name` varchar(32) NULL COMMENT '真实姓名',`id_number` varchar(20) NULL COMMENT '身份证号',`id_card_front` varchar(128) NULL COMMENT '身份证正面',`id_card_back` varchar(128) NULL COMMENT '身份证背面',`id_card_handheld` varchar(128) NULL COMMENT '手持身份证',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`register_media_time` datetime NULL COMMENT '注册自媒体时间',`area` varchar(20) COMMENT '地区',`company` varchar(20) COMMENT '公司',`career` varchar(20) COMMENT '职业',PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户资料表';
CREATE TABLE `news_article_basic` (`article_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '文章ID',`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',`channel_id` int(11) unsigned NOT NULL COMMENT '频道ID',`title` varchar(128) NOT NULL COMMENT '标题',`cover` json NOT NULL COMMENT '封面',`is_advertising` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否投放广告,0-不投放,1-投放',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '贴文状态,0-草稿,1-待审核,2-审核通过,3-审核失败,4-已删除',`reviewer_id` int(11) NULL COMMENT '审核人员ID',`review_time` datetime NULL COMMENT '审核时间',`delete_time` datetime NULL COMMENT '删除时间',`reject_reason` varchar(200) COMMENT '驳回原因',`comment_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '累计评论数',`allow_comment` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否允许评论,0-不允许,1-允许',PRIMARY KEY (`article_id`),KEY `user_id` (`user_id`),KEY `article_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章基本信息表';CREATE TABLE `news_article_content` (`article_id` bigint(20) unsigned NOT NULL COMMENT '文章ID',`content` longtext NOT NULL COMMENT '文章内容',PRIMARY KEY (`article_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='文章内容表';

MySQL分布式设计相关推荐

  1. MySQL分布式事务(XA事务)

    MySQL分布式事务(XA事务) 官网:https://dev.mysql.com/doc/refman/5.7/en/xa.html 1.什么是分布式事务 分布式事务就是指事务的参与者.支持事务的服 ...

  2. sql 拆分_实践参考:MySQL架构设计从开发规范、选型、拆分到减压实战指南

    导引 作者,李辉,原新浪爱彩票运维负责人,常用网名:门牙没了.曾主导新浪爱彩票的MySQL运维工作.培训合伙人.资深讲师,中国科学院大学在读研究生(大数据方向),擅长大型项目的关系型数据库运维和管理, ...

  3. 深入解读 MySQL 架构设计原理,剖析存储架构选型核心

    孙玄,江湖人称"玄姐",前58集团技术委员会主席,前转转二手交易平台首席架构师.今天想跟你聊点儿企业里那些年薪百万的架构师,他们的架构设计思维是如何升级的,以及他们是如何玩转 My ...

  4. 分布式设计与开发(四)------数据拆分

    一个大型系统里各个环节中最容易出性能和可用性问题的往往是数据库,因此分布式设计与开发的一个重要领域就是如何让数据层具有可扩展性,数据库的扩展分为Scale Up 和Scale Out,而Scale U ...

  5. 老九学堂之分布式设计教材

    老九学堂之分布式设计教材 作者:老九-技术大黍 原文:分布式系统设计教材 社交:知乎 公众号:老九学堂(新人有惊喜) 特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权 前言 本文由老 ...

  6. 系统和分布式设计专题

    1.模块化的好处 那么什么是模块化呢?< Java 应用架构设计:模块化模式与 OSGi >一书中对它的定义是:模块化是一种处理复杂系统分解为更好的可管理模块的方式.为什么模块间解耦,复用 ...

  7. 不属于mysql常量的是_MySQL中,下面______常量的写法是不正确的?-智慧树MySQL数据库设计与应用章节答案...

    MySQL数据库设计与应用:MySQL中,下面______常量的写法是不正确的?[?ж???] A:'2.1E5 B:0x123abc C:b'121' D:null MySQL数据库设计与应用章节测 ...

  8. 阿里开源Mysql分布式中间件:Cobar

    这几天研究了下Cobar, Cobar是阿里巴巴研发的关系型数据的分布式处理系统(Amoeba的升级版,该产品成功替代了原先基于Oracle的数据存储方案,目前已经接管了3000+个MySQL数据库的 ...

  9. 面试mysql中怎么创建索引_阿里面试:MySQL如何设计索引更高效?

    有情怀,有干货,微信搜索[三太子敖丙]关注这个不一样的程序员. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列文章. ...

最新文章

  1. Django 数据库
  2. R语言构建xgboost模型:控制训练信息输出级别verbose参数
  3. 验证码的三种做法[纯数字][纯汉字][字母\数字\汉字混合]C#
  4. shell脚本复制文件夹内容到另外的文件夹,如果存在则自动备份
  5. windows操作系统中配置java环境
  6. Nacos源码InstanceController
  7. 多个div并排居中显示
  8. C++ 模板的局限性以及解决01
  9. [Caffe]:关于 Error parsing text-format Caffe.NetParameter: xxx.xx : Expected interger or identifier.
  10. 八款你不得不知的开源前端JS框架
  11. 十分钟倒计时flash_Flash 101-第6部分:最终倒计时
  12. python贴吧-python爬取贴吧帖子
  13. python 过采样算法_浅谈SMOTE算法 如何利用Python解决非平衡数据问题
  14. MATLAB计算二阶系统单位阶跃响应及在for循环中在图像加图例
  15. 杏子语录(2020年01月)
  16. 《Android软件安全权威指南》原生程序分析 阅读笔记
  17. [转载]jQuery诞生记-原理与机制
  18. 【记录CF】Codeforces Round #777 (Div. 2) A~C 题解
  19. Pod 一直停留在 Terminating 状态,我等得花儿都谢了~
  20. IOS客户端app在线安装ipa包,

热门文章

  1. 笔记:Linux环境C语言复习(16)// 网络
  2. jQuery实现Windows扑克牌小游戏代码
  3. 2019-8-24 [Linux] 15.搭建两台虚拟机实现互联组成一个小型网络 详细版
  4. 振兴会杜振国分析目前传统基建板块的情况
  5. 防统方Oracle,风信子医院数据库安全与防统方解决方案
  6. 3 个鲜为人知的 Python 特性
  7. 【HDU4966】GGS-DDU
  8. 图纸文档管理系统让设计变得更简单、规范
  9. 2050的编程竞赛青年见面会感想
  10. 【Nodejs】Too many levels of symbolic links