存储的瓶颈写到现在就要进入到深水区了,如果我们所做的网站已经到了做数据库垂直拆分和水平拆分的阶段,那么此时我们所面临的技术难度的挑战也会大大增强。
这里我们先回顾下数据库的垂直拆分和水平拆分的定义:
垂直拆分是一个粗粒度的拆分数据,它主要是将原来在一个数据库下的表拆分到不同的数据库里,水平拆分粒度比垂直拆分要更细点,它是将一张表拆到不同数据库里,粒度的粗细也会导致实现技术的难度的也不一样,很明显水平拆分的技术难度要远大于垂直拆分的技术难度。难度意味着投入的成本的增加以及我们需要承担的风险的加大,我们做系统开发一定要有个清晰的认识:能用简单的方案解决问题,就一定要毫不犹豫的舍弃复杂的方案,当系统需要使用高难度技术的时候,我们一定要让自己感受到这是迫不得已的。
我是以java工程师应聘进了我现在的公司,所以在我转到专职前端前,我也做过不少java的应用开发,当时我在公司的前辈告诉我,我们公司的数据库建模很简单,怎么个简单法了,数据库的表之间都没有外键,数据库不准写触发器,可以写写存储过程,但是存储过程决不能用于处理生产业务逻辑,而只能是一些辅助工作,例如导入导出写数据啊,后面听说就算是数据库做到了读写分离,数据之间同步也最好是用java程序做,也不要使用存储过程,除非迫不得已。开始我还不太理解这些做法,这种不理解不是指我质疑了公司的做法,而是我在想如果一个数据库我们就用了这么一点功能,那还不如让数据库公司为咋们定制个阉割版算了,不过在我学习了hadoop之后我有点理解这个背后的深意了,其实作为存储数据的数据库,它和我们开发出的程序的本质是一样的那就是:存储和计算,那么当数据库作为一个业务系统的存储介质时候,那么它的存储对业务系统的重要性要远远大于它所能承担的计算功能,当数据库作为互联网系统的存储介质时候,如果这个互联网系统成长迅速,那么这个时候我们对数据库存储的要求就会越来越高,最后估计我们都想把数据库的计算特性给阉割掉,当然数据库基本的增删改查我们是不能舍弃的,因为它们是数据库和外界沟通的入口,我们如果接触过具有海量数据的数据库,我们会发现让数据库运行的单个sql语句都会变得异常简洁和简单,因为这个时候我们知道数据库已经在存储这块承担了太多的负担,那么我们能帮助数据库的手段只能是尽量降低它运算的压力。
回到关于数据库垂直拆分和水平拆分的问题,假如我们的数据库设计按照我们公司业务数据库为蓝本的话,那么数据库进行了水平拆分我们会碰到什么样的问题了?为了回答这个问题我就要比较下拆分前和拆分后会给调用数据库的程序带来怎样的不同,不同主要是两点:
第一点:被拆出的表和原库的其他表有关联查询即使用join查询的操作需要进行改变;
第二点:某些增删改(注意:一般业务库设计很少使用物理删除,因为这个操作十分危险,这里的删往往是逻辑删除,一般做法就是更新下记录的状态,本质是一个更新操作)牵涉到拆分的表和原库其他表共同完成,那么该操作的事务性就会被打破,如果处理不好,假如碰到操作失败,业务无法做到回滚,这会对业务操作的安全性带来极大的风险。
关于解决第一点的问题还是相对比较简单的,方式方法也很多,下面我来讲讲我所知道的一些方法,具体如下:
方法一:在垂直拆表时候,我们先梳理下使用到join操作sql查询,梳理的维度是以被拆分出的表为原点,如果是弱依赖的join表我们改写下sql查询语句,如果是强依赖的join表则随拆分表一起拆分,这个方法很简单也很可控,但是这个技术方案存在一个问题,就是让拆分粒度变大,拆分的业务规则被干扰,这么拆分很容易犯一个问题就是一个数据库里总会存在这样一些表,就是很多数据库都会和它关联,我们很难拆解这些关联关系,当我们无法理清时候就会把该表做冗余,即不同数据库存在雷同表,随着业务增长,这种表的数据同步就成为了数据库的一个软肋,最终它会演变为整个数据库系统的短板甚至是全系统的短板。
方法二:我们拆表的准则还是按业务按需求在数据库层面进行,等数据库拆好后,再改写原来受到影响的join查询语句,这里我要说明的是查询语句修改的成本很低,因为查询操作是个只读操作,它不会改变任何底层的东西,如果数据表跨库,我们可以把join查询拆分为多次查询,最后将查询结果在内存中归纳和合并,其实我们如果主动拆库,绝不会把换个不同的数据库产品建立新库,肯定是使用相同数据库,同类型的数据库基本都支持跨库查询,不过跨库查询听说效率不咋地,我们可以有选择的使用。这种方案也有个致命的缺点,我们做数据库垂直拆分绝不可能一次到位,一般都是多次迭代,而该方案的影响面很大,关联方过多,每次拆表几乎要检查所有相关的sql语句,这会导致系统不断累积不可预知的风险。
以下三段内容是方法三:
不管是方法一还是方法二,都有一个很根本的缺陷就是数据库和上层业务操作耦合度很高,每次数据库的变迁都导致业务开发跟随做大量的同步工作,这样的后果就是资源浪费,做服务的人不能天天被数据库牵着鼻子走,这样业务系统的日常维护和业务扩展会很存问题,那么我们一定要有一个服务和数据库解耦方案,那么这里我们就得借鉴ORM技术了。(这里我要说明下,方法一和方法二我都是以修改sql阐述的,在现实开发里很多系统会使用ORM技术,互联网一般用ibatis和mybatis这种半ORM的产品,因为它们可以直接写sql和数据库最为亲近,如果使用hibernate则就不同了,但是hibernate虽然大部分不是直接写sql,但是它只不过是对数据库操作做了一层映射,本质手段是一致,所以上文的sql可以算是一种指代,它也包括ORM里的映射技术)
传统的ORM技术例如hibernate还有mybatis都是针对单库进行的,并不能帮我们解决垂直拆分的问题,因此我们必须自己开发一套解决跨库操作的ORM系统,这里我只针对查询的ORM谈谈自己的看法(讲到这里是不是有些人会有种似成相识的感觉,这个不是和分布式系统很像吗)。
其实具体怎么重构有问题的sql不是我想讨论的问题,因为这是个技术手段或者说是一个技术上的技巧问题,我这里重点讲讲这个ORM与服务层接口的交互,对于服务层而言,服务层最怕的就是被数据库牵着鼻子走,因为当数据库要进行重大改变时候,服务层总是想方设法让自己不要发生变化,对于数据库层而言服务层的建议都应该是合理,数据库层要把服务层当做自己的需求方,这样双方才能齐心协力完成这件重要的工作,那么服务层一般是怎样和数据库层交互的呢?
从传统的ORM技术我们可以找到答案,具体的方式有两种:
第一种:以hibernate为代表的,hibernate框架有一套自己的查询语言就是hql,它类似于sql,自定义一套查询语言看起来很酷,也非常灵活,但是实现难度非常之高,因为这种做法相当于我们要自己编写一套新的编程语言,如果这个语言设计不好,使用者又理解不深入,最后往往会事与愿违,就像hibernate的hql,我们经常令可直接使用sql也不愿意使用hql,这其中的缘由用过的人一定很好理解的。
第二种:就是数据层给服务层提供调用方法,每个方法对应一个具体的数据库操作,就算底层数据库发生重大变迁,只要提供给服务端的方法定义不变,那么数据库的变迁对服务层影响度也会最低。
前面我提到技术难度是我们选择技术的一个重要指标,相比之下第二种方案将会是我们的首选。
垂直拆分数据库还会带来另一个问题就是对事务的影响,垂直拆分数据库会导致原来的事务机制变成了分布式事务,解决分布式事务问题是非常难的,特别是如果我们想使用业界推出的解决分布式事务方案,那么要自己实现个分布式事务就更难了,不过这里我要说明一下,我这里说的更难是和我写本文有关,我本篇文章之所以现在才写是因为我想先研究下业界推出的分布式解决方案,但是这些方案的原理看得我很沮丧,我就想如果我们直接用方案的接口实现了它,因为还是不懂他的很多原理,那么这些方案其实就是不可控方案,说不定使用过多就会给系统埋下定时炸弹,因此这里我就只提提这些方案,有兴趣的童鞋可以去研究下:
一、X/OPEN组织推出的分布式事务规范XA,其中还包括该组织定义的分布式事务处理模型X/OPEN;
二、大型网站一致性理论CAP/BASE
三、 PAXOS协议。
这里特别要提的是PAXOS协议,我以前写过好几篇关于zookeeper的文章,zookeeper框架有一个特性就是它本身是一个分布式文件系统,当我们往zookeeper写数据时候,zookeeper集群能保证我们的写操作的可靠性,这个可靠性和我们使用线程安全来控制写数据一样,绝对不会让写操作出错,之所以zookeeper能做到这点,是因为zookeeper内部有一个类似PAXOS协议的协议,这个协议类似一个选举方案,它能保证写入操作的原子性。
其实事务也是和线程安全技术类似,只不过事务是要保证一个业务操作的原子性问题,当然事务还要有个特点就是回滚机制即业务操作失败,事务可以保证系统恢复到业务操作前的状态,回滚机制的本质其实是维护业务操作的状态性,具体点我这里列举个例子:当系统将要执行一个业务操作时候,我们首先为业务系统定义一个初始状态,业务执行操作时候我们可以定义一个执行状态,操作成功就是一个成功状态,操作失败就是一个操作失败状态,如果业务操作是失败状态,我们可以让业务回滚到初始状态,更进一步如果执行状态超时也可以将整个业务状态回退到初始状态,其实所有事务回滚机制的本质基本都是如此。记得不久前,在群里有个群友就问大家如何实现分布式事务,他想要知道的分布式事务是有没有一种技术能像我们操作数据库或者是jdbc那样一个commit,一个rollback就搞定,但是现实中的分布式事务比commit和rollback复杂的多,不可能简单的让我们写几个标记就能实现分布式事务,当然业界是有方案的,就是我上面提到的,如果有人真想知道可以自己研究下,不过我本人现在还是不太懂上面这些技术的原理和思想。
其实当时我马上给那位群友一个解答,我说我们开发时候是经常碰到分布式事务,但是我们解决分布式事务大多数从业务角度来解决的,而没去选择纯技术手段,因为技术手段太复杂难以控制。这个答案可能不会令提问者满意,但是我现在还是坚持这个观点,这个观点符合我提到的原则,当技术方案难度过高,我们就不要轻易选择使用它,因为这么做是很危险的,今天我就举个例子吧,这样可能更有说服力。我现在做的系统很多业务操作经常要和其他系统共同完成,其他系统有我们公司自己的系统,也有其他企业的系统,这里我还是把业务操作比作一辆在高速公路的汽车,那么每个系统就是高速公路上的一个收费站,业务每到一个收费站,该系统的数据库就会在对应的数据库的某张表里某条记录上记录一个状态,当汽车跑完全程,各个收费站就会相互通知,告诉大家任务完成,最终将所有的状态置为已完成,如果失败,就废掉这辆汽车,收费站之间也会相互通知,让所有的记录状态回归到初始状态,就当从来没有这辆汽车来过。这个做法的原理就是使用了事务回滚的本质,状态的变迁和回退,这个做法在业务系统开发里也有个专有术语就是工作流。其实大多数问如何实现分布式事务如何实现的问题的本质就是想解决事务的回滚问题,我们其实不要被这个分布式事务的名字给吓住了,其实有很多不起眼的技术手段和业务手段都能达到相同的目的。
晚上11点了,看来本文今天写不完了,今天就到此为止,最后我要总结下本文的内容,具体如下:
1. 大型网站解决存储瓶颈的问题,我们要找准存储这个关键点,因为数据库其实是存储和运算的组合体,但是在我们这个场景下,存储是第一位的,当存储是瓶颈时候我们要狠下心来尽量多的抛弃数据的计算特点,所以上文中我提出我们数据库就不要滥用计算功能了例如触发器、存储过程等等。
2. 数据库剥离计算功能不代表不要数据的计算功能,因为没有数据的计算功能数据库也就没价值了,那么我们要将数据库的计算功能进行迁移,迁移到程序里面,一般大型系统程序和数据库都是分开部署到不同服务器上,因此程序里处理数据计算就不会影响到数据库所在服务器的性能,就可以让安装数据库的服务器专心服务于存储。
3. 我们要尽一切可能的把数据库的变化对服务层的影响降到最低,最好是数据库做拆分后,现有业务不要任何的更改,那么我们就得设计一个全新的数据访问层,这个数据访问层将数据库和服务层进行解耦,任何数据库的变化都由数据访问层消化,数据访问层对外接口要高度统一,不要轻易改变。
4. 如果我们设计了数据访问层来解决数据库拆分的问题,数据访问层加上数据库其实就组合出了一个分布式数据库的解决方案,由此可见拆分数据库的难度是很高的,因为数据库将拥有分布式的特性,而分布式开发就意味开发难度的增加。
5. 对于分布式事务的处理,我们尽量要从具体问题具体分析,不要一感觉这个事务操作本质是分布式事务就去寻找通用的分布式事务技术手段,这样的想法其实是回避困难的思想,结果可能会是把问题搞得更加复杂。
好了,今天就写到这里吧,祝大家晚安,生活愉快!