读书笔记: 大型网站系统与Java中间件实践(5)

5 数据访问层

分布式系统中关系型数据库的扩展方式一般是分库分表和读写分离。分库分表会带来一系列问题,如事务处理、分布式sequence、跨表跨库查询等。本章对这几个问题进行了阐述,并从不同角度介绍了数据访问层的设计。最后对读写分离带来的挑战和应对进行了简要描述。

5.1 分布式事务

事务的支持对业务来说是一个非常重要的特性,数据库软件对单库的ACID事务特性的支持是比较到位的。但是分库之后,同一个事务的处理可能涉及多个库节点,此时事务就变成分布式事务了。

5.1.1 分布式理论CAP

在说分布式事务之前,先介绍一个分布式系统理论。

在分布式系统中有一个比较出名的理论叫CAP/BASE理论。
CAP是三个英文单词的缩写,分别是一致性、可用性和分区容错性。一致性即所有节点在同一时间读到同样的数据。可用性意思是无论是成功还是失败,每个请求都能够收到一个反馈,强调系统的响应。分区容错性强调系统的一部分出现问题时系统仍然能够继续工作。

分布式系统不能同时满足这三个特性,假如这三个特性总面积用一个大小固定的圆来表示,任何情况下都只能选择其中两个来提升,而另外一个则会受到损失。因此在进行系统设计和权衡时,其实就是在选择CA、AP或CP。

事实上,在分布式系统中一般侧重可用性和分区容错性,即优先满足AP。在满足AP的基础上,再来考虑解决一致性的问题。Base理论描述如下:Basically Available,基本可用,允许分区失败。Soft state,软状态,接受一段时间的状态不同步。Eventually consistent,最终一致,保证最终数据的状态是一致的。

在大型分布式系统中,为了更好地保证可用性和扩展性,一般不要求强一致性,而是采用最终一致的策略来实现。

5.1.2 分布式事务解决方案

XA规范
X/Open组织(现在的Open Group)定义了分布式事务处理模型-DTP模型(Distributed Transaction Processing Reference Model)。DTP包括应用程序AP,事务管理器TM、资源管理器RM和通信管理器CRM。

常见的事务管理器TM是交易中间件,常见的资源管理器RM是数据库,常见的通信管理器CRM是消息中间件。

通常,把一个数据库内部的事务处理,如对多个表的操作,作为本地事务看待。数据库的事务处理对象是本地事务,而分布式事务处理对象是全局事务。所谓全局事务,是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即是一个全局事务。例如,一个事务中可能更新几个不同的数据库,对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖于与全局事务相关的其他数据库的操作是否成功。此时就需要一个交易中间件TM,由它通知和协调相关数据库的提交或回滚。

XA就是X/Open DTP定义的交易中间件(事务管理器)与数据库之间的接口规范(即接口函数),由数据库厂商来实现。数据库事务的开始、结束以及提交、回滚都是通过XA接口规范来完成的。

分布式事务解决方案中的两阶段提交和三阶段提交就是根据以上思想(要么全部提交,要么全部回滚)提出的。可以说二阶段提交其实就是实现XA分布式事务的关键。

两阶段提交
两阶段提交协议中,事务协调者在请求阶段通知事务参与者提交或取消事务,然后获取事务参与者的决策结果。接着进入提交阶段,事务协调者基于第一阶段的投票结果,来决定是提交还是取消事务。当且仅当所有参与者同意提交事务时,协调者才通知所有参与者进行事务提交,否则通知所有参与者取消事务。

两阶段提交存在以下问题。

  • 同步阻塞
    在执行过程中,所有事务参与者节点都是事务阻塞型的。在请求阶段,事务协调者需要等待所有事务参与者返回结果,任何一个节点的阻塞都会导致整个过程的阻塞。
  • 单点故障
    一旦协调者发生故障,参与者会一直阻塞下去。
  • 数据不一致
    在第二阶段,即提交阶段,如果发生局部网络异常或其他故障导致部分参与者没有收到确认提交通知,将会导致一部分参与者提交了事务,而另外一部分没有提交事务,最终表现为数据不一致。

三阶段提交
为了解决两阶段提交协议的弊端,业界引入了三阶段提交。三阶段提交协议在协调者和参与者中引入了超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步,先询问再锁资源。
三阶段提交只是在二阶段提交基础上做了一些改进,但本质是没有变的,都是先做准备,再做真正的提交。

另外还有一种比两阶段提交更轻量一些的保证一致性的协议——Paxos协议,Zookeeper采用的就是Paxos协议的改进。

其他还有本地消息表和MQ事务消息也可以用来解决分布式事务问题(见参考资料1)。

柔性事务
所谓柔性事务,就是根据BASE理论提出的,允许出现一定时间的中间状态,最终达到一致性状态即可。柔性事务可通过异步确保,补偿,最大努力通知几种方式来实现。

5.2 分布式系统唯一id

在单表中,通常用自增id作为主键,由数据库自身保证id的线性增长。但在分表之后,主键id就不能这样生成了,否则会造成不同数据表主键重复。

Id通常具有唯一性和连续性两种性质。如果只考虑唯一性的话,那么可以参考uuid的生成方式,或者根据业务情况使用各个种子(不同维度的标识,如时间,mac,ip,本机计数等)来生成唯一id。这种id虽然可以保证唯一性,但是在整个分布式系统中的连续性不好。

还可以用一个独立的系统来对id进行管理,每个数据库表使用id时都从这个id生成器上取。但存在几个问题:性能不好,因为每次都要从远程获取id;生成器的稳定性问题,一旦生成器不可用就影响整个系统。

下面介绍几种id生成策略。

5.2.1 Sequence ID

数据库自增长序列或字段,也是最常见的方式。由数据库维护,在一张表中是唯一的。优点是简单方便,性能也能接受,且数字id天然排序,对分页或需要排序的结果很有帮助。缺点是:

  • 不同数据库语法和实现不同,在数据库迁移或多版本支持的时候需要处理。
  • 单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成,有单点故障风险。
  • 当性能达不到要求时,难于扩展。
  • 分表分库涉及分布式id时行不通。

针对主库单点有一种优化方案,那就是使用多个master库,且每个master库的起始id不一样,步长一样(可以为master库个数)。如有三个master库,master1生成的id为1,4,7,10…,master2生成的id为2,5,8,11…,master3生成的id为3,6,9,12…,这样也能保证数据库集群中的唯一id,也可以大大降低id生成数据库操作的负载。当然这种方案中步长与master节点数是有关系的,也就是说master节点不能随意增加。

5.2.2 UUID和GUID

UUID生成简单,且不管是数据迁移还是系统数据合并,都能很好地适应,不会产生冲突,保证全局唯一性。但UUID没有排序,使用字符串存储,查询效率较低,存储消耗的空间较大,长度太长导致传输数据量大,几乎没有可读性。
事实上UUID有多种实现方案,GUID是微软的实现方案,其优缺点同UUID。

5.2.3 UUID的改进

由于UUID长度太长,可读性差,因此基于UUID有一些改进方案。

(1) 通过改变编码方式来改变长度

通常情况下,UUID由128位二进制组成,经过16进制编码后为36位字符串(连接符-占4位)。在此基础上,微软的GUID可以进一步通过64进制编码,将其变成19位数字。而在Java中也可以通过64进制转换或base64编码将其转换成22位字符串。网上资料较多,不累述。

经过转换后长度缩减了,但是存在重复的概率,而且可读性仍然不是很好,检索效率不高。

(2) Comb(combine guid/timestamp)算法

该算法结合GUID和时间戳,通过组合GUID和系统时间,以使其在索引和检索具有更优的性能。
COMB的基本设计思路是这样的: 既然GUID因毫无规律造成索引效率低下,影响了系统的性能,那么能不能通过组合方式,保留UUID的前10个字节,加上6个字节的GUID生成时间,将两者结合起来使用,即保留唯一性又增加了有序性,以此提供索引效率。

5.2.4 SnowFlake雪花算法

SnowFlake是Twitter开源的分布式Id生成算法,结果是一个Long型的ID。其核心思想是:使用41bit作为毫秒数(可使用69年),10bit作为机器的ID(5个bit是数据中心,5bit是机器ID),12bit作为毫秒内的流水号(即每个节点每毫秒可产生4096个ID),一个符号位为永远是0放在最前面。

SnowFlake算法可以根据自身项目的需要进行一定的修改,比如估算未来的数据中心个数,每个数据中心的机器数以及每一毫秒可能的并发数来调整算法中需要的bit数。

算法优点是不依赖于数据库,灵活方便,性能优于数据库,而且ID按照时间在单机上是递增的。雪花算法生成的id转换成字符串最长是19位(最大值2^63-1)。

5.2.5 MongoDB的ObjectId(BSON ObjectId)

MongoDB的ObjectId和SnowFlake算法类似,一共有12字节,长度是96bit。

前四个字节为时间戳,精确到秒。接下来3个字节是所在主机的唯一标识符,通常是机器主机名的散列值,后面2个字节是进程标识符。前面这9个字节保证了同一秒内不同机器不同进程产生的id是不同的。后三个字节是一个自动增加的计数器,确保相同进程同一秒产生的id也是不一样的,3个字节保证一秒内能产生1600万(2^24)左右不同id。

5.3 跨库跨表查询

分库带来了分布式事务问题,分表带来了分布式id问题,除此之外,分库分表还会带来一系列sql操作的问题。举个简单例子,在同一个库中的跨表查询,用join可以解决。但是当这两张表不在同一个库内,而是分布于两个库时,join就不行了。解决办法有以下几种方式:

  • (1)在应用层将跨表查询分成两次操作
  • (2)数据冗余,对一些常用的信息就行冗余,可以把原来需要join的操作变成单表查询。
  • (3)借助外部系统(例如搜索引擎)解决。

再说一个由分表带来的合并查询的问题。假如有一张用户信息表,根据用户id进行分表,分成了多张用户信息表。现在要对用户进行排序,展示第100页的用户信息。单表时只需要根据分页参数查询第100页的用户,而现在却要将每张表的前100页数据都取出来进行合并之后再排序,取出合并排序之后的第100页数据。当分页数一大,这个排序的数据量也随之增长。

另外还有其他的Max,Min,Sum,Count等函数处理、求平均值、非排序分页等等问题,这里不展开讨论。

总之,尽量不要分库分表,尤其是分库。

5.4 数据访问层的设计与实现

数据访问层是应用进行数据读/写访问的抽象层,在这个层上解决各个应用通用的访问数据库的问题。

对于无需分库分表的系统来说,数据的访问通过Spring提供的操作模板来操作数据库,已有的成熟方案基本没什么太大问题。这里说的问题是上文已经介绍过的分布式系统分库分表带来的数据访问问题,数据访问层说的是分布式数据访问层。

5.4.1 分布式数据访问层设计

数据访问层是应用进行数据读/写访问的抽象层,在这个层上解决各个应用通用的访问数据库的问题。这些问题是指分布式系统分库分表带来的数据访问问题,上文已经介绍过。因此数据访问层说的是分布式数据访问层(Distribure Access Layer,DAL),解决的是分布式存储方案带来的跨库跨表访问问题。

下面从两个角度来看DAL的设计与实现。

(1)从对外提供数据访问层的方式来看,DAL有三种实现方式,分别是:数据层专有API方式、采用JDBC方式、基于某个ORM/类ORM接口的方式。层次结构如下图所示:
在这里插入图片描述

比较来说,通过JDBC方式使用的数据层是兼容性和扩展性最好的,实现成本上也是相对最高的。封装某个ORM或类ORM框架的方式具备一定的通用性(限制了ORM),实现成本相对JDBC接口方式的要低。采用专有API的方式则是特定场景下的选择。

(2)从SQL执行流程的顺序来看,DAL要参与以下过程:
在这里插入图片描述

SQL解析阶段,考虑两个问题,一是对SQL支持的程度,是否需要支持所有的SQL;而是支持多少SQL方言,对于不同厂商超出标准SQL的部分要支持多少。这需要根据具体场景来决定,没有标准选择。

规则处理就是设计一种可用于定位数据的规则,以便通过此规则确认要操作的数据所在的位置。可采用固定哈希值,一致性哈希,虚拟节点,自定义等方式来设计规则。

SQL改写是因为在分库分表场景中,同一个逻辑表的不同物理表的表名可能一样,也可能不一样,需要根据具体情况来完成对表的操作。

在规则处理阶段就可以确认一个数据源组,但一个数据源组中是包含主库和从库的。数据源选择指在确定好的数据源组中选择需要操作的主库或从库。

接下来是SQL执行和执行结果处理阶段。这个过程中比较重要的是对异常的处理和判断,需要能够从异常中明确判断出数据库不可用的情况。

5.4.2 分布式数据访问层实现框架

市面上DAL产品是比较多的,主要的有:在Cobar基础上发展起来开源的MyCat,阿里部分开源的TDDL,当当开源的Sharding-JDBC。

TDDL
TDDL(Taobao Distributed Data Layer)是淘宝的分布式数据访问层中间件,主要解决了分库分表对应用的透明化以及异构数据库之间的数据复制。它是一个基于集中式配置的jdbc dataSource实现,具有主备,读写分离,动态数据库配置等功能。
TDDL功能比较强大,但没有开源。

Sharding-JDBC

Sharding-JDBC是当当网开源的数据库中间件。它是一个轻量级的DAL框架,使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可以理解为增强版的JDBC驱动,兼容性强。使用起来比较简单方便,网上资料也很多。

开源中国上有一篇关于Sharding-JDBC深度解析主题问答的文章(参考资料2),这里就不多说了。

参考资料

1.https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html。
2.https://my.oschina.net/editorial-story/blog/888650

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值