目录
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 Router | Mysql官方 | 更新中 |
Atlas | 360 | 读写分离、从库负载均衡、自动分表等 |
Mycat | 阿里 | 读写分离、故障转移、分布式Join、分布式事务、全局唯一ID、分表等 |
proxySQL | sysown | 读写分离、故障转移 |