数据库中间件/分库分表相关知识点整理
来源
来自对大佬们文章的总结,参考的文章都列在最后啦
1 数据库拆分
垂直拆分 -> 读写分离 -> 分库分表(水平拆分)
1.1 垂直拆分
- 多张表的数据库 -> 多个小的数据库
- 大表 -> 小表(实际开发比较少用)
- 服务化(SOA Service-Oriented Architecture)改造
1.2 读写分离
- 一个 Master 节点(主库)对应多个 Salve 节点(从库)
补充知识点:集群、mysql主从切换、主从复制、binlog
1.2.1 优点
- 减轻单个库访问压力
1.2.1 挑战
- DBA(Database Administrator):
多了很多集群运维工作 - 开发人员:
- 基本读写分离功能:读请求 写请求
- 主从数据同步延迟问题:强一致性场景,写/读都走主库
- 事务问题:跨多个库事务,写/读都走主库
- 感知集群信息变更:主从切换、增加新节点
1.3 分库分表
垂直分库 + Master/Salve 模式 -> 高并发访问操作;但是业务表中的数据还是会很大 -> 维护和性能
- 只分表
- 只分库
- 分库分表
1.3.1 优点
- 存储能力的水平扩展:多个mysql主从复制集群
- 写能力的水平扩展:每个分片都有一个master写入、索引开销等
1.3.2 挑战
希望像操作单库单表一样操作分库分表
-
基本的数据库增删改功能
希望像操作单库单表一样操作分库分表- sql解析
- sql路由:库路由和表路由
- sql改写
- sql执行:并发带不同的库
- 结果集合并
-
分布式id
单库的自增主键id会冲突,需要一个全局的id生成器 -
分布式事务
- XA事务
- 柔性事务
-
动态扩容
增加分库分表的数量
2 主流数据库中间件设计方案
单库单表,通过连接池与数据库建立连接,进行读写操作
读写分离和分库分表,应用都要操作多个数据库实例,需要使用数据库中间件
2.1 设计方案
典型:proxy、smart-client
2.1.1 proxy模式
2.1.1.1 优点
- 多语言支持:以mysql为例,如果proxy本身实现了mysql的通信协议,可以就将其看成一个mysql服务器
- 对业务开发同学透明:可以把proxy当成mysql服务器
2.1.1.2 缺点
- 实现复杂:需要实现被代理的数据库server端的通信协议,也许只能代理某一种数据库,如mysql
- proxy本身需要保证高可用:不能挂
- 租户隔离:多个应用都访问proxy代理的底层数据库时
补充知识点:怎么隔离
2.1.2 smart-client模式
通常smart-client是在连接池或者driver的基础上进行了一层封装,smart-client内部与不同的库建立连接,sql交给smart-client
- 读写分离 -> 选择走从库还是主库
- 分库分表 -> 进行sql解析、sql改写等操作,然后路由到不同的分库,将结果合并,返回给应用
2.1.2.1 优点
- 实现简单:proxy需要实现数据库的服务端协议,而smart-client不需要实现客户端通信协议,有厂商提供的Driver
- 天然去中心化:以sdk方式,被应用引用,部署到不同节点,且直连数据库;相对proxy高可用
2.1.2.2 缺点
- 通常仅支持某一种语言:例如tddl需使用java语言开发
- 版本升级困难:多个应用都依赖某版本jar包,有bug都要升级;而proxy只要升级代理服务器
2.2 业界产品
各有优缺点
proxy实现
目前的实现方案有:
- 阿里巴巴开源的cobar
- 阿里云上的drds
- mycat团队在cobar基础上开发的mycat
- mysql官方提供的mysql-proxy
- 奇虎360在mysql-proxy基础开发的atlas
- 当当网开源的sharing-sphere
- 等等
smart-client实现
目前的实现方案有:
- 阿里巴巴开源的tddl
- 大众点评开源的zebra
- 当当网开源的sharding-jdbc
- 蚂蚁金服的zal
- 等等
3 读写分离核心要点
3.1 基本路由功能
3.1.1 sql类型判断
- write语句
- query语句
3.1.2 强制走主库
具体实现上有2种方案:hint 或API
- hint:比如
/*master*/select * from table_xx
- Api:数据库中间件决定
3.2 从库路由策略
一些简单的选择策略包括:
- 随机选择(random)
- 按照权重进行选择(weight)
- 或者轮循(round-robin)
- 等等
- 就近路由:跨IDC(Internet Data Center)部署的数据库集群
3.3 HA、Scalable相关
- HA(High Available):主库宕机,需要重新选主
- Scalable:读qps太高,增加从库,分担流量
3.3.1 配置中心
事实上:主从变更,增加从库 -> 配置信息变更 -> 监听配置中心
配置中心的选择:
- 阿里的diamond
- 百度的disconf
- 点评开源的lion
- 携程开源的apollo
- 等等
3.3.1.1 问题
- 延迟:监控服务监控到集群信息变更 -> 配置中心 -> 数据库中间件,必然存在一些延迟
- 主从切换
- 从库切换,可以自动重试
- 大量的配置信息需要推送
3.3.2 轻量级的HA保障
- 主动隔离,异步检测
3.3.3 限流和降级
- 烂sql拦截
4 分库分表
核心要点:希望像操作单个数据库实例那样编写sql,数据库中间件帮忙屏蔽所有底层的复杂逻辑
示例场景:批量插入
4.1 SQL解析
目前较为流行的sql解析器包括:
- FoundationDB SQL Parser
- Jsqlparser
- Druid SQL Parser:解析性能最好,支持数据库方言最多
4.2 SQL路由
分库分表的字段称为路由字段,或者分区字段。
注意点:SQL中应该包含这个路由字段- INSERT- SELECT- UPDATE、DELETE。
路由规则有:
- 库规则:用于确定到哪一个分库
- 表规则:用于确定到哪一个分表
4.3 SQL改写
很复杂,支持简单的OLTP场景,迈向OLAP
补充知识点:OLTP(On-Line Transaction Processing,联机事务处理),OLAP(On-Line Analytical Processing,联机分析处理),HTAP
4.4 SQL执行
拆出来的多个SQL,并发执行
4.5 结果集合并
权衡实现复杂度、执行效率,case by case分析
-
对于查询条件:
分区字段 =、IN
普通字段 NOT IN、BETWEEN…AND、LIKE、NOT LIKE等 -
聚合函数:
大部分支持MAX、MIN、COUNT、SUM
部分支持AVG
部分支持函数嵌套、GROUP BY -
子查询:
支持非常有限
分为FROM部分的子查询和WHERE部分的子查询
语法上兼容,但是无法识别子查询中的分区字段,或者要求子查询的表名必须与外部查询表名相同,又或者只能支持一级嵌套子查询 -
JOIN:
很复杂
不可能把两个表的所有分表,全部拿到内存中来进行JOIN
取巧的办法,一个是Binding Table,另外一个是小表广播 -
分页排序:
分页的效率较低
ORDER BY和LIMIT -
关于JOIN的特属说明:
-
Binding Table:
强关联的表的路由规则设置为完全一样,在同一个分库中join -
小表广播:
每个分库内都实时同步一份完整的数据,在同一个分库中join
补充知识点:同步组件,canal、puma
-
4.6 二级索引(辅维度同步)
user_id分库分表,同步到另一个集群,按phone_id分库分表
4.7 分布式id生成器
- 基于zk
- 基于mysql
- 基于缓存
- 基于算法
- twitter的snowflake算法
- 美团开源的分布式ID生成系统Leaf
- 百度开源的分布式唯一ID生成器UidGenerator
- 等等
4.8 分布式事务
4.8.1 事务简介
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)
- 四个属性:ACID
- 原子性(atomicity)
- 一致性(consistency)
- 隔离性(isolation)
- 持久性(durability)/永久性(permanence)
4.8.2 本地事务
只需要操作单一的数据库,ACID特性由数据库支持
补充:spring @Transitional
使用注意
4.8.3 分布式事务典型场景
- 跨库事务
- 分库分表
- 服务化(SOA)
4.8.4 X/Open DTP模型与XA规范
4.8.4.1 DTP模型(分布式事务处理 Distributed Transaction Processing)
5个基本元素:
- 应用程序(Application Program ,简称AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作。
- 资源管理器(Resource Manager,简称RM):如数据库、文件系统等,并提供访问资源的方式。
- 事务管理器(Transaction Manager ,简称TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。
- 通信资源管理器(Communication Resource Manager,简称CRM):控制一个TM域(TM domain)内或者跨TM域的分布式应用之间的通信。
- 通信协议(Communication Protocol,简称CP):提供CRM提供的分布式应用节点之间的底层通信服务。CRM底层采用OSI TP(Open Systems Interconnection — Distributed Transaction Processing)通信服务
4.8.4.2 XA规范
XA是DTP模型定义TM和RM之间通讯的接口规范。
4.8.4.2.1 两阶段提交协议(2PC)
4.8.4.2.1.1 XA规范对2PC的2点优化
- Read-only
- One-phase Commit
4.8.4.2.1.2 存在的问题
- 同步阻塞问题
- 单点故障:协调者TM发生故障
- 数据不一致
4.8.4.2.2 三阶段提交协议(Three-phase commit)
4.8.4.2.2.1 3PC针对2PC改动点
- 引入超时机制
- 在第一阶段和第二阶段中插入一个准备阶段
4.8.4.2.2.2 3PC和2PC区别
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞
都无法彻底解决分布式的一致性问题
4.8.5 BASE理论与柔性事务
4.8.5.1 经典的分布式系统理论-CAP
- 一致性
强 / 最终 / 弱 - 可用性
- 分区容错性
4.8.5.2 BASE理论
BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)
- 基本可用(Basically Available)
- 软状态( Soft State)
- 最终一致( Eventual Consistency)
4.8.5.3 典型的柔性事务方案
- 最大努力通知(非可靠消息、定期校对)
- 可靠消息最终一致性(异步确保型)
- TCC(两阶段型、补偿型)(Try-Confirm-Cancel)
4.8.5.3.1 最大努力通知
- 不可靠消息
- 定期校对
4.8.5.3.2 TCC两阶段补偿型(maybe目前最火)
4.8.5.3.2.1 TCC两阶段提交 VS XA两阶段提交
- XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
- TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
4.8.5.3.2.2 补偿性事务(Compensation-Based Transactions)
补偿是一个独立的支持ACID特性的本地事务,用于在逻辑上取消服务提供者上一个ACID事务造成的影响,对于一个长事务(long-running transaction),与其实现一个巨大的分布式ACID事务,不如使用基于补偿性的方案,把每一次服务调用当做一个较短的本地ACID事务来处理,执行完就立即提交
4.8.5.3.2.3 TCC事务模型 VS DTP事务模型
4.8.5.3.2.4 TCC事务的优缺点
- 优点
解决提交占用资源锁时间过长导致的性能问题 - 缺点
开发成本,主业务从业务都要改造
4.8.5.3.3 可靠消息最终一致性
两种方案
- 基于MQ的事务消息
- 并不是所有的mq都支持事务消息。也就是消息一旦发送到消息队列中,消费者立马就可以消费到。此时可以使用独立消息服务、或者本地事务表。
4.9 分库分表常用方案
分库分表方案中有常用的方案,hash取模和range范围方案
4.9.1 hash取模
- 优点:数据均匀,不会有热点
- 缺点:数据迁移和扩容很难
4.9.2 range范围方案
- 优点:利于将来的扩容
- 缺点:有热点问题
4.9.3 俩者结合
4.9.4 分库分表能无限扩容么
- 不能,数据库连接过多
- 解决办法:不让应用连接所有的数据库 -> 异地多活
4.10 什么时候分库分表?数据的量级?
5. 异地多活
附
tddl 配置
- tddl-rule.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="vtabroot" class="com.taobao.tddl.interact.rule.VirtualTableRoot" init-method="init">
<property name="dbType" value="MYSQL" />
<property name="defaultDbIndex" value="XXX_0000_GROUP" />
<property name="tableRules">
<map>
<entry key="xxx_user" value-ref="xxx_user" />
<entry key="xxx_dept" value-ref="xxx_dept" />
</map>
</property>
</bean>
<bean id="xxx_user" class="com.taobao.tddl.interact.rule.TableRule">
<property name="dbNamePattern" value="XXX_{0000}_GROUP" />
<property name="tbNamePattern" value="xxx_user_{0000}" />
<property name="dbRuleArray" value="(#corp_id,1,1024#.hashCode().abs().longValue() % 1024).intdiv(64)" />
<property name="tbRuleArray" value="(#corp_id,1,1024#.hashCode().abs().longValue() % 1024)" />
<property name="allowFullTableScan" value="false" />
</bean>
<bean id="xxx_dept" class="com.taobao.tddl.interact.rule.TableRule">
<property name="dbNamePattern" value="XXX_{0000}_GROUP" />
<property name="tbNamePattern" value="xxx_dept_{0000}" />
<property name="dbRuleArray" value="(#corp_id,1,1024#.hashCode().abs().longValue() % 1024).intdiv(64)" />
<property name="tbRuleArray" value="(#corp_id,1,1024#.hashCode().abs().longValue() % 1024)" />
<property name="allowFullTableScan" value="false" />
</bean>
</beans>
- datasource.xml
<bean id="dataSource" class="com.taobao.tddl.client.jdbc.TDataSource" init-method="init">
<!--<property name="appName" value="XXX_APP"/>-->
<property name="appName" value="XXX_APP"/>
<property name="appRuleFile" value="/xxx/tddl-rule.xml"/>
<property name="dynamicRule" value="true"/>
</bean>