从毕业到现在已经快7年的开发经验了。做过基础用户系统,积分商城,偷菜游戏,论坛,博客等;我自己也开发了一个全栈在线视频网站(http://sishuok.com/),也开发过几万、几十万、几千万、几亿不同数量级的系统,踩了很多坑,学到了很多经验。我设计了一些系统,我有自己的看法。我个人认为系统的设计应该根据场景和时间而有所不同。一个系统不可能一下子设计得完美无缺。在资源有限的情况下,首先要解决最核心的问题。问题,并预测/发现未来可能出现的问题,一步步解决最痛苦的问题。也就是说,系统设计是一个不断迭代的过程,问题是在迭代中发现和修复的;即满足要求的系统是不断迭代优化的,架构不是一下子就完美的。这是一个连续的过程。个人不要相信黑客架构的灵丹妙药。但是,如果一开始就有一个良好的基础系统设计,那么日后更容易达到更令人满意的目标。在设计系统时,应该多考虑一下墨菲定律:1.没有什么事情像看起来那么简单;2.一切都会比你预想的要花更长的时间;3.会出错的事情总会出错;4.如果你担心某事的发生会使它更有可能发生。但我们还必须考虑80/20法则。在系统设计的早期阶段,有限的资源得到了明智的利用。我们的目标是系统既能满足现有需求,又能支持未来需求。在系统不断开发的过程中,前辈们也总结了很多设计原则/经验;而且我也很幸运地使用了一些经验/原则。设计原则是在系统开发初期或演化过程中根据自己系统的特点来搭配使用的。如果一开始不是核心问题,请不要把系统设计复杂化。在系统设计初期,拆分需要权衡是构建一个大而全的系统,还是按功能拆分系统;资源有限,无需拆分系统(如拆分产品、拆分订单等),只需要搭建一个大而全的系统。比如在设计京东秒杀系统的时候,预计一旦上线,体量会非常大,投入的资源也相当充足。在这种情况下,就需要考虑按功能拆分系统。笔者遇到的拆分主要有以下几种情况:系统维度:按照系统功能/业务拆分,如商品系统、购物车、结算、订单系统等;功能维度:拆分一个系统的功能,比如优惠券系统可以拆分为后台优惠券创建系统、优惠券领取系统、优惠券使用系统等;读写维度:根据读写比的特点拆分,比如商品系统,交易的每个系统都会读,读大于写,所以可以拆分为:商品写服务,商品读服务;读服务可以考虑全缓存提高性能;比如写入量太大,需要考虑分库分表;详情页请考虑数据异构拆分系统,将分散在多个地方的数据聚合到一个存储中,提高读取的性能和可靠性;AOP维度:根据访问特性,按照AOP进行拆分,比如商品详情页,可以分为CDN、页面渲染系统;CDN是一个AOP系统;模块维度:如按基础或代码维护特性拆分,如基础模块:分库分表、数据库连接池等;而一般的代码维护是按照三层架构(Web、Service、DAO)来划分的。服务化首先判断是否只需要简单的单点远程服务调用。如果单机无法应对,需要集群,是否可以通过在客户端注册多台机器,使用Nginx做负载均衡来解决;当调用者越来越多时,需要考虑使用服务的自动注册和发现(比如Dubbo使用zookeeper);还要考虑服务的分组/隔离。不同的调用者提供不同的服务组来隔离访问;后来随着调用量的增加,其他的考虑比如服务限流,黑白名单等等。还有一些细节需要注意,比如超时、重试机制、服务路由(可以在不同组之间动态切换)、故障补偿等,这些都会影响服务质量。总结为进程内服务--->单点远程服务--->集群手动注册服务--->自动注册和发现服务---->服务分组/隔离/路由---->限流/黑白名单。数据版本控制,可以回滚。设计时考虑是否需要数据版本控制,数据维护问题是否需要回滚。例如,产品维护是否需要版本控制?我们目前有一些非常重要的系统需要对数据进行版本化和支持回滚。整体设计类似于下图的设计:流程可以定义如果你接触过保险业务,你会发现不同的保险理赔服务是不一样的,所以我们在设计的时候设计了一套理赔流程服务系统。核保流程和理赔流程分离后关联,部分理赔流程可以重复利用,提供个性化的理赔流程。状态和状态机在设计一个交易订单系统的时候,会有正向状态(待支付、待发??货、发货、完成)和反向状态(取消、退款)等,正向和反向状态应该根据你的自己的系统特性决定了是否需要单独存储。另外,订单状态有变化,如待付款、已付款、待收货、迁移完成;需要考虑是否需要使用状态机来驱动状态变化和后续的流程节点操作。还要考虑并发状态修改的问题,同一个订单同一时刻只有一次修改;状态变化的有序问题,状态变化消息的先到先得问题,比如支付成功消息和用户取消消息的时间差。消息队列消息队列用于解耦一些不需要同步调用的服务或者订阅一些系统关心的变化;使用消息队列可以实现服务解耦(一对多消费)、异步、缓冲(调峰)等。比如电商系统中的交易订单数据,被很多系统关注和订阅,比如订单生产系统、定时发货系统、订单风控系统等;如果订阅者太多,那么订阅单个消息队列就会成为瓶颈,需要考虑消息队列的多个镜像副本。大流量缓存持久化当电商搞大促的时候,此时的系统流量会比正常流量高几倍甚至几十倍。这时,必须进行一些特殊的设计,以保证系统能够顺利度过这个时期;但是解决方案有很多,一般都是以牺牲强一致性为代价,但是保证最终一致性。比如扣除库存,可以考虑这样的设计:直接在Redis中扣除,然后记录扣除日志,通过Worker同步到DB。还有一个交易订单系统,可以这样设计:首先结算服务调用订单接收服务将订单存储在:订单Redis和订单队列表中,订单队表可以根据需求层面,通过queuebuffertable提高订单接收,然后通过同步Worker同步到订单中心表;假设用户已经为订单付款,订单状态机将驱动状态变化。此时订单队列表中的订单可能还没有同步到订单中心表中。这时,状态机必须重试。如果用户查看单个订单的明细,可以直接从订单Redis中查看;但是如果他查询订单列表,他需要考虑订单Redis和列表的结合。在设计同步Worker时,需要考虑并发处理和重复处理的问题,是单机串行扫描处理(每个Worker只扫描一部分表)还是集群处理(Map-Reduce),以及是否需要添加到订单队列表中。相关字段:处理器(正在处理哪个应用程序)和处理状态(正在处理、已处理、处理失败)。数据校对在使用消息异步机制的场景下,消息可能会丢失,需要考虑数据校对和纠错,保证数据的一致性和完整性。worker可以周期性扫描原表进行补偿,扫描周期根据实际场景定义。数据异构化订单分库分表,一般是按照订单ID进行分表,所以如果要查询一个用户的订单列表,需要聚合N个表的数据返回,会导致低读取订单表的表现;这时订单表需要异构,一组用户订单表是异构的,按照用户ID分库分表;此外,还需要考虑对历史订单数据进行归档。还有一种异构场景,比如商品详情页,因为数据源太多,影响服务稳定性的因素太多,所以最好的方式就是把用到的数据异构存储,形成数据闭环;提高服务性能和稳定性。然而,一些数据的异质性意义不大。例如,股票价格可以异步加载或与并发请求相结合。后台系统操作可以反馈在我接触过的很多系统中,很多场景都需要反馈,比如修改了一些内容,想预览最终效果,也就是想得到一些反馈;有些是规则系统,我希望在系统中看到这些规则。系统数据下的反馈。因此,请在设计后台系统时考虑可预览和可反馈的效果。后台系统审批需要对一些重要的后台功能进行审批流程设计,比如调整价格、日志操作等,确保操作可追溯、可审计。反重设计,比如结算页面,需要考虑重复提交,需要在后续订单扣库存时防止重复扣库存。解决方案可以考虑反重KEY和反重table。在某些场景下,比如重复支付,如果某些电商网站同时支持微信支付和京东支付,则无法避免不同渠道的重复支付,但在系统设计时需要记录每一种支付情况.比如下图是我在京东上用京东支付和微信支付模拟重复支付后退款的支付明细:幂等设计在交易系统中经常用到消息,现有的消息中间件基本不保证一定会有不重复消费消息;因此,业务系统在重复消费消息时需要考虑幂等处理。另外,在使用第三方支付时,第三方支付会进行异步回调,所以还要考虑回调的幂等处理。【本文为专栏作家张凯涛原创文章,作者微信公众号:凯涛的博客(kaitao-1234567)】
