数据层访问层的设计与实现
上一节引出了分布式数据访问层的各种问题,这里讲讲数据访问层的设计与实现。
1. 对外提供数据访问层的方式
数据访问层有多种提供能力的方式:
- 专用 API ,不推荐,通用性差;
- 通用方式,使用 JDBC 的方式,数据层作为一个 JDBC 的实现,暴露 JDBC 接口给应用,成本低;
- ORM 类别的接口,介入上两种之间,一般都会使用类 ORM 框架,如 iBatis、hibernate、Spring JDBC 等,推荐。
上面三种分别画图即如下图所示。
可以发现使用 JDBC 的兼容性和扩展性是最好的,但是实现成本高,封装一个 ORM 框架后会具备通用性。
JDBC 的方式有时候会优于 ORM 的方案,比如分页排序的时候,ORM 需要在内存里进行操作,JDBC 就不会需要那么多内存,在访问的数据的时候能规避不需要的数据查询;此外,ORM 动态修改 SQL 会比较困难,JDBC 的方式则没有那么困难。
2. 数据层流程
我们知道一般来说访问数据库有以下几个操作:
- SQL 解析;
- 规则处理;
- SQL 改写;
- 数据源选择;
- SQL 执行;
- 结果集返回并处理。
2.1 SQL 解析阶段
此时主要考虑两个问题:一个是对 SQL 支持的程度,另外一个则是支持多少 SQL 的方言。
对于解析的缓存可以提升解析速度,当然需要注意缓存的容量限制,一般系统中执行的 SQL 数量相对是可控的,不过为了安全,解析的缓存需要加上数量上限。
解析可以得到 SQL 的关键信息,比如表名、字段等等,下一步则是规则处理。
2.2 规则处理阶段
2.2.1 固定哈希
首当其冲的就是「固定哈希」方式,比如对某个字段进行取模,然后分散到不同的数据库和表中,除了根据 id 取模,还有根据时间,如天、星期、月、年来进行存储数据,这一般用于数据产生后相关日期不进行修改的情况。
我们可以想象通过取模到不同的库,那么如果分库后还需要分表呢?如下图所示。
首先我们对2取模到两个库,然后对4取模到不同库的不同表中。当然也可以直接对4取模,然后判断是否大于2进行分库,分表是相同的。
2.2.2 一致性哈希
Consistent Hashing ,是1997年提出的,最大变化是把节点对应的哈希值变为了一个范围,而不再是离散的,如果有新节点加入,则接管一部分范围的哈希;如果有节点退出,原来管理的哈希被下一个节点管理。这很好想象,即一个环形结构。
新增和删除一个节点都只会对一个节点造成影响。
2.2.3 虚拟节点对一致性哈希的改进
为了解决数据不均衡的问题,引入了虚拟节点的概念,即原先的四个物理节点可以变为很多的虚拟节点,每个节点支持一段哈希,相应有很多虚拟节点,均衡对应到整个哈希环上,如果一个物理节点失效,对应就是很多虚拟节点失效。
2.2.4 映射表与规则自定义计算方式
映射表是分库分表字段的值的查表方法来确定数据源的方法。一般用于热点数据的特殊处理,或者在一些场景下对不完全符合规律的规则进行的补充。
而规则自定义计算方式是最灵活的一种方式。它已经不算是以配置的方式来做规则,而是通过比较复杂的计算来解决数据访问的问题。
比如 id 对4取模进行分库,我们对一些热点 id 需要独立到另外的库,则很简答的,有如下代码:
1
2
3
4 if (id in hotset) {
return 4
}
return id % 4;
2.3 改写 SQL 阶段
一般标准是分库后尽量避免跨库查询。
比如现在有一个商品表,里面有商家 id 和商品 id ,我们可以选择对这两种 id 作为依据进行分库,这里就要进行考量,通过哪个指标进行分库。
进行改写 SQL 的一个原因是,如果分库后,有多个同名表,就需要进行后缀的区分;另外相对的索引名也可能需要区分;除了名字以外,对于取平均这样的操作,就需要先 Count 再 Avg ,就必须要修改 SQL 来达到了。
2.4 数据源选择阶段
现在我们不再是一张表,可能是分库分表的结果,直观看就是一个矩阵的表,我们不仅要选择库,还要选择表,需要根据当前执行 SQL 的特点,计算得到这次 SQL 请求要访问的数据库。
2.5 执行与结果处理阶段
没啥好说的,重点在于异常的处理与判断。
这里是三层数据源整体视图:
3. 独立部署的数据访问层实现方式
从数据层的物理不说来说可以分为 jar 部署与 Proxy 的方式。
如果是 Proxy 的方式,客户端和 Proxy 之间的协议有两种选择:数据库协议与私有协议。
- 数据库协议,则应用把 Proxy 当做一个数据库,使用 JDBC 的实现来连接 Proxy ,可以减少一次从数据库协议到对象然后再从对象到数据库协议的转换,而 Proxy 需要完全实现一套相关数据库的协议,成本比较高,应用到 Proxy 没法做连接复用;
- 私有协议,应用需要一个独立的数据层客户端,Proxy 的实现相对简单,而且应用到 Proxy 的连接可复用。
下图可以清晰看到应用接入有 MySQL 与自身协议两种方式,连接数据库时,可以使用具体的适配器访问,也可以使用 JDBC 驱动访问,更加灵活,直接在协议层来控制数据,能够实现少一次转换就完成调用的工作。
4. 读写分离的挑战与应对
读写分离可以分担主库的读压力。
4.1 主库从库非对称场景
4.1.1 数据结构相同,一主多从
在 MySQL 中可以使用 Replication 解决复制的问题,而且延迟相对较小。
一主多从,主来写,从和主一起提供读,此时,我们可以如下方式进行数据复制。
数据同步服务器和 DB 主库的交互根据被修改、增加的数据主键来获取内容,采用行复制。
而通过行复制的方式是一个不优雅但能解决问题的方式,比较优雅的是基于数据库的日志来进行数据复制。
4.1.2 主备分库方式不同的数据复制
章节名中的「非对称」指的是源数据与目标数据不是镜像关系,也就是说数据库与目标数据库实现方式不同。
比如现在分库条件不同,我们需要进行数据库的复制怎么办?比如订单系统,主库是买家 id 进行分库,备库则可以进行卖家 id 进行分库,这就需要进行非对称的复制。
4.1.3 引入数据变更平台
我们除了数据变更到其他数据库这一种场景,还有搜索引擎的索引构建、缓存的失效等。我们可以构建一个通用的平台来管理和控制数据的变更。
我们引入 Extractor 和 Applier ,其中 Extractor 负责把数据源变更的信息加入到数据分发平台中,而 Applier 的作用是把这些变更应用到相应的目标上,中间的数据分发平台则是由多个管道组成的。不同的数据来源需要不同的 Extractor 来进行解析和变更,不同的目标需要用不同的 Applier 把数据落库到目标数据库上。
4.2 数据平滑迁移
数据迁移最大的挑战是在迁移过程中,如果有数据变化怎么办?我们可以在迁移时进行增量日志,迁移结束把变化进行处理。
而复制,记录增量日志,这两步是一个逐渐收敛的过程,因此需要进行数据比对,如果新库数据与源库数据不同,则记录下来,然后停止迁移的写,进行增量日志处理,使得新库数据是新的。