MySQL分布式设计

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 是异步的实现机制,同样可能导致晚做的修改被做的修改所覆盖
    Replicaiton
  • 不仅B库数据错误,且A&B库数据不一致
  • 主备架构搭建除了配置双主同步,还需要配置第三故障转移/高可用方案
5.3 高可用复合架构
1. 简介

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

2. 原理

高可用复合架构

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

A库宕机的情况:
A库宕机
B库宕机

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 orm

pymysql.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.3
                persist_selectable = mapper.persist_selectable
            except AttributeError:
                # SA < 1.3
                persist_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.py

class 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 SQLAlchemy

app = 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'  # 设置表所在的数据库URI
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))


 # 地址表   存储在db2中
class Address(db.Model):
    __tablename__ = 't_adr'
    __bind_key__ = 'db2'  # 设置表所在的数据库URI
    id = 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 orm

app = 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 = bind
        return 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 SQLAlchemy

app = 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'  # 设置数据库db1
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))


# 地址表
class Address(db.Model):
    __tablename__ = 't_adr'
    __bind_key__ = 'db2'  # 设置数据库db2
    id = 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='文章内容表';
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值