分布式数据层中间件:实现分库分表+动态数据源+读写分离
分布式数据层中间件
1.简介
分布式数据访问层中间件,旨在为供一个通用数据访问层服务,支持MySQL动态数据源、读写分离、分布式唯一主键生成器、分库分表、动态化配置等功能,并且支持从客户端角度对数据源的各方面(比如连接池、SQL等)进行监控,后续考虑支持NoSQL、Cache等多种数据源。
2.作用
- 动态数据源
- 读写分离
- 分布式唯一主键生成器
- 分库分表
- 连接池及SQL监控
- 动态化配置等
2.如何实现分库分表
既然知道了分库分表的原因,那么业内都是如何实现分库分库的呢?
分库分表的技术方案总体上来讲分为两大类:应用层依赖类中间件、中间层代理类中间件。
- 应用层依赖类中间件
这类分库分表中间件的特点就是和应用强耦合,需要应用显示依赖相应的jar包(以Java为例),比如知名的TDDL、当当开源的sharding-jdbc、蘑菇街的TSharding、携程开源的Ctrip-DAL等。我们以sharding-jdbc为例,其架构图如下所示:
此类中间件的基本思路,就是重新实现JDBC的API,通过重新实现DataSource、PrepareStatement等操作数据库的接口,让应用层在基本不改变业务代码(需业务自行定义路由规则)的情况下透明地实现分库分表的能力。
中间件给上层应用提供熟悉的JDBC API,内部通过一系列的准备工作获取真正可执行的sql,然后底层再按照传统的方法(比如数据库连接池)获取物理连接来执行sql,最后把数据结果合并处理成ResultSet返回给应用层。
优点
无需额外部署,性能损耗低,无中心化
缺点
不支持异构语言,与应用强耦合,SQL支持能力较弱(受应用影响),连接消耗数高
- 中间层代理类中间件
这类分库分表中间件的核心原理是在应用和数据库的连接之间搭起一个代理层,上层应用以标准的MySQL协议来连接代理层,然后代理层负责转发请求到底层的MySQL物理实例,这种方式对应用只有一个要求,就是只要用MySQL协议来通信即可,所以用MySQL Workbench这种纯的客户端都可以直接连接你的分布式数据库,自然也天然支持所有的编程语言。比较有代表性的产品有开创性质的Amoeba、阿里开源的Cobar、Mycat 、当当开源的sharding-proxy,奇虎360开源的Atlas等。我们以sharding-proxy为例,其架构图如下所示:
优点
支持异构语言,与应用完全解耦,SQL支持能力较强,连接消耗数低
缺点
需连接消耗数,性能损耗略高,存在中心化
无论应用层依赖类中间件还是中间层代理类中间件,其核心流程都是相同的,即:SQL解析->SQL路由->SQL重写->SQL执行->结果处理(排序,聚合,合并)。除了分库分表功能之外,分布式数据库中间件还可以集成分布式自增主键,数据库治理,分布式事务等功能。
SQL解析是整个分布式数据库中间件的核心,SQL解析和程序代码解析类似,它按照SQL语法对SQL文本进行解析,识别出文本中各个部分然后以抽象语法树(AST)的形式输出。开源产品使用的SQL解析引擎各不相同。不同的解析引擎偏重的能力有所不同,有的更注重解析SQL的性能,有的更注重支持SQL的能力,还有的更注重扩展性和兼容性。使用比较多的有JSQLParser,Druid,ANTLR等。
常见的数据层中间件
1.TDDL
淘宝根据自己的业务特点开发了TDDL框架,主要解决了分库分表对应用的透明化以及异构数据库之间的数据复制,它是一个基于集中式配置的JDBC datasource实现。
特点
实现动态数据源、读写分离、分库分表。
缺点
分库分表功能还未开源,当前公布文档较少,并且需要依赖diamond(淘宝内部使用的一个管理持久配置的系统)
2.DRDS
阿里分布式关系型数据库服务(Distribute Relational Database Service,简称DRDS)是一种水平拆分、可平滑扩缩容、读写分离的在线分布式数据库服务。
前身为淘宝 TDDL,下一代是 DRDS,整合云服务,收费、Cobar、TDDL整合,商用,首选。
2.Atlas
Atlas是由 Qihoo 360公司Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目。
它在MySQL官方推出的MySQL-Proxy 0.8.2版本的基础上,修改了大量bug,添加了很多功能特性。目前该项目在360公司内部得到了广泛应用,很多MySQL业务已经接入了Atlas平台,每天承载的读写请求数达几十亿条。
主要功能:
1.读写分离
2.从库负载均衡
3.IP过滤
4.自动分表
5.DBA可平滑上下线DB
6.自动摘除宕机的DB
3.MTDDL(Meituan Distributed Data Layer)
美团点评分布式数据访问层中间件
特点
实现动态数据源、读写分离、分库分表,与tddl类似。
下面我以MTDDL为例,也可以参考淘宝tddl,完整详解分布式数据层中间件的架构设计。
分布式数据层中间件架构设计
下图是一次完整的DAO层insert方法调用时序图,简单阐述了MTDDL的整个逻辑架构。
其中包含了:
1.分布式唯一主键的获取
2.动态数据源的路由
3.以及SQL埋点监控等过程:
分布式数据层中间件:具体实现
1.动态数据源及读写分离
在Spring JDBC AbstractRoutingDataSource的基础上扩展出MultipleDataSource动态数据源类,通过动态数据源注解及AOP实现。
2.动态数据源
MultipleDataSource动态数据源类,继承于Spring JDBC AbstractRoutingDataSource抽象类,实现了determineCurrentLookupKey方法,通过setDataSourceKey方法来动态调整dataSourceKey,进而达到动态调整数据源的功能。其类图如下:
3.动态数据源AOP
ShardMultipleDataSourceAspect动态数据源切面类,针对DAO方法进行功能增强,通过扫描DataSource动态数据源注解来获取相应的dataSourceKey,从而指定具体的数据源。具体流程图如下:
4.配置和使用方式举例
/**
* 参考配置
*/
<bean id="multipleDataSource" class="com.sankuai.meituan.waimai.datasource.multi.MultipleDataSource">
/** 数据源配置 */
<property name="targetDataSources">
<map key-type="java.lang.String">
/** 写数据源 */
<entry key="dbProductWrite" value-ref="dbProductWrite"/>
/** 读数据源 */
<entry key="dbProductRead" value-ref="dbProductRead"/>
</map>
</property>
</bean>
/**
* DAO使用动态数据源注解
*/
public interface WmProductSkuDao {
/** 增删改走写数据源 */
@DataSource("dbProductWrite")
public void insert(WmProductSku sku);
/** 查询走读数据源 */
@DataSource("dbProductRead")
public void getById(long sku_id);
}
5.分布式唯一主键生成器
众所周知,分库分表首先要解决的就是分布式唯一主键的问题,业界也有很多相关方案:
序号实现方案优点缺点UUID本地生成,不需要RPC,低延时;
扩展性好,基本没有性能上限无法保证趋势递增;
UUID过长128位,不易存储,往往用字符串表示2Snowflake或MongoDB ObjectId分布式生成,无单点;
趋势递增,生成效率快没有全局时钟的情况下,只能保证趋势递增;
当通过NTP进行时钟同步时可能会出现重复ID;
数据间隙较大3proxy服务+数据库分段获取ID分布式生成,段用完后需要去DB获取,同server有序可能产生数据空洞,即有些ID没有分配就被跳过了,主要原因是在服务重启的时候发生;
无法保证有序,需要未来解决,可能会通过其他接口方案实现
综上,方案3的缺点可以通过一些手段避免,但其他方案的缺点不好处理,所以选择第3种方案:分布式ID生成系统Leaf。
6.分布式ID生成系统Leaf
分布式ID生成系统Leaf,其实是一种基于DB的Ticket服务,通过一张通用的Ticket表来实现分布式ID的持久化,执行update更新语句来获取一批Ticket,这些获取到的Ticket会在内存中进行分配,分配完之后再从DB获取下一批Ticket。
整体架构图如下:
每个业务tag对应一条DB记录,DB MaxID字段记录当前该Tag已分配出去的最大ID值。
IDGenerator服务启动之初向DB申请一个号段,传入号段长度如 genStep = 10000,DB事务置 MaxID = MaxID + genStep,DB设置成功代表号段分配成功。每次IDGenerator号段分配都通过原子加的方式,待分配完毕后重新申请新号段。
7.唯一主键生成算法扩展
MTDDL不仅集成了Leaf算法,还支持唯一主键算法的扩展,通过新增唯一主键生成策略类实现IDGenStrategy接口即可。IDGenStrategy接口包含两个方法:getIDGenType用来指定唯一主键生成策略,getId用来实现具体的唯一主键生成算法。其类图如下:
8.分库分表
在动态数据源AOP的基础上扩展出分库分表AOP,通过分库分表ShardHandle类实现分库分表数据源路由及分表计算。ShardHandle关联了分库分表上下文ShardContext类,而ShardContext封装了所有的分库分表算法。其类图如下:
分库分表流程图如下:
9.分库分表取模算法
分库分表目前默认使用的是取模算法,分表算法为 (#shard_key % (group_shard_num * table_shard_num)),分库算法为 (#shard_key % (group_shard_num * table_shard_num)) / table_shard_num,其中group_shard_num为分库个数,table_shard_num为每个库的分表个数。
例如把一张大表分成100张小表然后散到2个库,则0-49落在第一个库、50-99落在第二个库。核心实现如下:
public class ModStrategyHandle implements ShardStrategy {
@Override
public String getShardType() {
return "mod";
}
@Override
public DataTableName handle(String tableName, String dataSourceKey, int tableShardNum,
int dbShardNum, Object shardValue) {
/** 计算散到表的值 */
long shard_value = Long.valueOf(shardValue.toString());
long tablePosition = shard_value % tableShardNum;
long dbPosition = tablePosition / (tableShardNum / dbShardNum);
String finalTableName = new StringBuilder().append(tableName).append("_").append(tablePosition).toString();
String finalDataSourceKey = new StringBuilder().append(dataSourceKey).append(dbPosition).toString();
return new DataTableName(finalTableName, finalDataSourceKey);
}
}
10.分库分表算法扩展
MTDDL不仅支持分库分表取模算法,还支持分库分表算法的扩展,通过新增分库分表策略类实现ShardStrategy接口即可。ShardStrategy接口包含两个方法:getShardType用来指定分库分表策略,handle用来实现具体的数据源及分表计算逻辑。其类图如下:
11.全注解方式接入
为了尽可能地方便业务方接入,MTDDL采用全注解方式使用分库分表功能,通过ShardInfo、ShardOn、IDGen三个注解实现。
ShardInfo注解用来指定具体的分库分表配置:包括分表名前缀tableName、分表数量tableShardNum、分库数量dbShardNum、分库分表策略shardType、唯一键生成策略idGenType、唯一键业务方标识idGenKey;ShardOn注解用来指定分库分表字段;IDGen注解用来指定唯一键字段。具体类图如下:
12.配置和使用方式举例
// 动态数据源
@DataSource("dbProductSku")
// tableName:分表名前缀,tableShardNum:分表数量,dbShardNum:分库数量,shardType:分库分表策略,idGenType:唯一键生成策略,idGenKey:唯一键业务方标识
@ShardInfo(tableName="wm_food", tableShardNum=100, dbShardNum=1, shardType="mod", idGenType=IDGenType.LEAF, idGenKey=LeafKey.SKU)
@Component
public interface WmProductSkuShardDao {
// @ShardOn("wm_poi_id") 将该注解修饰的对象的wm_poi_id字段作为shardValue
// @IDGen("id") 指定要设置唯一键的字段
public void insert(@ShardOn("wm_poi_id") @IDGen("id") WmProductSku sku);
// @ShardOn 将该注解修饰的参数作为shardValue
public List<WmProductSku> getSkusByWmPoiId(@ShardOn long wm_poi_id);
}
连接池及SQL监控
DB连接池使用不合理容易引发很多问题,如连接池最大连接数设置过小导致线程获取不到连接、获取连接等待时间设置过大导致很多线程挂起、空闲连接回收器运行周期过长导致空闲连接回收不及时等等,如果缺乏有效准确的监控,会造成无法快速定位问题以及追溯历史。
连接池监控
实现方案
结合Spring完美适配c3p0、dbcp1、dbcp2、mtthrift等多种方案,自动发现新加入到Spring容器中的数据源进行监控,通过美团点评统一监控组件JMonitor上报监控数据。整体架构图如下:
连接数量监控
监控连接池active、idle、total连接数量,Counter格式:(连接池类型.数据源.active/idle/total_connection),效果图如下:
获取连接时间监控
监控获取空闲连接时间,Counter格式:(ds.getConnection.数据源.time),效果图如下:
以上!