Zebra是一个基于JDBC API协议上开发出的高可用、高性能的数据库访问层解决方案,简化了读写分离、分库分表的开发工作,使得业务方在分库分库、读写分离的情况下,依然可以像操作单个库那样去操作,屏蔽底层实现的复杂性,对业务透明。

读写分离

在单台mysql实例的情况下,所有的读写操作都集中在这一个实例上。当读压力太大,单台mysql实例扛不住时,此时DBA一般会将数据库配置成集群,一个master(主库),多个slave(从库),master将数据通过binlog的方式同步给slave,可以将slave节点的数据理解为master节点数据的全量备份

  1. 对sql类型进行判断。如果是select等读请求,就走从库,如果是insert、update、delete等写请求,就走主库。
  2. 主从数据同步延迟问题。因为数据是从master节点通过网络同步给多个slave节点,因此必然存在延迟。因此有可能出现我们在master节点中已经插入了数据,但是从slave节点却读取不到的问题。对于一些强一致性的业务场景,要求插入后必须能读取到,因此对于这种情况,我们需要提供一种方式,让读请求也可以走主库,而主库上的数据必然是最新的。
  3. 事务问题。如果一个事务中同时包含了读请求(如select)和写请求(如insert),如果读请求走从库,写请求走主库,由于跨了多个库,那么jdbc本地事务已经无法控制,属于分布式事务的范畴。而分布式事务非常复杂且效率较低。因此对于读写分离,目前主流的做法是,事务中的所有sql统一都走主库,由于只涉及到一个库,jdbc本地事务就可以搞定。
  4. 高可用问题。主要包括:
  • 新增slave节点:如果新增slave节点,应用应该感知到,可以将读请求转发到新的slave节点上。
  • slave宕机或下线:如果其中某个slave节点挂了/或者下线了,应该对其进行隔离,那么之后的读请求,应用将其转发到正常工作的slave节点上。
  • master宕机:需要进行主从切换,将其中某个slave提升为master,应用之后将写操作转到新的master节点上。

分库分表

读写分离,主要是为了数据库读能力的水平扩展。而分库分表则是为了写能力的水平扩展。

  • sql解析:首先对sql进行解析,得到需要插入的四条记录的id字段的值分别为1,2,3,4

  • sql路由:sql路由包括库路由和表路由。库路由用于确定这条记录应该插入哪个库,表路由用于确定这条记录应该插入哪个表。

  • sql改写:上述批量插入的语法将会在 每个库中都插入四条记录,明显是不合适的,因此需要对sql进行改写,每个库只插入一条记录。

  • sql执行:一条sql经过改写后变成了多条sql,为了提升效率应该并发的到不同的库上去执行,而不是按照顺序逐一执行

  • 结果集合并:每个sql执行之后,都会有一个执行结果,我们需要对分库分表的结果集进行合并,从而得到一个完整的结果。

    区别

服务端代理(proxy:代理数据库)

​ 我们独立部署一个代理服务,这个代理服务背后管理多个数据库实例。而在应用中,我们通过一个普通的数据源(c3p0、druid、dbcp等)与代理服务器建立连接,所有的sql操作语句都是发送给这个代理,由这个代理去操作底层数据库,得到结果并返回给应用。在这种方案下,分库分表和读写分离的逻辑对开发人员是完全透明的。

不论你用的php、java或是其他语言,都可以支持。原因在于数据库代理本身就实现了mysql的通信协议,你可以就将其看成一个mysql 服务器。mysql官方团队为不同语言提供了不同的客户端驱动,如java语言的mysql-connector-java,python语言的mysql-connector-python等等。因此不同语言的开发者都可以使用mysql官方提供的对应的驱动来与这个代理服务器建通信

客户端代理(datasource:代理数据源)

​ 应用程序需要使用一个特定的数据源,其作用是代理,内部管理了多个普通的数据源(c3p0、druid、dbcp等),每个普通数据源各自与不同的库建立连接。应用程序产生的sql交给数据源代理进行处理,数据源内部对sql进行必要的操作,如sql改写等,然后交给各个普通的数据源去执行,将得到的结果进行合并,返回给应用。数据源代理通常也实现了JDBC规范定义的API,因此能够直接与orm框架整合。在这种方案下,用户的代码需要修改,使用这个代理的数据源,而不是直接使用c3p0、druid、dbcp这样的连接池。

更加轻量,可以与任何orm框架整合

实践

数据表拆分的首要原则,就是要尽可能找到数据表中的数据在业务逻辑上的主体,并确定大部分(或核心的)数据库操作都是围绕这个主体的数据进行,然后可使用该主体对应的字段作为分表键,进行分库分表。

业务逻辑上的主体,通常与业务的应用场景相关,下面的一些典型应用场景都有明确的业务逻辑主体,可用于分表键:

  • 面向用户的互联网应用,都是围绕用户维度来做各种操作,那么业务逻辑主体就是用户,可使用用户对应的字段作为分表键;
  • 侧重于卖家的电商应用,都是围绕卖家维度来进行各种操作,那么业务逻辑主体就是卖家,可使用卖家对应的字段作为分表键;

以此类推,其它类型的应用场景,大多也能找到合适的业务逻辑主体作为分表键的选择。

如果确实找不到合适的业务逻辑主体作为分表键,那么可以考虑下面的方法来选择分表键:

  • 根据数据分布和访问的均衡度来考虑分表键,尽量将数据表中的数据相对均匀地分布在不同的物理分库/分表中,适用于大量分析型查询的应用场景(查询并发度大部分能维持为1);
  • 按照数字(字符串)类型与时间类型字段相结合作为分表键,进行分库和分表,适用于日志检索类的应用场景。
分表方式 解释 优点 缺点 试用场景 版本要求
Hash 拿分表键的值Hash取模进行路由。最常用的分表方式。 数据量散列均衡,每个表的数据量大致相同。请求压力散列均衡,不存在访问热点 一旦现有的表数据量需要再次扩容时,需要涉及到数据移动,比较麻烦。所以一般建议是一次性分够。 在线服务。一般均以UserID或者ShopID等进行hash。 任意版本
Range 拿分表键按照ID范围进行路由,比如id在1-10000的在第一个表中,10001-20000的在第二个表中,依次类推。这种情况下,分表键只能是数值类型。 数据量可控,可以均衡,也可以不均衡扩容比较方便,因为如果ID范围不够了,只需要调整规则,然后建好新表即可。 无法解决热点问题,如果某一段数据访问QPS特别高,就会落到单表上进行操作。 离线服务。 2.9.4以上
时间 拿分表键按照时间范围进行路由,比如时间在1月的在第一个表中,在2月的在第二个表中,依次类推。这种情况下,分表键只能是时间类型。 扩容比较方便,因为如果时间范围不够了,只需要调整规则,然后建好新表即可。 数据量不可控,有可能单表数据量特别大,有可能单表数据量特别小无法解决热点问题,如果某一段数据访问QPS特别高,就会落到单表上进行操作。 离线服务。比如线下运营使用的表、日志表等等 2.9.4以上