MySQL的分布式——flask-sqlalchemy实现读写分离

1、复制

作用

  • 对数据进行备份,实现高可用HA
  • 通过读写分离,提高吞吐量,实现高性能

原理

  • 当主库中有数据更新时,主库会将该操作写入一个二进制日志文件中,从库中专门有一个io线程去读取主库的二进制文件的内容,但不会立即写入从库,而是将它写入一个延后中继日志文件,然后再通过SQL线程重做中继日志文件中的数据修改的操作,保存到从库中去,从而完成主库和从库的数据同步。

在这里插入图片描述

1.1 主从架构(一主多从)
读写分离,提高吞吐量
主库只有一台,一旦挂了,无法写入,所以主库并没有实现高可用
从库实现了高可用

在这里插入图片描述

1.2 主备架构
单库读写,没有实现读写分离, 性能一般
高可用, 一旦主库挂了, 就启用备库
这种方案被阿里云、美团等企业广泛使用

在这里插入图片描述

问题: 既然主备互为备份, 为什么不采用双主方案, 提供两台主进行负载均衡?
原因是:为了避免数据的冲突,防止造成数据的不一致性。 虽然在两边执行的修改有先后顺序,但由于 Replication 是异步的实现机制,同样可能会导致 晚做的修改被早做的修改所覆盖
在这里插入图片描述

1.3 高可用复合架构

相当于优化前面两个方案

读写分离, 提高吞吐量
高可用, 一旦主库挂了, 就启用备库,主从库都实现了高可用

在这里插入图片描述

2、flask-sqlalchemy实现读写分离

sqlalchemy 并没有像 django-orm 一样内置完善的读写分离方案, 但是提供了可以自定义的接口: 官方文档, 我们可以借此对 flask-sqlalchemy 进行二次开发, 实现读写分离

基本实现思路:

  • 实现自定义的 session类, 继承 SignallingSession类
  • 重写 get_bind方法, 根据读写需求选择对应的数据库地址
  • 实现自定义的 SQLAlchemy类, 继承 SQLAlchemy类
  • 重写 create_session方法, 在内部使用自定义的 Session类

环境准备:搭建好两台数据库,1个主、2个从数据库
主数据库端口 3306
从数据库端口 8306

二次开发代码实现

import random
from flask_sqlalchemy import SQLAlchemy, SignallingSession, get_state
from flask import Flask
from sqlalchemy import orm

# 1.创建app对象
app = Flask(__name__)

# 2.添加数据库配置信息
# app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://账号:密码@ip地址:端口/数据库名称"
# 单库连接信息
# app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://root:mysql@192.168.243.157:3306/test39"
# 多库连接信息
app.config["SQLALCHEMY_BINDS"] = {
    "master": "mysql+pymysql://root:mysql@192.168.243.157:3306/test39",  # 主库
    "slave1": "mysql+pymysql://root:mysql@192.168.243.157:8306/test39",  # 从库1
    "slave2": "mysql+pymysql://root:mysql@192.168.243.157:3306/test39",  # 从库2
}
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_ECHO"] = True


# 3.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)

                # 使用默认的主数据库
                # SQLALCHEMY_DATABASE_URI 返回数据库引擎
                # return SessionBase.get_bind(self, mapper, clause)

        from sqlalchemy.sql.dml import 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("访问从库:{}".format(slave_key))
            # 返回从库的数据库引擎
            return state.db.get_engine(self.app, bind=slave_key)


# 3.2 自定义RoutingSQLAlchemy,继承于SQLAlchemy,重写写create_session,替换底层的SignallingSession
class RoutingSQLAlchemy(SQLAlchemy):

    def create_session(self, options):
        # 使用自定义实现了读写分离的RoutingSession
        return orm.sessionmaker(class_=RoutingSession, db=self, **options)


# 3.3根据RoutingSQLAlchemy创建数据库对象
db = RoutingSQLAlchemy(app)

# db数据库功能: 1.模型类指定数据库查询  2.读写分离  3.定向查询[想查那个数据库只需要将数据库名字传入即可]


# 4.自定义模型类
# 构建模型类
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 hello_world():
    # 写操作 -- 访问主库
    # write_data()
    # 读操作 -- 访问从库
    # read_data()
    # 更新操作 -- 访问主库
    update_data()
    return 'Hello World!'


def write_data():
    user1 = User(name="xiaoming", age=18)
    db.session.add(user1)
    db.session.commit()


def read_data():
    user1 = User.query.filter(User.name == "xiaoming").first()
    print(user1)


def update_data():
    User.query.filter(User.age == 18).update({"name": "curry"})
    db.session.commit()


if __name__ == '__main__':
    # 默认会根据SQLALCHEMY_DATABASE_URI的连接信息创建和删除表
    # TODO:后续可以根据模型类中__bind_key__指定特定数据库创建和删除表
    # 删除表
    # db.drop_all()
    # 创建表
    # db.create_all()
    app.run(debug=True, port=8000)

3、分片

需求分析

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

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

注意

不要轻易分库分表,因为分片会带来 诸多分布式问题, 让应用的复杂度大量增加
应避免"过度设计"和"过早优化", 先尽力去做其他优化,例如:升级硬件、升级网络、读写分离、索引优化、缓存设计等等。
当数据量达到单表瓶颈时候(参考值: 单表记录1000W+/硬盘100G+),再考虑分库分表
如果需要进行分库分表, 优先考虑垂直拆分

3.1 垂直拆分
3.1.1 垂直分表

分析
1、对于字段较多的表, 每条记录占用的空间也会较多, 导致每次从硬盘中读取的记录以及查询缓存可缓存的记录数量较少, 影响查询查询效率
2、针对字段多的表就可以采用垂直分表来进行拆分, 这样可以减少表体积, 提高查询效率

拆分规则:

  • 1、相关性:如: 用户名和密码经常配合使用, 将其分到用户认证表, 生日和邮箱等个人信息经常一起访问, 将其分到用户信息表

  • 2、使用频率:可以将字段根据 常用 和 不常用 进行划分, 并进行分表处理

3.1.2 垂直分库
  • 分析:将一个数据库中的多张表拆分到多个数据库(服务器节点)中
  • 注意点:
    由于 本地事务不支持跨库操作, 所以应该将 有相关联性的表放在同一个库中
    如: 如果后续头条项目垂直分库, 将用户相关的放在数据库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.2 水平拆分
3.2.1 水平分表

分析:
1、将 一张表的记录 拆分到多张表中;
2、对于记录较多的表(比如用户表,用户太多,全都记录在一张表), 会出现索引膨胀, 查询超时等问题, 影响用户体验。
3、水平分表后, 将分表分散放在多个数据库节点中(比如:将用户表分成表1、表2,分别存放在数据库1、2)

4、分布式的问题

一旦在数据库设计中引入了分布式, 则会带来诸多分布式问题, 这里介绍两个主要的问题:

  • 1、分布式事务问题(本地事务不支持跨库操作)
  • 2、跨节点 Join/排序/分页 的问题
4.1 分布式事物问题解决方案

方案1:将有关联的表放在一个数据库中

同库操作可以使用一个事务
如用户表&用户频道表, 文章基本信息表&文章内容表放在一起

方案2:开启二阶段提交协议(简称 2PC协议 / XA协议)

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

在这里插入图片描述

  • sqlalchemy 也支持分布式事务

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

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

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

在这里插入图片描述

4.2 解决跨节点 Join/排序/分页

方案1:分两次查询进行, 在应用端合并

方案2

  • 使用一些第三方方案(数据库中间件)
  • 开源框架除了Mycat, 功能较少
  • 需要一定学习成本, 二次开发需要公司具有一定技术实力
名称公司说明
MySQL RouterMysql官方更新中
Atlas360读写分离、从库负载均衡、自动分表等
Mycat阿里读写分离、故障转移、分布式Join、分布式事务、全局唯一ID、分表等
proxySQLsysown读写分离、故障转移
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值