Category Archives: Architecture

“手动干预排序”的设计与实现

你的记录一般会按某个业务字段排序,比如ID,生成时间等等。 在论坛或SNS系统中,你可能要允许手工调整记录间的排序,比如“置顶一个帖子”,“下沉某个帖子让人感觉它是一个月前发表的”等等。 在设计社区系统的数据库时,你首先要有意识地考虑下某个记录列表是否需要手工排序。如果需要,你应该使用一个专门的字段来解决这个问题。 ============================= 下面就说下这个字段(假设它叫 sort_order)相关的设计与实现: 1. 考虑到比较式分页的需求,这个字段的值要像ID一样不能重复。你可以用数据库的序列生成器生成这个字段,也可以像下面将要说的那样,根据一定的编码规则生成这个字段。 2. 如果你设计的是社区系统,那么你的记录的原始排序规则很有可能是“发表时间”;这时“手动干预排序”可能相当于“让人感觉某个帖子是另一个时间点发表的”。 因此,sort_order最好体现时间信息。我的做法是:sort_order = 记录创建时间的毫秒数 串接  3位随机数。 这样基本可以保证唯一,而且携带时间信息。你会问为什么不直接用纳秒数?因为系统生成纳秒数需要1毫秒以上的时延,对性能伤害很大。

通过异步机制解耦主干流程和附加流程

在web2.0系统中,用户的一次写操作,可能不仅仅意味着系统里要新增一条相应的记录,系统里可能还有很多统计性的事情要做。 比如在社区系统中,一个用户发了一个帖子,系统除了生成这个帖子的记录之外,还要给用户的发贴数加1,用户使用过的tag数加1,每个tag对应的帖子数加1 。。。 可以把主流程(生成帖子记录)和附加流程(各种加1)整合在一个事务里执行,程序健壮且易理解。但也可以在完成主流程后,发一条消息出去,让消息的消费者完成这些统计操作。 这种 异步机制最明显的好处,就像很多人说的那样,可以 尽快响应用户请求,提升性能。 但更重要的好处是, 它实现了解耦,有利于各模块独立发展,并促进分工。 1. 把附加流程实现在主流程的模块里,不符合主模块的职责定位; 主模块可能要因此依赖很多附加模块的库,不利于系统的简洁,对发布也有影响。 2. 把附加流程实现在主流程的模块里,不利于主模块和附加模块的开发人员各司其责,代码也容易遗漏或出错。 由于异步消息的不可靠性,这样做也有它的副作用: 1. 消息可能会丢失,导致附加流程未能执行。 要解决这个问题,只有使用不丢消息的消息中间件:这个中间件只有在收到消费者的回执后才会删除消息。 2. 消息可能重复,导致附加流程执行两次。如果你的附加流程不是统计性质,则让消费者做好幂等性就好了; 如果是统计类操作的话,那就要另建去重措施了。如果对统计数值的准确性要求不高,可以用bloom filter简单搞定; 否则的话,需要在缓存里保存一段时间内的消息ID,收到消息后做下比对再决定要不要消费它。

如果你的系统既有web界面,又要暴露remote api,应该怎么分层?

如果你的系统既有web界面,又要暴露remote api,应该怎么分层? 我的回答是: 1. 如果web操作为主,remote api是少数,那就用最常见的web-biz模式;web层不必那么薄,可以通过组装biz services实现use cases 2. 如果remote api为主,web操作为辅,则应该用web-app-biz模式,并且web层不准直调biz层。 ============================================== 这张图好理解,也比较容易接受:app层组装biz service并实现校验逻辑,对外暴露服务;web层本身很薄,主要职能是把请求转给app层,校验都不用做。 可能存在的争论是:为什么要禁止web层直调biz层? 两层在同一个系统里(比如同一个JVM里),直调无障碍; 禁止直调的话,如果一个操作不作为remote api对外暴露,仍要在app层中加一层封装的代码供web层调用,岂不是很蛋疼? 这些坏处确实是存在的。但是经验表明,禁止直调带来的好处更多: 1. 系统做大后,出于分工或性能考虑,可能需要把web层剥离为一个独立系统。 如果你禁止直调,分分钟可以完成剥离; 否则,几乎不可能,除非你直接把biz service暴露成remote api(非常不好的做法)。 2. web层直调省不了多少工作量。问自己这样一个问题:校验及报错在哪里做? 非直调的话,校验做在app层,并使用标准的errorCode/errorMessage API返回错误,web层自己不必再校验; 直调的话,web层需要自己做一堆校验,工作量还是有的。 3. 直调时,web层要自己实现校验逻辑和biz service组装逻辑,直接使用domain objects, 这要求web层的开发者也要非常非常清楚业务逻辑;如果不允许直调,web层开发者只须组装request对象和渲染response对象,他可以把精力更多地放在javascript/css上,对业务逻辑只需有所了解即可;也就是说,web层的开发可以轻松外包(对于资源紧张的团队,这一点很有意义) 4. 最严重的问题在于,如果允许直调,对于某些use case,web层和app层可能会实现重复的校验逻辑和biz service组装逻辑。不要小看这些逻辑,可轻可重; 如果没有实现在单一的地方,系统很容易腐化 (违反DRY原则) ========================================== 禁止直调除了要作为开发规范让大家遵守,也应该在技术上作好防卫性设计。如果你用的是maven,可以这样: <!–web工程的pom.xml–> <dependency> <groupId>some.group</groupId> <artifactId>app</artifactId> </dependency> <dependency> <groupId>some.group</groupId> <artifactId>app-impl</artifactId> <exclusions> <exclusion> <groupId>some.group</groupId> <artifactId>biz</artifactId> […]

小技巧:从多个数据源中取出前10条记录

考虑这样的场景:论坛里有个页面,混合展示所有的精华贴和热门贴; 帖子按ID逆序排列,并且要有分页。 怎么写出一个符合这种要求的SQL? 受限于你的数据库设计和性能约束,这样的SQL可能很难写。 有种办法是:先取出10条精华贴,再取出10条热门贴,然后再在内存里按ID逆序排列并去重,最后返回前10条。 这样做不需要做复杂的SQL查询,性能也能接受。不过,这种模式只适于 比较式分页(ID小于某个值的前10个帖子),不适应于普通的页码式分页(第N页)。  考虑一下取第二页的场景,你就明白我的意思了。

消息中间件可能重发消息,怎么办?

消息中间件可能重发消息,怎么办?只能要求客户端保证幂等性了。 但这又增加了客户端的复杂性,人家可能不愿意。 其实解决办法很简单(也很流氓),你自己做个客户端SDK,在这里面做好消息去重,然后让你的客户集成它就是了。

有必要搞pipeline之类的东西吗?

有必要搞pipeline之类的东西吗? 把各块功能封装成独立的API,然后每个request handler分别调用不同的api组合不就行了吗?这样还免得搞框架,也不用写什么配置文件。 这种质疑不是没有道理,但最近的亲身体会表明,pipeline体系在开发效率、代码正确率上有确有一定优势。 比方说,要加一个切面功能的话,Pipeline模式中开发者要在各个request handler的xml配置中加上一行,API模式中开发者要修改各个 request handler类。 比较: 1. 在xml中加配置肯定比在各个类中加代码更快 2. 在xml中加不易出错,而在各个类代码中改相对容易出错     a. java代码在结构上不如xml文件清晰,插错地方的可能性比较高     b. 在java代码中新增API调用语句前要先收集输入,如果插入点的上下文没有直接可用的变量作为输入,则需要重构代码,引发bug隐患; 在xml中配置没有这个问题,因为pipeline体系中强制规定了输入变量应从pipeline context中获取,总是能获取到的。     c. 在java代码中调用API后要根据输出和异常决定下一步怎么走,这可能导致比较大的流程变更,而且不同的request handler需要做不同的变更,这非常容易出错; 在xml中配置没有这个问题,因为pipeline体系各valve自己已经决定了下一个出口,不需要调用者操心。 总结一下,pipeline体系中的处理结点是特殊的API,它们有高度统一的接口和更高的自恰性,组装起来比普通API更容易,所以应对功能变更的能力也更强。

为什么要做模块分拆?

为什么大类要拆成小类?为什么要把系统拆成多个模块? 以下列举出的原因都是老生常谈,对很多人来说都是直觉的东西; 直觉的坏处是:如果它成直觉了,你反而说不出它有什么好处了,当争论来临时,你就无法说服你的同事了。 1. 大变小,能使部分功能可重用。如果所有功能的揉在一起,那一般就没有专们针对某小块功能的接口,外界无法直调这块功能,也就无法重用。 2. 模块拆分,实际上强制实施了封装性,使得模块之间只能通过有限的接口互相访问,从而降低耦合。耦合的意思是:一样东西变时,另一样东西不必变;拆分后,一个模块的变动如果不影响接口,其它的模块就不用变; 如果未拆分,一个功能可能全身都是接口,每处都有可能被其他功能依赖,自己任意一个小变都会导致其他模块跟着变。 3. Separation of Concern, 即分离关注点,每个团队、每个人只需要关注、维护系统的一部分模块或一个子系统。这在大中型组织中非常重要,对开发效率的影响是全方位的。它可以降低开发人员的系统知识门槛,并使得每个模块都有相应的精熟者,对开发效率的影响不言而喻。 4. 最后,模块拆分对开发环境、配置管理、运行维护也很重要。如果一个代码库过大,svn checkout一下,编译一下,可能半小时就过去了; 如果系统未做拆分,一个哥们提交了一行错误代码导致编译错误,会使得整个团队都无法继续开发; 如果整个应用作为单个进程运行,那么某块功能一挂,整个应用都挂,比如某个垂直频道压力过大,导致进程异常退出,那么压力不大的其他频道也没法访问了。

长连接交互中的连接恢复与异常处理

1. 客户端应该有自动重连机制,或者,在需要发送业务数据前如果发现连接已断开,则重连。 2. 有一种极端的情况是,服务端出问题关闭连接,客户端立即重连,由于服务端问题还没修复,客户端重连上后马上又断开。。。如果并发很高的话,服务端会不堪重负。 解决办法是客户端应逐渐重连间隔,第二次重连在第一次5秒后,第三次在10秒后,第N次在1分种后。。。 3. 服务端出现异常如何处理?如果确定当前处于request/reply语境下,则应该回送当前操作相应的错误报文(就像web服务器的http 500); 否则,应该关闭连接。为什么这时要关闭连接?    a. 如果你写一个错误报文给客户端,那么客户端可能并不知道这个错误针对的是哪个请求,因为客户端可能并没有主动请求,是服务端主动发下行消息时才出的错。    b. 如果你不关闭连接,什么也不干,客户端可能会陷入无限等待,因为当前可能正处于request/reply语境下,服务端却不知道。 比如,netty handler中执行到exceptionCaught()方法时,并不知道自己处于哪种语境下。

异步程序的难搞之处

1. 任务完成的顺序与代码被执行的顺序未必相同。 先调用asyncA(), 再调用asyncB(),但最后可能是B任务率先完成。 2. 有时候不知道代码走到当前此处的根本原因,给调试带来麻烦。 同步程序中,总是可以在调用栈的底部找到触发当前操作的原因,而异步程序中,走到这里可能是因为另一个线程做了什么操作,修改了公有变量,导致本线程走到这里; 那么是哪个线程做什么操作,需要对代码非常熟悉才知道。 3. Callback中出了异常,主线程往往不知道,写在主线程代码里的异常处理机制没被触发。 待续 。。。

服务端开发中的两种“异步”

服务端开发时经常会用到“异步”这个概念。要注意你说的是哪种异步,否则可能会导致沟通的误会,甚至设计的失误。 1. 客户端和服务端之间的异步交互。 客户端发出一个业务请求,服务端在这个业务请求完成之前就发出响应,然后再在后台进行处理。 2. 服务端处理请求时,在内部使用异步模式。 比如收到请求时将它丢给一个线程池来处理。 请求处理中可以同时使用这两种异步,也可以只用一种。 对于http来说,一般是“交互要同步”,但“服务端内部处理要异步”。I/O线程收到请求后,立即丢给worker线程池处理;但在worker线程处理完请求、回送响应之前,客户端要处于阻塞状态,等待响应;服务端内部使用异步处理,对客户端并没有什么好处,但它可以提高服务端自身的吞吐率。 一个常见的失误就是,一味想着“异步”,结果把一些本该同步的c/s交互也变成了异步。 最近我在处理websocket握手逻辑时就遇到这样一个问题。我们基于Netty在握手时进行了用户验证。Netty的握手API采用异步编程模型,于是我们就把验证的逻辑放到了握手API的callback中;结果 验证逻辑还没执行完,客户端就收到“握手成功”的通知,然后发出正常的业务请求。解决办法是先验证,再执行握手;如果验证逻辑采用了异步编程模型,则可以把握手API放到验证逻辑的callback中。