简介
2001年,为了解决许多公司的软件团队陷入不断增长的过程泥潭,一批业界专家一起概括出了一些可以让软件开发团队具有快速工作、响应变化能力的价值 观和原则,他们称自己为敏捷联盟。敏捷开发过程的方法很多,主要有:SCRUM,Crystal,特征驱动软件开发(Feature Driven Development,简称FDD),自适应软件开发(Adaptive Software Development,简称ASD),以及最重要的极限编程(eXtreme Programming,简称XP)。极限编程(XP)是于1998年由Smalltalk社群中的大师级人物Kent Beck首先倡导的。
极限编程
设计和编程都是人的活动。忘记这一点,将会失去一切。
极限编程(XP)是敏捷方法中最著名的一个。它是由一系列简单却互相依赖的实践组成。这些实践结合在一起形成了一个胜于部分结合的整体。
下面是极限编程的有效实践:
- 完整团队 XP项目的所有参与者(开发人员、客户、测试人员等)一起工作在一个开放的场所中,他们是同一个团队的成员。这个场所的墙壁上随意悬挂着大幅的、显著的图表以及其他一些显示他们进度的东西。
- 计划游戏 计划是持续的、循序渐进的。每2周,开发人员就为下2周估算候选特性的成本,而客户则根据成本和商务价值来选择要实现的特性。
- 客户测试 作为选择每个所期望的特性的一部分,客户可以根据脚本语言来定义出自动验收测试来表明该特性可以工作。
- 简单设计 团队保持设计恰好和当前的系统功能相匹配。它通过了所有的测试,不包含任何重复,表达出了编写者想表达的所有东西,并且包含尽可能少的代码。
- 结对编程 所有的产品软件都是由两个程序员、并排坐在一起在同一台机器上构建的。
- 测试驱动开发 编写单元测试是一个验证行为,更是一个设计行为。同样,它更是一种编写文档的行为。编写单元测试避免了相当数量的反馈循环,尤其是功功能能验证方面的反馈循环。程序员以非常短的循环周期工作,他们先增加一个失败的测试,然后使之通过。
- 改进设计 随时利用重构方法改进已经腐化的代码,保持代码尽可能的干净、具有表达力。
- 持续集成 团队总是使系统完整地被集成。一个人拆入(Check in)后,其它所有人责任代码集成。
- 集体代码所有权 任何结对的程序员都可以在任何时候改进任何代码。没有程序员对任何一个特定的模块或技术单独负责,每个人都可以参与任何其它方面的开发。
- 编码标准 系统中所有的代码看起来就好像是被单独一人编写的。
- 隐喻 将整个系统联系在一起的全局视图;它是系统的未来影像,是它使得所有单独模块的位置和外观变得明显直观。如果模块的外观与整个隐喻不符,那么你就知道该模块是错误的。
- 可持续的速度 团队只有持久才有获胜的希望。他们以能够长期维持的速度努力工作,他们保存精力,他们把项目看作是马拉松长跑,而不是全速短跑。 极限编程是一组简单、具体的实践,这些实践结合在形成了一个敏捷开发过程。极限编程是一种优良的、通用的软件开发方法,项目团队可以拿来直接采用,也可以增加一些实践,或者对其中的一些实践进行修改后再采用。
敏捷开发
人与人之间的交互是复杂的,并且其效果从来都是难以预期的,但却是工作中最重要的方面。
敏捷软件开发宣言:
- 个体和交互 胜过 过程和工具
- 可以工作的软件 胜过 面面俱到的文档
- 客户合作 胜过 合同谈判
- 响应变化 胜过 遵循计划
虽然右项也有价值,但是我们认为左项具有更大的价值。
- 我们最优先要做的是通过尽早的、持续的交付有价值的软件来使客户满意。
- 即使到了开发的后期,也欢迎改变需求。敏捷过程利用变化来为客户创造竞争优势。
- 经常性地交付可以工作的软件,交付的间隔可以从几个星期到几个月,交付的时间间隔越短越好。
- 在整个项目开发期间,业务人员和开发人员必须天天都在一起工作。
- 围绕被激励起来的个体来构建项目。给他们提供所需的环境和支持,并且信任他们能够完成工作。
- 在团队内部,最具有效果并富有效率的传递信息的方法,就是面对面的交谈。
- 工作的软件是首要的进度度量标准。
- 敏捷过程提倡可持续的开发速度。责任人、开发者和用户应该能够保持一个长期的、恒定的开发速度。
- 不断地关注优秀的技能和好的设计会增强敏捷能力。
- 简单是最根本的。
- 最好的构架、需求和设计出于自组织团队。
- 每隔一定时间,团队会在如何才能更有效地工作方面进行反省,然后相应地对自己的行为进行调整。
当软件开发需求的变化而变化时,软件设计会出现坏味道,当软件中出现下面任何一种气味时,表明软件正在腐化。
- 僵化性: 很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其它改动。
- 脆弱性: 对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。
- 牢固性: 很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
- 粘滞性: 做正确的事情比做错误的事情要困难。
- 不必要的复杂性: 设计中包含有不具任何直接好处的基础结构。
- 不必要的重复性: 设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一。
- 晦涩性: 很难阅读、理解。没有很好地表现出意图。
敏捷团队依靠变化来获取活力。团队几乎不进行预先设计,因此,不需要一个成熟的初始设计。他们更愿意保持设计尽可能的干净、简单,并使用许多单元测试和验 收测试作为支援。这保持了设计的灵活性、易于理解性。团队利用这种灵活性,持续地改进设计,以便于每次迭代结束生成的系统都具有最适合于那次迭代中需求的 设计。 为了改变上面软件设计中的腐化味,敏捷开发采取了以下面向对象的设计原则来加以避免,这些原则如下:
- 单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。 - 开放-封闭原则(OCP)
软件实体应该是可以扩展的,但是不可修改。 - Liskov替换原则(LSP)
子类型必须能够替换掉它们的基类型。 - 依赖倒置原则(DIP)
抽象不应该依赖于细节。细节应该依赖于抽象。 - 接口隔离原则(ISP)
不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。 - 重用发布等价原则(REP)
重用的粒度就是发布的粒度。 - 共同封闭原则(CCP)
包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成任何影响。 - 共同重用原则(CRP)
一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中的所有类。 - 无环依赖原则(ADP)
在包的依赖关系图中不允许存在环。 - 稳定依赖原则(SDP)
朝着稳定的方向进行依赖。 - 稳定抽象原则(SAP)
包的抽象程度应该和其稳定程度一致。
上述中的包的概念是:包可以用作包容一组类的容器,通过把类组织成包,我们可以在更高层次的抽象上来理解设计,我们也可以通过包来管理软件的开发和发布。目的就是根据一些原则对应用程序中的类进行划分,然后把那些划分后的类分配到包中。
下面举一个简单的设计问题方面的模式与原则应用的示例:
问题:
选择设计运行在简易台灯中的软件,台灯由一个开关和一盏灯组成。你可以询问开关开着还是关着,也可以让灯打开或关闭。
解决方案一:
下面图1是一种最简单的解决方案,Switch对象可以轮询真实开关的状态,并且可以发送相应的turnOn和turnOff消息给Light。
解决方案二:
上面这个设计违反了两个设计原则:依赖倒置原则(DIP)和开放封闭原则(OCP),DIP原则告诉我们要优先依赖于抽象类,而Switch依赖了具 体类Light,对OCP的违反是在任何需要Switch的地方都要带上Light,这样就不能容易扩展Switch去管理除Light外的其他对象。为 了解决这个方案,可以采用ABSTRACT SERVER模式,在Switch和Light之间引入一个接口,这样就使得Switch能够控制任何实现了这个接口的东西,这也就满足了DIP和OCP 原则。如下面图2所示:
解决方案三:
上面图2所示解决方案,违返了单一职责原则(SRP),它把Switch和Light绑定在一起,而它们可能会因为不同的原因而改变。这种问题可以采用ADAPTER模式来解决,适配器从Switchable 派生并委托给Light,问题就被优美的解决了,现在,Switch就可以控制任何能够被打开或者关闭的对象。但是这也需要会出时间和空间上的代价来换取。如下面图3所示:
敏捷设计是一个过程,不是一个事件。它是一个持续的应用原则、模式以及实践来改进软件的结构和可读性的过程。它致力于保持系统设计在任何时间都尽可能得简单、干净和富有表现力。
参考文献
- 设计模式-可复用面向对象软件的基础 -- 李英军等译
- 重构-改善既有代码的设计 -- 侯捷等译
- 敏捷软件开发-原则、模式与实现 -- 邓辉译
第1章 引言
随着互联网应用的广泛普及,海量数据的存储和访问成为了系统设计的瓶颈问题。对于一个大型的 互联网应用,每天几十亿的PV无疑对数据库造成了相当高的负载。对于系统的稳定性和扩展性造成了极大的问题。通过数据切分来提高网站性能,横向扩展数据层 已经成为架构研发人员首选的方式。水平切分数据库,可以降低单台机器的负载,同时最大限度的降低了了宕机造成的损失。通过负载均衡策略,有效的降低了单台 机器的访问负载,降低了宕机的可能性;通过集群方案,解决了数据库宕机带来的单点数据库不能访问的问题;通过读写分离策略更是最大限度了提高了应用中读取 (Read)数据的速度和并发量。目前国内的大型互联网应用中,大量的采用了这样的数据切分方案,Taobao,Alibaba,Tencent,它们大 都实现了自己的分布式数据访问层(DDAL)。以实现方式和实现的层次来划分,大概分为两个层次(Java应用为例):JDBC层的封装,ORM框架层的 实现。就JDBC层的直接封装而言,现在国内发展较好的一个项目是被称作“变形虫”(Amoeba)的项目,由阿里集团的研究院开发,现在仍然处于测试阶 段(beta版),其运行效率和生产时效性有待考究。就ORM框架层的实现而言,比如Taobao的基于ibatis和Spring的的分布式数据访问 层,已有多年的应用,运行效率和生产实效性得到了开发人员和用户的肯定。本文就是以ORM框架层为基础而实现的分布式数据访问层。本课题的难点在于分库 后,路由规则的制定和选择以及后期的扩展性,比如:如何做到用最少的数据迁移量,达到扩充数据库容量(增加机器节点)的目的。核心问题将围绕数据库分库分 表的路由规则和负载均衡策略展开。
第2章 基本原理和概念
2.1基本原理:
人类认知问题的过程总是这样的:what(什么)-?why(为什么)-?how(怎么
做),接下来,本文将就这三个问题展开讨论和研究:
2.1.1什么是数据切分
"Shard"这个词英文的意思是"碎片",而作为数据库相关的技术用语,似乎最早见于大型多人在线角色扮演游戏中。"Sharding"姑且称之为"分片"。Sharding 不是一门新技术,而是一个相对简朴的软件理念。众所周知,MySQL 5之后才有了数据表分区功能,那么在此之前,很多 MySQL 的潜在用户都对 MySQL的扩展性有所顾虑,而是否具备分区功能就成了衡量一个数据库可扩展性与否的一个关键指标(当然不是唯一指标)。数据库扩展性是一个永恒的话题,MySQL的推广者经常会被问到:如在单一数据库上处理应用数据捉襟见肘而需要进行分区化之类的处理,是如何办到的呢? 答案是:Sharding。 Sharding 不是一个某个特定数据库软件附属的功能,而是在具体技术细节之上的抽象处理,是水平扩展(Scale Out,亦或横向扩展、向外扩展)的解决方案,其主要目的是为突破单节点数据库服务器的 I/O 能力限制,解决数据库扩展性问题。
通过一系列的切分规则将数据水平分布到不同的DB或table中,在通过相应的DB路由或者table路由规则找到需要查询的具体的DB或者table,以进行Query操作。这里所说的“sharding”通常是指“水平切分”,这也是本文讨 论的重点。具体将有什么样的切分方式呢和路由方式呢?行文至此,读者难免有所疑问,接下来举个简单的例子:我们针对一个Blog应用中的日志来说明, 比如日志文章(article)表有如下字段:

面对这样的一个表,我们怎样切分呢?怎样将这样的数据分布到不同的数据库中的表中去呢?其实 分析blog的应用,我们不难得出这样的结论:blog的应用中,用户分为两种:浏览者和blog的主人。浏览者浏览某个blog,实际上是在一个特定的 用户的blog下进行浏览的,而blog的主人管理自己的blog,也同样是在特定的用户blog下进行操作的(在自己的空间下)。所谓的特定的用户,用 数据库的字段表示就是“user_id”。就是这个“user_id”,它就是我们需要的分库的依据和规则的基础。我们可以这样做,将user_id为1~10000的所有的文章信息放入DB1中的article表中,将user_id为10001~20000的所有文章信息放入DB2中的article表中,以此类推,一直到DBn。这样一来,文章数据就很自然的被分到了各个数据库中,达到了数据切分的目的。接下来要解决的问题就是怎样找 到具体的数据库呢?其实问题也是简单明显的,既然分库的时候我们用到了区分字段user_id,那么很自然,数据库路由的过程当然还是少不了user_id的。考虑一下我们刚才呈现的blog应用,不管是访问别人的blog还是管理自己的blog,总之我都要知道这个blog的用户是谁吧,也 就是我们知道了这个blog的user_id,就利用这个user_id,利用分库时候的规则,反过来定位具体的数据库,比如user_id是234,利 用该才的规则,就应该定位到DB1,假如user_id是12343,利用该才的规则,就应该定位到DB2。以此类推,利用分库的规则,反向的路由到具体 的DB,这个过程我们称之为“DB路由”。
当然考虑到数据切分的DB设计必然是非常规,不正统的DB设计。那么什么样的DB设计是正统的DB设计呢?
我们平常规规矩矩用的基本都是。平常我们会自觉的按照范式来设计我们的数据库,负载高点可能 考虑使用相关的Replication机制来提高读写的吞吐和性能,这可能已经可以满足很多需求,但这套机制自身的缺陷还是比较显而易见的(下文会提 及)。上面提到的“自觉的按照范式设计”。考虑到数据切分的DB设计,将违背这个通常的规矩和约束,为了切分,我们不得不在数据库的表中出现冗余字段,用 作区分字段或者叫做分库的标记字段,比如上面的article的例子中的user_id这样的字段(当然,刚才的例子并没有很好的体现出user_id的 冗余性,因为user_id这个字段即使就是不分库,也是要出现的,算是我们捡了便宜吧)。当然冗余字段的出现并不只是在分库的场景下才出现的,在很多大 型应用中,冗余也是必须的,这个涉及到高效DB的设计,本文不再赘述。
2.1.2为什么要数据切分
上面对什么是数据切分做了个概要的描述和解释,读者可能会疑问,为什么需要数据切分呢?像Oracle这样成熟稳定的数据库,足以支撑海量数据的存储与查询了?为什么还需要数据切片呢?的确,Oracle的DB确实很成熟很稳定,但是高昂的使 用费用和高端的硬件支撑不是每一个公司能支付的起的。试想一下一年几千万的使用费用和动辄上千万元的小型机作为硬件支撑,这是一般公司能支付的起的吗?即 使就是能支付的起,假如有更好的方案,有更廉价且水平扩展性能更好的方案,我们为什么不选择呢?
但是,事情总是不尽人意。平常我们会自觉的按照范式来设计我们的数据库,负载高点可能考虑使 用相关的Replication机制来提高读写的吞吐和性能,这可能已经可以满足很多需求,但这套机制自身的缺陷还是比较显而易见的。首先它的有效很依赖 于读操作的比例,Master往往会成为瓶颈所在,写操作需要顺序排队来执行,过载的话Master首先扛不住,Slaves的数据同步的延迟也可能比较 大,而且会大大耗费CPU的计算能力,因为write操作在Master上执行以后还是需要在每台slave机器上都跑一次。这时候Sharding可能会成为鸡肋了。Replication搞不定,那么为什么Sharding可以工作呢?道理很简单,因为它可以很好的扩展。我们知道每台机器无论配置多么好它都有自身的 物理上限,所以当我们应用已经能触及或远远超出单台机器的某个上限的时候,我们惟有寻找别的机器的帮助或者继续升级的我们的硬件,但常见的方案还是横向扩 展,通过添加更多的机器来共同承担压力。我们还得考虑当我们的业务逻辑不断增长,我们的机器能不能通过线性增长就能满足需求?Sharding可以轻松的将计 算,存储,I/O并行分发到多台机器上,这样可以充分利用多台机器各种处理能力,同时可以避免单点失败,提供系统的可用性,进行很好的错误隔离。
综合以上因素,数据切分是很有必要的,且我们在此讨论的数据切分也是将MySql作为背景 的。基于成本的考虑,很多公司也选择了Free且Open的MySql。对MySql有所了解的开发人员可能会知道,MySQL 5之后才有了数据表分区功能,那么在此之前,很多 MySQL 的潜在用户都对 MySQL的扩展性有所顾虑,而是否具备分区功能就成了衡量一个数据库可扩展性与否的一个关键指标(当然不是唯一指标)。数据库扩展性是一个永恒的话题,MySQL的推广者经常会被问到:如在单一数据库上处理应用数据捉襟见肘而需要进行分区化之类的处理,是如何办到的呢?答案也是Sharding,也就是我们所说的数据切分方案。
我们用免费的MySQL和廉价的Server甚至是PC做集群,达到小型机+大型商业DB的效果,减少大量的资金投入,降低运营成本,何乐而不为呢?所以,我们选择Sharding,拥抱Sharding。
2.1.3怎么做到数据切分
说到数据切分,再次我们讲对数据切分的方法和形式进行比较详细的阐述和说明。
数据切分可以是物理上的,对数据通过一系列的切分规则将数据分布到不同的DB服务器上,通过路由规则路由访问特定的数据库,这样一来每次访问面对的就不是单台服务器了,而是N台服务器,这样就可以降低单台机器的负载压力。
数据切分也可以是数据库内的,对数据通过一系列的切分规则,将数据分布到一个数据库的不同表 中,比如将article分为article_001,article_002等子表,若干个子表水平拼合有组成了逻辑上一个完整的article表,这 样做的目的其实也是很简单的。举个例子说明,比如article表中现在有5000w条数据,此时我们需要在这个表中增加(insert)一条新的数 据,insert完毕后,数据库会针对这张表重新建立索引,5000w行数据建立索引的系统开销还是不容忽视的。但是反过来,假如我们将这个表分成100个table呢,从article_001一直到article_100,5000w行数据平均下来,每个子表里边就只有50万行数据,这时候我们向一张 只有50w行数据的table中insert数据后建立索引的时间就会呈数量级的下降,极大了提高了DB的运行时效率,提高了DB的并发量。当然分表的好 处还不知这些,还有诸如写操作的锁操作等,都会带来很多显然的好处。
综上,分库降低了单点机器的负载;分表,提高了数据操作的效率,尤其是Write操作的效率。行文至此我们依然没有涉及到如何切分的问题。接下来,我们将对切分规则进行详尽的阐述和说明。
上文中提到,要想做到数据的水平切分,在每一个表中都要有相冗余字符作为切分依据和标记字段,通常的应用中我们选用user_id作为区分字段,基于此就有如下三种分库的方式和规则:(当然还可以有其他的方式)
按号段分:
(1) user_id为区分,1~1000的对应DB1,1001~2000的对应DB2,以此类推;
优点:可部分迁移
缺点:数据分布不均
(2)hash取模分:
对user_id进行hash(或者如果user_id是数值型的话直接使用user_id的值也可),然后用一个特定的数字,比如应用中需要将一个数据库切分成4个数据库的话,我们就用4这个数字对user_id的hash值进行取模运算,也 就是user_id%4,这样的话每次运算就有四种可能:结果为1的时候对应DB1;结果为2的时候对应DB2;结果为3的时候对应DB3;结果为0的时 候对应DB4,这样一来就非常均匀的将数据分配到4个DB中。
优点:数据分布均匀
缺点:数据迁移的时候麻烦,不能按照机器性能分摊数据
(3)在认证库中保存数据库配置
就是建立一个DB,这个DB单独保存user_id到DB的映射关系,每次访问数据库的时候都要先查询一次这个数据库,以得到具体的DB信息,然后才能进行我们需要的查询操作。
优点:灵活性强,一对一关系
缺点:每次查询之前都要多一次查询,性能大打折扣
以上就是通常的开发中我们选择的三种方式,有些复杂的项目中可能会混合使用这三种方式。通过上面的描述,我们对分库的规则也有了简单的认识和了解。当然还会有更好更完善的分库方式,还需要我们不断的探索和发现。
第3章 本课题研究的基本轮廓
上面的文字,我们按照人类认知事物的规律,what?why?how这样的方式阐述了数据库 切分的一些概念和意义以及对一些常规的切分规则做了概要的介绍。本课题所讨论的分布数据层并不仅仅如此,它是一个完整的数据层解决方案,它到底是什么样的 呢?接下来的文字,我将详细阐述本研究课题的完整思想和实现方式。
分布式数据方案提供功能如下:
(1)提供分库规则和路由规则(RouteRule简称RR),将上面的说明中提到的三中切分规则直接内嵌入本系统,具体的嵌入方式在接下来的内容中进行详细的说明和论述;
(2)引入集群(Group)的概念,保证数据的高可用性;
(3)引入负载均衡策略(LoadBalancePolicy简称LB);
(4)引入集群节点可用性探测机制,对单点机器的可用性进行定时的侦测,以保证LB策略的正确实施,以确保系统的高度稳定性;
(5)引入读/写分离,提高数据的查询速度;
仅仅是分库分表的数据层设计也是不够完善的,当某个节点上的DB服务器出现了宕机的情况的时 候,会是什么样的呢?是的,我们采用了数据库切分方案,也就是说有N太机器组成了一个完整的DB,如果有一台机器宕机的话,也仅仅是一个DB的N分之一的 数据不能访问而已,这是我们能接受的,起码比切分之前的情况好很多了,总不至于整个DB都不能访问。一般的应用中,这样的机器故障导致的数据无法访问是可 以接受的,假设我们的系统是一个高并发的电子商务网站呢?单节点机器宕机带来的经济损失是非常严重的。也就是说,现在我们这样的方案还是存在问题的,容错 性能是经不起考验的。当然了,问题总是有解决方案的。我们引入集群的概念,在此我称之为Group,也就是每一个分库的节点我们引入多台机器,每台机器保 存的数据是一样的,一般情况下这多台机器分摊负载,当出现宕机情况,负载均衡器将分配负载给这台宕机的机器。这样一来,
就解决了容错性的问题。所以我们引入了集群的概念,并将其内嵌入我们的框架中,成为框架的一部分。

如上图所示,整个数据层有Group1,Group2,Group3三个集群组成,这三个集 群就是数据水平切分的结果,当然这三个集群也就组成了一个包含完整数据的DB。每一个Group包括1个Master(当然Master也可以是多个)和N个Slave,这些Master和Slave的数据是一致的。 比如Group1中的一个slave发生了宕机现象,那么还有两个slave是可以用的,这样的模型总是不会造成某部分数据不能访问的问题,除非整个Group里的机器全部宕掉,但是考虑到这样的事情发生的概率非常小(除非是断电了,否则不易发生吧)。
在没有引入集群以前,我们的一次查询的过程大致如下:请求数据层,并传递必要的分库区分字段 (通常情况下是user_id)?数据层根据区分字段Route到具体的DB?在这个确定的DB内进行数据操作。这是没有引入集群的情况,当时引入集群会 是什么样子的呢?看图一即可得知,我们的路由器上规则和策略其实只能路由到具体的Group,也就是只能路由到一个虚拟的Group,这个Group并不 是某个特定的物理服务器。接下来需要做的工作就是找到具体的物理的DB服务器,以进行具体的数据操作。基于这个环节的需求,我们引入了负载均衡器的概念 (LB)。负载均衡器的职责就是定位到一台具体的DB服务器。具体的规则如下:负载均衡器会分析当前sql的读写特性,如果是写操作或者是要求实时性很强 的操作的话,直接将查询负载分到Master,如果是读操作则通过负载均衡策略分配一个Slave。我们的负载均衡器的主要研究放向也就是负载分发策略, 通常情况下负载均衡包括随机负载均衡和加权负载均衡。随机负载均衡很好理解,就是从N个Slave中随机选取一个Slave。这样的随机负载均衡是不考虑 机器性能的,它默认为每台机器的性能是一样的。假如真实的情况是这样的,这样做也是无可厚非的。假如实际情况并非如此呢?每个Slave的机器物理性能和 配置不一样的情况,再使用随机的不考虑性能的负载均衡,是非常不科学的,这样一来会给机器性能差的机器带来不必要的高负载,甚至带来宕机的危险,同时高性 能的数据库服务器也不能充分发挥其物理性能。基于此考虑从,我们引入了加权负载均衡,也就是在我们的系统内部通过一定的接口,可以给每台DB服务器分配一 个权值,然后再运行时LB根据权值在集群中的比重,分配一定比例的负载给该DB服务器。当然这样的概念的引入,无疑增大了系统的复杂性和可维护性。有得必 有失,我们也没有办法逃过的。
有了分库,有了集群,有了负载均衡器,是不是就万事大吉了呢?事情远没有我们想象的那么简 单。虽然有了这些东西,基本上能保证我们的数据层可以承受很大的压力,但是这样的设计并不能完全规避数据库宕机的危害。假如Group1中的slave2宕机了,那么系统的LB并不能得知,这样的话其实是很危险的,因为LB不知道,它还会以为slave2为可用状态,所以还是会给slave2分配负载。这 样一来,问题就出来了,客户端很自然的就会发生数据操作失败的错误或者异常。这样是非常不友好的!怎样解决这样的问题呢?我们引入集群节点的可用性探测机 制,或者是可用性的数据推送机制。这两种机制有什么不同呢?首先说探测机制吧,顾名思义,探测即使,就是我的数据层客户端,不定时对集群中各个数据库进行 可用性的尝试,实现原理就是尝试性链接,或者数据库端口的尝试性访问,都可以做到,当然也可以用JDBC尝试性链接,利用Java的Exception机 制进行可用性的判断,具体的会在后面的文字中提到。那数据推送机制又是什么呢?其实这个就要放在现实的应用场景中来讨论这个问题了,一般情况下应用的DB数据库宕机的话我相信DBA肯定是知道的,这个时候DBA手动的将数据库的当前状态通过程序的方式推送到客户端,也就是分布式数据层的应用端,这个时候在 更新一个本地的DB状态的列表。并告知LB,这个数据库节点不能使用,请不要给它分配负载。一个是主动的监听机制,一个是被动的被告知的机制。两者各有所 长。但是都可以达到同样的效果。这样一来刚才假设的问题就不会发生了,即使就是发生了,那么发生的概率也会降到最低。
上面的文字中提到的Master和Slave,我们并没有做太多深入的讲解。如图一所示,一 个Group由1个Master和N个Slave组成。为什么这么做呢?其中Master负责写操作的负载,也就是说一切写的操作都在Master上进 行,而读的操作则分摊到Slave上进行。这样一来的可以大大提高读取的效率。在一般的互联网应用中,经过一些数据调查得出结论,读/写的比例大概在10:1左右,也就是说大量的数据操作是集中在读的操作,这也就是为什么我们会有多个Slave的原因。但是为什么要分离读和写呢?熟悉DB的研发人员都 知道,写操作涉及到锁的问题,不管是行锁还是表锁还是块锁,都是比较降低系统执行效率的事情。我们这样的分离是把写操作集中在一个节点上,而读操作其其他 的N个节点上进行,从另一个方面有效的提高了读的效率,保证了系统的高可用性。读写分离也会引入新的问题,比如我的Master上的数据怎样和集群中其他 的Slave机器保持数据的同步和一致呢?这个是我们不需要过多的关注的问题,MySql的Proxy机制可以帮助我们做到这点,由于Proxy机制与本 课题相关性不是太强,
在这里不做详细介绍。
综上所述,本课题中所研究的分布式数据层的大体功能就是如此。以上是对基本原理的一些讨论和阐述。接下来就系统设计层面,进行深入的剖析和研究。
第4章 系统设计
4.1系统实现层面的选择
在引言部分中提到,该系统的实现层面有两种选择,一种是基于JDBC层面上的选择,一种是基 于现有数据持久层框架层面上的选择,比如Hibernate,ibatis。两种层面各有长处,也各有不足之处。基于JDBC层面上的系统实现,系统开发 难度和后期的使用难度都将大大提高。大大增加了系统的开发费用和维护费用。本课题的定位是在成型的ibatis持久层框架的基础上进行上层的封装,而不是 对ibatis源码的直接修改,这样一来使本系统不会对现有框架有太多的侵入性,从而也增加了使用的灵活性。之所以选择ibatis,原因如下:
(1)ibatis的学习成本非常低,熟练的Java Programmer可在非常的短时间内熟练使用ibatis;
(2)ibatis是轻量级的ORM,只是简单的完成了RO,OR的映射,其查询语句也是通 过配置文件sql-map.xml文件在原生sql的层面进行简单的配置,也就是说我们没有引入诸如Hibernate那样的HQL的概念,从而增强了sql的可控性,优秀的DBA可以很好的从sql的层面对sql进行优化,使数据层的应用有很强的可控性。Hibernate虽然很强大,但是由于Hibernate是OR的一个重型封装,且引入HQL的概念,不便于DBA团队对sql语句的控制和性能的调优。
基于以上两点理由,本课题在ORM的产品的选择上选择了易学易用且轻量级的持久层框架ibatis。下面的讨论也都是特定于ibatis的基础上的讨论。
4.2其他开源框架的选择
在一些大型的Java应用中,我们通常会采用Spring这样的开源框架,尤其是IoC(DI)这部分,有效的帮助开发人员管理对象的依赖关系和层次,降低系统各层次之间的实体耦合。Spring的优点和用处我相信这是开发人员众所周 知的,在此不再赘述。本课题的数据层也将采用Spring做为IoC(DI)的框架。
可能 MySQL Proxy 很多数据库技术人员都比较熟悉了,在前不久,国内也有开发者发布了一个Amoeba(变形虫)项目(http://sourceforge.net/projects/amoeba),这个项目专注分布式数据库 Proxy 开发,引起了广泛的关注。 DBA notes( http://www.dbanotes.net ) 站长Fenng 有幸采访了阿米巴项目的开发者陈思儒,以下是采访全文。
Fenng:你好,思儒,很高兴能接受采访,简要介绍一下自己吧? 陈思儒: 我目前是盛大计算机(上海)有限公司的一位高级研究员。 在就业生涯中从事分布式消息系统、分布式应用、多层框架设计、规则引擎开发框架研究,以及 Java 2D MMORPG框架研究。
Fenng: 说一下当初开发 Amoeba 项目的缘由如何,估计其中也会有不少小故事吧? 我可是非常好奇。 陈思儒: 其实 Amoeba 的前身是网络数据包分析代理,之所以 Amoeba 能够快速稳定发展也是因为个人在前期使用这些技术分析过一些游戏的数据包(这儿不方便透露一些细节,这些都只是个人爱好而已,并没有破坏那些被我研究过的游戏 :D )。
为什么会有 Amoeba 这个产品,这个话题的确非常有意思,我关注 MySQL Proxy 也有一段时间了。MySQL Proxy 的这种想法做的非常棒,它能够根据自己的想法去构造目标的 MySQL Proxy 应用,比如监控 SQL 执行、数据流量、读写分离。但由于我们使用 MySQL Proxy 并不能非常轻易的解决一些问题(读写分离、数据切分、水平切分、负载均衡),而是需要写大量的Lua Script,这些 Lua 并不是现成的而是自己需要去写。这个工作对于并不熟悉 MySQL Proxy 内置变量、MySQL Protocol 来说是非常困难的。
因此带着这个想法我就设想做一个非常容易使用、可移植性非常强的软件。Amoeba就因此诞生了。为什么叫Amoeba? 其实这个想法我是突然想到的,Amoeba中文意思是"变形虫",Amoeba被设想为数据库代理的开发框架,它可以为符合Amoeba框架的任何数据库开发代理层。因此也比较象"变形虫"一样能够变成目标数据库的代理层软件。
Fenng: 我观察到 Amoeba 与 Oracle 交互的时候似乎还是模拟 MySQL 的驱动器, 实际情况是否如此? 陈思儒:目前Amoeba有2个产品:Amoeba for MySQL,Amoeba for Aladdin 。这2个产品有不同的适用范围:
- Amoeba for MySQL:被代理的只有MySQL数据库,需要分析 MySQL网络协议,它的代价比Aladdin会小很多,而且性能也较高;
- Amoeba for Aladdin:被代理的可以是目前提供Jdbc driver的所有数据库,这些数据库可以同时并存于Amoeba for Aladdin后端。
其实在我上一家公司的时候也正在研发 Amoeba for Oracle,这个产品目前还正在研发状态。这个产品性能也跟 Amoeba for MySQL 一样出色。
就 Amoeba for Aladdin 产品来说,后端的任何数据与 Aladdin 交互采用 JDBC Driver. 对于 Aladdin来说,数据库的协议是透明的,而应用跟 Aladdin 的交互则采用 MySQL 协议,这个做法很多人有不明白,其实做这个决定主要是想借助 MySQL 使用的广泛程度以及对各种 开发语言的支持。因此对前端的应用来说 Aladdin 就是一个虚拟的 MySQL 数据库。你这个问题应该是问使用 Aladdin 的时候。
Fenng: 是的,是在看了你的 Aladdin 架构图后产生的疑惑。你比我严谨多了(笑)。能否冒昧问一下 Amoeba 项目当前的局限? 陈思儒:虽然 Amoeba 能够很好的解决水平切分、垂直切分等,但还是会存在一些局限性,这些局限性是因为我们在设计目标的数据库架构的时候就必须考虑到我们未来的数据库框架(可以线性扩容的数据库架构)。因此也有些查询将不支持,比如跨数据库服务器进行Join,我们要尽量避免类似的业务出现,也可以通过多次查询来解决这类问题。
Fenng: 你在前面也说到了 MySQL Proxy,能否简单的说说 Amoeba 与 MySQL Proxy的区别? 陈思儒:其实说与MySQL Proxy的区别应该是Amoeba for MySQL与 MySQL Proxy 的区别,在上面表述的第二点应该都涉及到了:
- Amoeba只是目标数据库代理的开发框架。
- Amoeba for Aladdin 是另外一个类似Amoeba for MySQL的产品。他们共同点是都可以做负载均衡(HA、ROUNDROBIN,WEIGHTBASED)、读写分离、数据切分(垂直、水平)、failOver。
Fenng: 据说你也分析过 Oracle 的 TNS 协议,你认为可靠性如何? 陈思儒:的确,我在上一家公司为做 Amoeba for Oracle 分析过 TNS协议,可靠性以及安全方面都是没什么问题,Oracle 还提供了一些网络层的性能参数,用来改变Oracle的网络吞吐量。但是 Oracle 的数据部封包做法让我和老同事在分析 Oracle 数据包的时候伤透脑筋, 我觉得 Oracle 如果想在协议上面升级版本将不是一件容易的事情。而MySQL 的协议封包的做法我比较赞同。
Fenng: 对于分析 TNS 的可行性,我的看法倒是和你类似。顺便问一下,现在 Amoeba 是否已经有了成功案例, 方便的话能否举几个? 陈思儒:Amoeba for MySQL 的成功案例,目前就网友直接跟我说的有几个,但我还没具体了解他们具体的公司名称,以后知道了我再透露。
Fenng: 到时候千万要通知我一下。对了,能否说一下 Amoeba 项目的愿景以及下一步的目标? 陈思儒:目前距离Amoeba发展目标还有一点距离,Amoeba 未来将是一个更加容易使用、可管理、可动态装载配置、Amoeba集群等。
如果未来可行的话,我将补充 MySQL 协议,做一个用于负载均衡的"重定向路由器"(类似F5功能,但它只是做连接跳转)。
Fenng: 现在开发者主要是你一个人? 是否还有其他维护者 ? 陈思儒:Amoeba 框架、Amoeba for MySQL、Amoeba for Aladdin 目前的开发就我一个人。
Fenng: 很高兴你接受我的采访。期待 Amoeba 项目取得更大的成就,也祝你工作愉快! 陈思儒:客气!也希望有更多的开源爱好者或是数据库技术爱好者加入到这个项目的开发中来。关于 Amoeba 项目的进展我会在 "Amoeba 开发者博客"上更新,欢迎订阅。
//核查ULR是否有效
function checklongurl($src)
{
$matches = array();
preg_match('@^(?:http://)?([^/]+)@i', $src, $matches);
$host = $matches[1];
// get last two segments of host name
preg_match('/[^.]+\.[^.]+$/', $host, $matches);
$ischeck = isValid($matches[0]);
}
首先我们分解'@^(?:http://)?([^/]+)@i'
@是分隔符
^表示匹配的字符必须在最前边,也就是什么字符开始
()圆括号是标记一个正则表达式的开始和结束。
@后面的那个i表示不区分大小写
//)后面的那个?号表示前面的出现0次或者1次
PS
PHP正则表达式的基本语法:
一个正则表达式,分为三个部分:分隔符,表达式和修饰符。
单例模式可能是所有设计模式中最简单,但却是非常重要的一种设计模式。
很多情况下都需要类只有一个实例,比如说数据库的实例对象(可以看深空的那个数据库类。就是采用了单例模式)。因为他只有一个类图。所以说他简单。
通过几个提问,可能你能更容易的理解单例模式!
你如何创建类的实例?
使用new关键字
那如何避免一个类的实例创建?
可以这样操作么?
class MyObject{
private function __construct() {
}
}
那这样将如何获取这个类的实例?
因为构造函数已经被私有化。只有类的内部才能调用这个类的实例。外部想调用是没办法的。
那如果这样操作是不是就解决了?
class MyObject{
private function __construct() {
}
public function getInstance(){
return new MyObject;
}
}
直接外部调用getInstance()方法就能够返回这个类的实例。这样做的好处是保证整个应用程序中只有一个类的实例。当然。需要进一步的限制。为了保证这个类的实例是独一无二的。所以得在类的内部进行存储,判断。下面是标准的单例模式的例子
/**
* 单例模式实例
*/
class MyObject{
//提供一个私有的静态成员。他用来存储这个类在整个应用程序中的实例
static $obj;//并且没有声明是什么保护类型。子类也可以直接使用。
//下面这个变量是测试用的
private $test;
private function __construct(){
//可以在这里写入你构造这个类实例的时候初始化操作
$this->test="我将进行单例模式的操作";
}
//通常情况下的单利模式都是采用getInstance作为单例引用的方法名
public static function getInstance(){
//通过instanceof类型判断是否这个类的实例变量已经进行了初始化。此过程是保证整个类的实例是独一无二的
if(!(self::$obj instanceof self)){
self::$obj = new self();
}
return self::$obj;
}
//对__clone方法进行私有化是为了防止外部通过复制这个类的副本而形成不同的实例引用。
private function __clone(){}
public function test(){
echo $this->test;
}
}
$a= MyObject::getInstance();
$a->test();
self的用法,self代表了整个类的引用,而不是任何具体的实例引用。
类图我就不画了。挺简单的东西。就一个类。
作为单例模式。必须具有一下几个特征:
1、必须有一个显性的构造器,并且申明为私有的(private)
2、必须有一个静态成员(static mamber)来记录这个类的实例引用。
3、必须有一个公开的(public)方法来对这个类进行实例化调用
其他的注意项我已经在上面的例子中进行了著名。结果我就不贴了。自己运行尝试。
定义单例模式:
确保一个类只有一个实例,并提供全局访问点。
没什么需要交代的了。就这么多。赶紧去修改自己的数据库类吧!
上几章都大致了解了在OOP设计中,应该针对抽象编程而不是具体实现编程。
但是上面几章的代码中或多或少都又"new"来创建对象的实例。那么在这些地方,就不是针对抽象编程,而成了具体实现的编程。
但使用"new" 有错吗?从本质上讲是没错的,因为这是OOP的基础。但是,从另一个角度去说,他是错误的。但错不在他。而在程序上面。
简单的说,就是我们使用了new关键词将代码的执行硬编码进了程序之中。他不能在程序运行时来决定运行哪一个。也就是说,当我们希望一个项目需要改变的时候,需要添加新的对象的时候,需要打开文件进去修改
第三节的装饰模式重点讲过一个原则:对修改关闭,对扩展打开。但是,频繁使用new这个创建实例的办法就是在破坏这个原则。因为你总是会要在不久的将来改变他的。只要你要添加新的对象。你必定要修改源文件。
那错误具体是由什么导致的?第一章我说过,OO编程的基本原理不是那些原则,而是Change!改变。一切都是因为改变捣的鬼。
看看下面这个例子:
我们需要开一个面包店。已经建立好了面包订购的类(Bread):
下面用另一个类的方法调用他。
function orderBread(){
$bread = new Bread();//如果说大家看了上面的设计模式,或者已经对设计模式有了一定的认识,这里肯定那个希望是建立一个抽象类或者接口。可是那样就没办法实例化了
//对面包进行各样的操作
$bread->prepare();
$bread->bake();
$bread->cut();
$bread->box();
}
这里本身没有任何错误。但是,如果我要更多的面包呢?只能由我们来决定面包的种类,然后叫这个方法来制造面包了
function orderBread($_bread){
if($_bread == "BlackBread"){
$this->break = new BlackBread();
}else if ($_break == "WhiteBread"){
$this->break = new WhiteBread();
}else if($_break == "SweetBread"){
$this->break = new SweetBread();
}else ($_break == "NutsBread"){
$this->break = new NutsBread();
}
//对面包进行各样的操作
$bread->prepare();
$bread->bake();
$bread->cut();
$bread->box();
}
手写一堆代码。。真麻烦
恩,这样就完成了。如果以后要改呢?要增加面包,或者减少面包种类呢?是不是必须得打开这个文件来修改他。删删改改的。
这样就找到问题结症了。问题不在new,而在这一堆的改变上面。他们随时会改变的。你就得随时的去改变这些文件。
那就把它拿出来。
if($_bread == "BlackBread"){
$this->break = new BlackBread();
}else if ($_break == "WhiteBread"){
$this->break = new WhiteBread();
}else if($_break == "SweetBread"){
$this->break = new SweetBread();
}else ($_break == "NutsBread"){
$this->break = new NutsBread();
}
这一块改变拿出来,放到另一个类里面。这样方法orderBread就不用关心是要定什么面包了。他只要知道调用它的时候需要制作一块面包。
这就是工厂!用工厂来建立对象!
先从工厂入手:
<?php
require("Bread.php");
class BreadFactory{
private $bread;
//通过这个creatBread方法创建我们的面包
public function creatBread($_bread){
if(is_string($_bread)){
//类型判断是必须的。php是弱类型语言。
if($_bread == "BlackBread"){
$this->bread = new BlackBread();
}elseif ($_bread == "WhiteBread"){
$this->bread = new WhiteBread();
}elseif($_bread == "SweetBread"){
$this->bread = new SweetBread();
}elseif($_bread == "NutsBread"){
$this->bread = new NutsBread();
}else{
echo "本工厂不提供这款面包";
}
//以上没有任何变化。将他从原来的面包点的代码中拿出来
}
return $this->bread;
}
}
?>
然后是面包种类
<?php
/**
* 这里存放很多很多种类的面包类。共用抽象类,Bread
*/
abstract class Bread {
abstract function prepare();
abstract function bake();
abstract function cut();
abstract function box();
}
class BlackBread extends Bread {
public function prepare(){
echo "准备好黑面包</br>";
}
public function bake(){
echo "烘烤黑面包</br>";
}
public function cut(){
echo "切片黑面包</br>";
}
public function box(){
echo "黑面包已经打包好了</br>";
}
}
class WhiteBread extends Bread{
public function prepare(){
echo "准备好白面包</br>";
}
public function bake(){
echo "烘烤白面包</br>";
}
public function cut(){
echo "切片白面包</br>";
}
public function box(){
echo "白面包已经打包好了</br>";
}
}
class SweetBread extends Bread{
public function prepare(){
echo "准备好甜面包</br>";
}
public function bake(){
echo "烘烤甜面包</br>";
}
public function cut(){
echo "切片甜面包</br>";
}
public function box(){
echo "甜面包已经打包好了</br>";
}
}
class NutsBread extends Bread{
public function prepare(){
echo "准备好果仁面包</br>";
}
public function bake(){
echo "烘烤果仁面包</br>";
}
public function cut(){
echo "切片果仁面包</br>";
}
public function box(){
echo "果仁面包已经打包好了</br>";
}
}
?>
面包和工厂准备好了。最后开店
/**
* 面包店类。开店咯
*/
require("BreadFactory.php");
class BreadStore{
private $bread;
//通过构造函数来创建面包.你开店必须得找到源头啊。所以得定义工厂是谁
public function __construct(BreadFactory $_bread){
$this->bread = $_bread;
}
public function orderBread($_type){
$orderBread = $this->bread->creatBread($_type);
$orderBread->prepare();
$orderBread->bake();
$orderBread->cut();
$orderBread->box();
}
}
最后测试一下
$a=new BreadFactory();
$b=new BreadStore($a);
//测试一下.我先订购一款黑面包
$b->orderBread("BlackBread");
//再来个白面包如何
$b->orderBread("WhiteBread");
结果是
准备好黑面包
烘烤黑面包
切片黑面包
黑面包已经打包好了
准备好白面包
烘烤白面包
切片白面包
白面包已经打包好了
是不是很简单,思路也非常清晰~
注意,我在Bread文件里面让所有的面包都继承了个抽象类,单是在工厂里却没使用这个抽象类。实际上,所有的变量前面你应该理解成Bread的类型。
这个其实是简单工厂。并不能叫做完全的模式。但是我个人在很多的教程里面都看到将这样的过程称为工厂模式。事实上,这只是个编程习惯,但经常使用。所以还是能将其归纳进工厂模式的。
下面来看看我的简单面包点类图是如何的
再次提醒!这里所说的实现接口并不是单指:写一个类,用implements关键字来实现一个接口。而是泛指实现某个超类型(可以是一个类也可以是具体接口)的某个方法
现在我们又有了新的改变。面包店生意太好了。有一些其他省市的面包店希望加盟我们。
好像问题不是很大,用上面的简单工厂方式。那就每个工厂一个工厂类。HNBreadFactory,GDBreadFactory。
这样并没多大问题。但是我们面包店生意好就是因为我们的一些质量有保证。如果我希望多点质量控制呢?都用同样的揉面,切面,装箱。流程都是一模一样的。如何操作?下面就用工厂方法来完成这个工作。看看他是如何工作的。
先将creatBread方法从工厂类里面移出来,并放入我们原先的面包店。但他只知道建立面包。具体建立什么面包他并不知道。需要完成具体面包的是在他的子类里,也就是说将他抽象话。不仅抽象整个Bread类,也要抽象这个creatBread方法。
下面是代码
<?php
/**
* 面包店类。开店咯
*/
abstract class BreadStore{
private $bread;
public function orderBread($_type){
//调用自身的抽象方法
$orderBread = $this->creatBread($_type);
$orderBread->prepare();
$orderBread->bake();
$orderBread->cut();
$orderBread->box();
}
abstract function creatBread($_type);
}
?>
这样做的好处是抽象的类BreadStore并不能决定建立什么样的面包。当然,我这里说的由子类建立面包,并不是他自身在运行时决定。而是在下面的子类来决定他应该如何提供面包。你从这个代码里面只能理解到。
$orderBread = $this->creatBread($_type);
订购面包。
订购什么样的面包,BreadStore并不知道这个面包是什么样的。可他是抽象的,不能实例化。所以是由他的子类来重写creatBread类来决定建立什么样的面包。
看看他的子类如何做的
<?php
require("Bread.php");
require("BreadStore.php");
//在湖南开店
class HNBreadStore extends BreadStore{
public function creatBread($_bread){
if(is_string($_bread)){
//类型判断是必须的。php是弱类型语言。
if($_bread == "BlackBread"){
return new HNBlackBread();
}elseif ($_bread == "WhiteBread"){
return new HNiteBread();
}elseif($_bread == "SweetBread"){
return new HNSweetBread();
}elseif($_bread == "NutsBread"){
return new HNNutsBread();
}
}
}
}
//在广东开店
class GDBreadStore extends BreadStore{
public function creatBread($_bread){
if(is_string($_bread)){
//类型判断是必须的。php是弱类型语言。
if($_bread == "BlackBread"){
return new GDBlackBread();
}elseif ($_bread == "WhiteBread"){
return new GDiteBread();
}elseif($_bread == "SweetBread"){
return new GDSweetBread();
}elseif($_bread == "NutsBread"){
return new GDNutsBread();
}else return null;
}
}
}
?>
子类里通过重写方法来决定如何建立面包。每一个加盟店都有自己的面包对象家族。他来决定采用哪样的面包。
下面是关键的地方。因为光又面包店,却没有面包,当然是不行的。
这里相对于上面的简单工厂模式有了点改变,因为为了更好的扩展面包的种类。所以面包抽象类进行了改变,直接由他来定义好了各种质量保证流程
<?php
/**
* 这里存放很多很多种类的面包类。共用抽象类,Bread
*/
abstract class Bread {
protected $name;
protected $douch;
protected $sauce;
protected $toppings=array();
public function prepare(){
echo "正在准备".$this->name."</br>";
echo "揉入".$this->douch."...<br>";
echo "加入调料".$this->sauce."...<br>";
echo "加入配料...";
foreach ($this->toppings as $value){
echo " ".$value." ";
}
echo "</br>";
}
function bake(){
echo "烘烤10分钟<br>";
}
function cut(){
echo "切成圆形<br>";
}
function box(){
echo "放入盒子<br>";
}
}
class HNBlackBread extends Bread {
public function __construct(){
$this->name = "湖南黑面包";
$this->douch = "酵母面团";
$this->sauce = "辣椒酱";
$this->toppings = array("鸡蛋");
}
}
class HNWhiteBread extends Bread{
public function __construct(){
$this->name = "湖南白面包";
$this->douch = "不发酵面团";
$this->sauce = "辣椒酱";
$this->toppings = array("鸡蛋");
}
}
class HNSweetBread extends Bread{
public function __construct(){
$this->name = "湖南甜面包";
$this->douch = "酵母面团";
$this->sauce = "辣椒酱";
$this->toppings = array("鸡蛋","糖精","果酱");
}
}
class HNNutsBread extends Bread {
public function __construct(){
$this->name = "湖南果仁面包";
$this->douch = "酵母面团";
$this->sauce = "辣椒酱";
$this->toppings = array("鸡蛋","开心果");
}
}
class GDBlackBread extends Bread {
public function __construct(){
$this->name = "广东黑面包";
$this->douch = "酵母面团";
$this->sauce = "糖精";
$this->toppings = array("鸡蛋");
}
}
class GDWhiteBread extends Bread{
public function __construct(){
$this->name = "广东白面包";
$this->douch = "不发酵面团";
$this->sauce = "糖精";
$this->toppings = array("鸡蛋");
}
}
class GDSweetBread extends Bread{
public function __construct(){
$this->name = "广东甜面包";
$this->douch = "酵母面团";
$this->sauce = "糖精";
$this->toppings = array("鸡蛋","糖精","果酱");
}
}
class GDNutsBread extends Bread {
public function __construct(){
$this->name = "广东果仁面包";
$this->douch = "酵母面团";
$this->sauce = "糖精";
$this->toppings = array("鸡蛋","开心果");
}
}
?>
这样整个工作就完成了。好吧,订购两个不同地区的面包来尝一下吧。
$b =new GDBreadStore();
$b->orderBread("BlackBread");
$b =new HNBreadStore();
$b->orderBread("BlackBread");
如果你将两个简单商店类的creaderBread设立成静态的那就更简单,大家可以自己测试一下。
下面是显示结果:
正在准备广东黑面包
揉入酵母面团...
加入调料糖精...
加入配料... 鸡蛋
烘烤10分钟
切成圆形
放入盒子
正在准备湖南黑面包
揉入酵母面团...
加入调料辣椒酱...
加入配料... 鸡蛋
烘烤10分钟
切成圆形
放入盒子
是不是很简单。再回头来看看测试代码
$b =new GDBreadStore();
$b->orderBread("BlackBread");
$b =new HNBreadStore();
$b->orderBread("BlackBread");
我建立两个店,然后订购。非常的复合我们自身在生活中的习惯。
ok。如果上面的都明白了。那你也明白了什么叫做工厂方法模式。
他和简单工厂很相似。所以很多人混为一谈!但他是通过子类来决定建立什么样的面包。而简单工厂是有其他的类来确定建立什么样的面包。
下面看看我们的面包加盟店的类图。可能这个关系更清楚一点:
解释都写在图上。就不啰嗦了=。=
定义我们的工厂方法模式:
定义一个创建对象的接口,但是具体对象有它的子类来决定。也就是工厂方法将对象实例化推迟到了子类
具体的工厂方法的类图
工厂方法让具体的对象解脱了出来。并不再依赖具体的类。而是抽象。
它正式是复合OOP设计中非常重要的一个原则:
依赖倒置原则(经常听到,却不明白到底什么意思)
定义:
依赖抽象编程。而不要依赖具体编程
看起来这个原则和上面的
针对接口编程,不针对实现编程
这个原则非常的相似。但是有本质区别的
他更注重的是依赖抽象。
就像上面的面包店。本身面包店BreadStore他不能决定生产什么样的面包,他只能依赖他的底层子类来决定。
面包本身(各种面包)也不能依赖自身去决定怎么样去切片怎么样去装箱。只能依赖他的抽象类Bread。
这个原则说明了:不要让高层组件依赖于底层组件。而是不管高还是底,都应该依赖抽象对象。
换句话说。如果这个例子里所有的面包对象都是依赖面包这个具体类而建立的,就是违反了这个原则,就导致了解耦的失败。
依赖倒置原则到底倒置了哪里呢?
先想想看,你要开一个面包店会怎么考虑?
A:为了能够烘烤,切片,装到盒子里给客户,我必须先准备好不同种类的面包。黑的白的,甜的或者果仁的。甚至更多。
这样是没错的,但是这样就依赖各种面包的具体类。也就是这是从高层开始依赖。而违反了这个原则。如果我倒过来呢?
A:先提供一个抽象接口,他告诉我可以提供这些面包,而我不用担心会如何制作。
很好。这样就不用理会那些具体的面包类了。剩下的就是如何去开面包店了。思维~~~倒过来了。从底层开始往上面思考。并且依赖抽象。而不是具体的类了。
下面几个原则来告诉你如何做到尽量不要违反这个原则:切记!是尽量不要违反。而不是任何地方都不违反。这和OO设计原则一个道理的!不可能完全遵循OO设 计原则而增加自己的工作两。设计原则和设计模式只是为了解决问题而存在,而不是产生问题。如果你连建立一个字符对象都用到此原则说明你有问题了。你应该思 考一下这个对象将来是否会发生什么改变,如果不会发生改变,不遵循任何原则也没什么影响。
变量不可以持有具体的引用!
(用new关键词就是在次优具体的引用了。尽量用工厂模式中的一种避免它)
不要让类派生自具体类
(如果派生自具体类,你就会依赖具体类。请派生自一个抽象)
不要覆盖基类中已经实现的方法
(如果覆盖基类已经实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该由所有的子类共享)
你不可能完全遵守这些方针。你应该尽量达到这个原则。而不是随时都遵守这个原则。自己慢慢体会吧=。=
让我们进入真正的抽象工厂模式吧。
现在我们需要进行原料控制,因为面包加盟店有些可能会偷工减料。所以我们给其指定原料工厂提供原料。
先准备一下原料工厂
interface MaterialFactory{
public function creatDouch();
public function creatSauce();
public function creatToppings();
}
/**
* 各个地区的原料工厂
*/
class HNMaterialFactory implements MaterialFactory{
public function creatDouch(){
return new FermentDouch();
}
public function creatSauce(){
return new ChiliSauce();
}
public function creatToppings(){
return new setToppings(new Egg(),new NewChiliSauce(),new Jam());
}
}
class GDMaterialFactory implements MaterialFactory{
public function creatDouch(){
return new NotFermentDouch();
}
public function creatSauce(){
return new Saccharin();
}
public function creatToppings(){
return new setToppings(new Egg(),new NewSaccharin(),new Jam());//作为示例,将这里写死也没什么影响
}
}
上面的原料工厂里也看到了。每个原料都来至不同的原料对象。下面是对原料进行简单的定义
<?php
/**
* 存储各种各样的原材料,当然,每种原料都是依赖的接口
*/
/**
* 各种原材料的接口
*
*/
interface Douch{
public function getDouch();
}
interface Sauce{
public function getSauce();
}
interface Toppings{
public function getToppings();
}
//面粉团集群
class FermentDouch implements Douch {
protected $douch;
public function __construct(){
$this->douch = "发酵面包";
}
public function getDouch(){
return $this->douch;
}
}
class NotFermentDouch implements Douch {
protected $douch;
public function __construct(){
$this->douch = "不发酵面包";
}
public function getDouch(){
return $this->douch;
}
}
//酱汁集群
class ChiliSauce implements Sauce {
protected $sauce;
public function __construct(){
$this->sauce = "辣椒酱";
}
public function getSauce(){
return $this->sauce;
}
}
class Saccharin implements Sauce{
protected $sauce;
public function __construct(){
$this->sauce = "果酱";
}
public function getSauce(){
return $this->sauce;
}
}
//作料集群
class setToppings implements Toppings {
protected $toppings;
public function __construct(Toppings $_egg,Toppings $_sauce,Toppings $_jam){
$this->toppings=array($_egg->getToppings(),$_sauce->getToppings(),$_jam->getToppings());
}
public function getToppings(){
return implode(",",$this->toppings);
}
}
class Egg implements Toppings {
protected $name;
public function __construct(){
$this->name = "鸡蛋";
}
public function getToppings(){
return $this->name;
}
}
class NewChiliSauce implements Toppings {
protected $name;
public function __construct(){
$this->name = "苏丹红辣椒酱";//辣飞你
}
public function getToppings(){
return $this->name;
}
}
class Jam implements Toppings {
protected $name;
public function __construct(){
$this->name = "高级果酱";
}
public function getToppings(){
return $this->name;
}
}
class NewSaccharin implements Toppings {
protected $name;
public function __construct(){
$this->name = "加量糖精";
}
public function getToppings(){
return $this->name;
}
}
?>
原料准备完毕。那如何将这个原料提供给具体的客户呢?
//在湖南开店
class HNBreadStore extends BreadStore{
public function creatBread($_bread){
$material = new HNMaterialFactory();
if(is_string($_bread)){
//类型判断是必须的。php是弱类型语言。
if($_bread == "BlackBread"){
$bread = new BlackBread($material);
$bread->setName("湖南口味的黑面包");
}elseif ($_bread == "WhiteBread"){
$bread = new WiteBread($material);
$bread->setName("湖南口味的白面包");
}elseif($_bread == "SweetBread"){
$bread = new SweetBread($material);
$bread->setName("湖南口味的甜面包");
}elseif($_bread == "NutsBread"){
$bread = new NutsBread($material);
$bread->setName("湖南口味的果仁面包");
}else return null;
}
return $bread;
}
}
//在广东开店
class GDBreadStore extends BreadStore{
public function creatBread($_bread){
$material = new GDMaterialFactory();
if(is_string($_bread)){
//类型判断是必须的。php是弱类型语言。
if($_bread == "BlackBread"){
$bread = new BlackBread($material);
$bread->setName("广东口味的黑面包");
}elseif ($_bread == "WhiteBread"){
$bread = new WiteBread($material);
$bread->setName("广东口味的白面包");
}elseif($_bread == "SweetBread"){
$bread = new SweetBread($material);
$bread->setName("广东口味的甜面包");
}elseif($_bread == "NutsBread"){
$bread = new NutsBread($material);
$bread->setName("广东口味的果仁面包");
}else return null;
}
return $bread;
}
}
重点在
$material = new HNMaterialFactory();
$bread = new BlackBread($material);
这里重写了新建不同面包的对象方法。
$material就是原料工厂。告诉$bread从这个原料工厂里提取原料。这样保证完成正确的面包种类。
面包的抽象类并没有发生任何改变:
/**
* 面包店类。开店咯
*/
abstract class BreadStore{
private $bread;
public function orderBread($_type){
//调用自身的抽象方法
$orderBread = $this->creatBread($_type);
$orderBread->prepare();
$orderBread->toString();
$orderBread->bake();
$orderBread->cut();
$orderBread->box();
}
abstract function creatBread($_type);
}
决定生产什么样的面包还是由各加盟店的子类来决定的。
下面是各种面包的定义
class BlackBread extends Bread {
protected $factory;
public function __construct(MaterialFactory $_factory){
$this->factory = $_factory;
}
public function prepare(){
$this->name = "正在准备".$this->name;
$this->douch = $this->factory->creatDouch();
$this->sauce = $this->factory->creatSauce();
$this->toppings = $this->factory->creatToppings();
}
}
class WhiteBread extends Bread{
public function __construct(MaterialFactory $_factory){
$this->factory = $_factory;
}
public function prepare(){
$this->name = "正在准备".$this->name;
$this->douch = $this->factory->creatDouch();
$this->sauce = $this->factory->creatSauce();
$this->toppings = $this->factory->creatToppings();
}
}
class SweetBread extends Bread{
public function __construct(MaterialFactory $_factory){
$this->factory = $_factory;
}
public function prepare(){
$this->name = "正在准备".$this->name;
$this->douch = $this->factory->creatDouch();
$this->sauce = $this->factory->creatSauce();
$this->toppings = $this->factory->creatToppings();
}
}
class NutsBread extends Bread {
public function __construct(MaterialFactory $_factory){
$this->factory = $_factory;
}
public function prepare(){
$this->name = "正在准备".$this->name;
$this->douch = $this->factory->creatDouch();
$this->sauce = $this->factory->creatSauce();
$this->toppings = $this->factory->creatToppings();
}
}
这里和上面的工厂方法是有区别的。大家可以自己翻上去查看。
重点改变是在新建这些面包种类的实例对象的时候在构造函数中引进了原料工厂。
真正起到作用的就是这里。
public function __construct(MaterialFactory $_factory){
$this->factory = $_factory;
}
准备工作中只是在准备,并不知道他准备的是什么口味的。所以弱耦合还是存在,并没破坏的。
由什么源材料决定什么样的面包。这是很自然的道理。
现在一切都有,只欠东风了。
$b =new GDBreadStore();
$b->orderBread("BlackBread");
$b =new HNBreadStore();
$b->orderBread("BlackBread");
输出其实和上面的一样。只是工作模式不同
正在准备广东口味的黑面包
不发酵面包
果酱
鸡蛋,加量糖精,高级果酱
烘烤10分钟
切成圆形
放入盒子
正在准备湖南口味的黑面包
发酵面包
辣椒酱
鸡蛋,苏丹红辣椒酱,高级果酱
烘烤10分钟
切成圆形
放入盒子
上面的工厂方法和简单工厂理解了的这个正式的工厂模式其实就非常容易理解了。。只是方式不同而已。可能细心的朋友已经发现了。工厂方法是蕴藏在抽象工厂模式中。
通过抽象工厂所提供的接口,可以创建产品的家族。利用这个接口书写代码,我们的代码将从实际工厂解耦,以便在不同上下文中实现各种各样的工厂,制造出各种不同的产品。例如,不同的区域,不同的操作系统,不同的外观以及操作。
因为代码从实际的产品中解耦了,所以我们可以替换不同的工厂来取得不同的行为(例如,取得番茄酱料,而不是取得大蒜酱料);
好了。让我们来定义抽象工厂模式吧:
提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。
抽象工厂允许客户使用抽象的接口来创建一组相关的产品。而不需要知道(或关心)实际产出的具体产品是什么。这样一来,客户就从具体的产品中被解耦。
类图。。。。。如下。。
画得比较乱。。。呃。。其实我已经理解了。只是不知道怎么用我这个软件话出来。。不能画折线。囧
至于为什么抽象工厂的方法经常以工厂方法的方式实现。。截一段深入潜出设计模式中的说明吧。
抽象工厂的任务是定义一个负责创建一组产品的接口。这个接口内的每个方法都负责创建一个具体的产品,同时,我们利用实现抽象工厂的子类来提供这些具体的做法。所以,在抽象工厂中利用工厂方法实现生产方法是相当自然的做法。
一下来区别工厂方法和抽象工厂之间:
工厂方法是使用类,而抽象工厂是操作的对象。
工厂方法创建对象的方法是继承,抽象工厂创建工厂的方法是组合。
抽象工厂是通过抽象方法(MaterialFactory),由这个类型的子类定义产品被产品的方法。但必须先实例化。然后将他传入一些针对抽象类型所写的代码中
工厂方法是直接通过抽象方法的子类决定如何创建产品。不需要在外部再实例化产品传入具体代码。
不管是使用工厂方法还是抽象工厂,都可以将对象创建封装起来,使应用程序解耦,并降低对特定实现的依赖。
抽象工厂是在需要创建产品家族和想让制造的相关产品集合其来的时候使用。
工厂方法在把客户代码需要实例化的具体类中解耦或者如果目前不知道将来需要实例化那些具体类的时候使用。
相对而言进过个人经验来判断是使用哪一个工厂模式。工厂方法相当于轻量级的设计模式。简单处理就可以使用他,只需要把抽象方法继承成子类并实现这个工厂方法就可以了。
抽象工厂的范围更广泛一点。
定义和用法
PHP extract() 函数从数组中把变量导入到当前的符号表中。
对于数组中的每个元素,键名用于变量名,键值用于变量值。
第二个参数 type 用于指定当某个变量已经存在,而数组中又有同名元素时,extract() 函数如何对待这样的冲突。
本函数返回成功设置的变量数目。
语法
| 参数 | 描述 |
|---|---|
| array | 必需。规定要使用的输入。 |
| extract_rules |
可选。extract() 函数将检查每个键名是否为合法的变量名,同时也检查和符号表中的变量名是否冲突。 对非法、数字和冲突的键名的处理将根据此参数决定。可以是以下值之一: 可能的值:
|
| prefix |
可选。请注意 prefix 仅在 extract_type 的值是 EXTR_PREFIX_SAME,EXTR_PREFIX_ALL,EXTR_PREFIX_INVALID 或 EXTR_PREFIX_IF_EXISTS 时需要。如果附加了前缀后的结果不是合法的变量名,将不会导入到符号表中。 前缀和数组键名之间会自动加上一个下划线。 |
例子 1
$a = 'Original';
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c";
?>
输出:
例子 2
使用全部参数:
$a = 'Original';
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array, EXTR_PREFIX_SAME, 'dup');
echo "\$a = $a; \$b = $b; \$c = $c; \$dup_a = $dup_a;";
?>
输出:
php mt_srand 播下一个更好的随机数发生器种子
mt_srand
(PHP 3 >= 3.0.6, PHP 4, PHP 5)
mt_srand -- 播下一个更好的随机数发生器种子
说明
用 seed 来给随机数发生器播种。从 PHP 4.2.0 版开始,seed 参数变为可选项,当该项为空时,会被设为随时数。
例子 1. mt_srand() 范例
代码
// seed with microseconds
function make_seed()
{
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 100000);
}
mt_srand(make_seed());
$randval = mt_rand();
?>
注: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现已自动完成。
参见 mt_rand(),mt_getrandmax() 和 srand()。![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
PHP mt_rand() 函数
定义和用法
mt_rand() 使用 Mersenne Twister 算法返回随机整数。
语法
说明
如果没有提供可选参数 min 和 max,mt_rand() 返回 0 到 RAND_MAX 之间的伪随机数。例如想要 5 到 15(包括 5 和 15)之间的随机数,用 mt_rand(5, 15)。
很多老的 libc 的随机数发生器具有一些不确定和未知的特性而且很慢。PHP 的 rand() 函数默认使用 libc 随机数发生器。mt_rand() 函数是非正式用来替换它的。该函数用了 Mersenne Twister 中已知的特性作为随机数发生器,它可以产生随机数值的平均速度比 libc 提供的 rand() 快四倍。
提示和注释
注释:自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现在已自动完成。
注释:在 3.0.7 之前的版本中,max 的含义是 range 。要在这些版本中得到和上例相同 5 到 15 的随机数,简短的例子是 mt_rand (5, 11)。
例子
在本例中,我们会返回一些随机数:
echo(mt_rand());
echo(mt_rand());
echo(mt_rand(10,100));
?>
输出类似:
513289678
35
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
注意:上面这个函数给出的随机整数,数字之外的字符是不会出来的,若想其他字符产生,就需另自定义方法,如下所示:
代码
/*
* $length:随机数字符串的长度
* $type:产生随机数的类型
* */
function random($length, $type = "") {
$chars = !$type ? "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz" : "0123456789abcdef";
$max = strlen($chars) - 1;
mt_srand((double)microtime() * 1000000);
for($i = 0; $i < $length; $i++) {
$string .= $chars[mt_rand(0, $max)];
}
return $string;
}
$var=random(32,'haha');
echo($var);
?>
输出:


