深入讲解交易系统设计原则——高并发原则

1.高并发原则

1.1无状态

如果设计的系统是无状态的,那么应用比较容易进行水平扩展。实际生产环境可能是这样的:应用无状态,配置文件有状态。例如,不同的机房需要读取不同的数据源,此时,就需要通过配置文件或配置中心来指定。

1.2拆分

在系统设计初期,是做一个大而全面的系统还是按照功能模块拆分系统,这个需要根据环境进行权衡。比如做一些交易量不大的系统,我们就没必要对系统进行细化的拆分了。但是像京东、淘宝之类的秒杀系统,访问量很大,就得按照功能来拆分了。

我们可以按照以下的情况来进行拆分:

  • 系统维度:按照系统功能、业务进行拆分,例如商品系统、购物车、交易、订单系统。
  • 功能维度:对一个系统进行功能再次拆分,比如,优惠券系统可以拆分为后台券发放系统、领券系统、消费券系统。
  • 读写维度:根据读写比例进行拆分。例如,商品系统,交易的各个系统都会读取数据,读的量大于写,这时可以拆分成商品写服务;读服务使用缓存来提高效率;写的量比较大时,可以考虑分库分表;有些聚合读取的场景,如商品详情页,可以考虑数据异构拆分系统,将分散在多出的数据聚合在一处,以提升系统的性能和可靠性。
  • AOP维度:根据访问特点,按照AOP进行拆分,例如,商品详情页可以分为CDN(也叫作AOP系统)、页面渲染系统。
  • 模块维度:按照代码维护特征进行拆分,如基础模块分库分表、数据库连接池;代码结构一般按照三层架构(Web、Service、DAO)来设计

1.3服务化

首先,判断是不是只需要简单的单点远程服务调用,单机不行集群是不是就可以解决呢?在客户端注册多台机器并使用nginx进行负载均衡是不是可以解决呢?随着调用方越来越多,应该考虑使用服务自动注册和发现(Zookeeper和Dubbo)。其次,还要考虑服务的隔离,如果有的系统模块访问量很大,有可能会把整个系统拖垮,所以我们还要为不同的调用方提供不同的服务分组,隔离访问。后期,随着调用量的增加还要考虑服务的限流、黑白名单等。还有一些细节要注意,例如超时时间、重试机制、服务路由、故障补偿,这些都会影响到服务的质量。

概括上面一段话:进程内服务——>单机远程服务——>集群手动注册服务——>自动注册和发现服务——>服务的分组/隔离/路由——>服务治理限流/黑白名单

1.4消息队列

消息队列用来解耦一些不需要同步调用的服务或者订阅一些自己系统关心的变化。使用消息队列可以实现服务解耦、异步处理、流量缓冲等。例如,在电商系统中的交易订单数据,该数据有非常多的系统关心并订阅。再例如,订单生产系统、订单风控系统等。如果订阅者太多,那么订阅单个消息队列就会成为系统瓶颈,此时,需要考虑对消息队列进行多个镜像复制。

使用消息队列是,还要注意处理生产消息失败,以及消息重复接收的情况。有些消息队列产品会提供生产重试功能,在达到指定重试次数还未生产成功时,会对外通知生产失败。这时,对于不能容忍生产失败的业务场景来说,一定要做好后续的数据处理工作,例如持久化数据要同时增加日志、报警灯。对于消息重复问题,特别是一些分布式消息队列,处于性能和开销的考虑,在一些场景下会发生消息重复接收,需要在业务层面进行防重处理。

1.4.1大流量缓冲

在电商搞大促时,系统流量会高于正常流量的几倍或几十倍,这是就要进行一些特殊的设计来保证系统的稳定性。解决的方法有很多,基本的规则大都是牺牲强一致性,从而保证最终一致性。

比如,扣减库存,可以这样设计:

上述过程,直接在Redis中扣减,然后记录下扣减日志,通过Worker同步到DB

再例如,交易订单系统,可以这样设计:

 针对上述图,首先,结算服务调用订单接单服务,将订单存储到订单Redis和订单队列表中,订单对列表可以按照水平扩展多个表,通过队列缓冲表提高接单能力。然后通过同步Worker同步到订单中心表;假设用户支付了订单,订单状态机就会驱动状态变更,此时,可能订单队列表的订单还没有同步到订单中心表,状态机要根据实际情况进行重试。

如果用户查看单个订单详情,那么可以直接从订单Redis中查到。但如果查询订单列表,则需要考虑订单Redis和列表的合并。

同步Worker在设计时,需要考虑并打处理和重复处理的问题,比如,使用单机串行扫描处理(每台Worker只扫描其中的一部分表)还是集群处理(MapReduce)。另外,需要考虑是否需要对订单队列表添加相关字段:处理人、处理状态、处理完成时间、失败次数等。

1.4.2 数据校对

在使用了消息异步机制的场景下,可能存在消息的丢失,需要考虑进行数据校对和修正来保证数据的一致性和完整性。可以通过Worker定期去扫描原始表,通过对业务数据进行校对,对有问题的数据进行补偿,扫描周期根据实际场景进行定义。

1.5 数据异构

1.5.1 数据异构

订单分库分表一般按照订单ID进行划分,如果要查询某个用户的订单列表,需要聚合多个表的数据后才能得到结果,这样会导致订单表的读取性能很低。此时需要对订单表进行异构,异构出一套用户订单表,按照用户ID进行分库分表。另外,还需要考虑对历史订单数据进行归档处理,来提升服务的性能和稳定性。而有些数据异构的意义不带,如库存价格,可以考虑异步加载,或者合并并发请求。

1.5.2 数据闭环

数据闭环如商品详情页,因为数据来源很多,影响服务稳定性的因素也就跟着多了。因此,最好的方法就是把使用到的数据进行异构存储,并形成数据闭环,具体步骤如下:

  • 数据异构:通过如MQ机制接收数据变更,然后原子化存储到合适的存储引擎,如Redis或持久化KV存储。
  • 数据聚合:这步是可选的。数据异构的目的是把数据从多个数据源拿过来,数据聚合的目的是吧这些拿到的数据做聚合,这样前端就可以一个调用拿到所有的数据,这些数据一般存储在KV存储中。
  • 前端展示:前端通过一次或少次调用拿到所需要的数据来展示。

这种方式的好处就是数据的闭环,任何依赖系统出现问题了,该部分系统还能正常工作,只是更新会有积压,但是不影响前端页面的展示。

另外,此处如果一次需要多个数据,那么可以考虑使用Hash Tag机制将相关的数据聚合到一个实例,如在展示商品详情页时需要商品基本信息和商品规则参数,此时就可以使用产品的ID来作为数据分片key,这样相同的产品ID的相关数据就会在一个实例中了。

数据闭环和数据异构其实就是一个概念,目的都是事先数据的自我控制,当其他系统出问题时可以做到不影响自己的系统,或者自己的系统出问题时不影响其他的系统。一般通过消息队列来实现数据的分发。

1.6 缓存银弹

缓存对于读服务器来说可谓抗流量的银弹。总结如下:

流程节点缓存技术
客户端

1.使用浏览器缓存

2.客户端应用缓存

客户端网络代理服务器开启缓存
广域网

1.使用代理服务器(含CDN)

2.使用镜像服务器

3.使用P2P技术

源站及源站网络

1.使用接入层提供的缓存机制

2.使用应用层提供的缓存机制

3.使用分布式缓存

4.静态化、伪静态化

5.使用服务器操作系统提供的缓存机制

 

 

 

 

 

 

 

 

 

  • 浏览器端缓存

设置请求的过期时间,如对响应头Expires、Cache-control进行控制。这种机制适用于对实时性不太敏感的数据,如商品详情页框架、商家评分、评价、广告等。但对于价格、库存等实时要求比较高的数据,就不能做浏览器缓存。

  • APP客户端缓存

在大促时为了防止瞬间流量冲击,一般会在大促前把APP需要访问的一些素材提前下发到客户端进行缓存,这样在大促时就不用去拉取这些素材了。还有例如首屏数据也可以缓存起来,在网络异常的情况下,还是有托底数据展示给用户的,还有地图也会采取离线地图来做缓存。

  • CDN缓存

有些页面、活动页、图片等服务可以考虑将页面、活动页、图片推送到离用户最近的CDN节点,让用户能在离它最近的节点找到想要的数据。一般有两种机制:推送机制和拉取机制。

推送机制:当内容变更后主动推送到CDN边缘节点

拉取机制:先访问边缘节点,当没有内容时,回源到服务器拿到内容并存储到节点上。

上面两种方式各有利弊。使用CDN时要考虑URL的设计,比如URL中不能有随机数,否则每次都穿透CDN回源到服务器,相当于CDN没有任何效果。对于爬虫,可以返回过期数据而选择不回源。

  • 接入层缓存

对于没有CDN缓存的应用来说,可以考虑使用如Nginx搭建一层接入层,该接入层可以考虑使用如下机制来实现。

机制名称机制描述
URL重写将URL按照指定的顺序或者格式重写,去除随机数
一致性哈希按照指定的参数(如分类、商品编号)做一致性哈希,从而保证相同数据落在一台服务器上
proxy_cache使用内存级/SSD级代理缓存来缓存内容
proxy_cache_lock使用lock机制,将多个回源合并为一个,以减少回源量,并设置相应的lock超时时间
shared_dict如果架构使用了nginx+lua实现,则可以考虑使用lua shared_dict进行cache,最大的好处就是reload缓存不会丢失

 

 

 

 

 

 

 

 

  • 应用层缓存

我们使用tomcat时,可以使用堆内缓存和堆外缓存。堆内缓存的最大问题就是重启时内存中的缓存会丢失,此时流量风暴来临,则有可能冲垮应用;还可以考虑使用local redis cache来代替堆外内存;或在接入层使用shared_dict来将缓存前置,以减少风暴。

local redis cache通过在应用所在服务器上部署一组Redis,应用直接读本机Redis获取数据,多机之间使用主从机制同步数据。这种方式没有网络消耗,性能是最优的。

  • 分布式缓存 

有一种机制是要废弃分布式缓存,改成应用local redis cache情况下,如果数据量不大,这种架构是最优的。但是如果数据量太大,单服务器存储不了,那么可以使用分片机制将流量分散到多台,或者直接用分布式缓存实现。常见的分片规则就是一致性哈希了。

我们来解说一下上图所示的应用架构 :

  • 首先接入层(Nginx+Lua)读取本地proxy cache / local cache
  • 如果不命中,则接入层会接着读取分布式集群Redis
  • 如果还不命中,则会回源到Tomcat,然后读取Tomcat应用堆内cache
  • 如果缓存都没命中,则调用依赖业务来获取数据,然后异步化写到Redis集群。

因为我们使用了nginx+lua,第二、第三步时可以使用lua-resty-lock非阻塞锁减少峰值时的回源量;如果你的服务是用户维度的,那么这种非阻塞锁大部分情况下不会有太大的作用。

1.6 并发化

假设一个读服务需要如下数据:

目标数据数据A数据B数据C数据D数据E
获取时间10ms15ms10ms20ms5ms

 

 

 

如果串行获取,那么需要60ms(时间叠加) 。

假设数据C可以依赖数据A和数据B,数据D谁也不依赖,数据E依赖数据C,那么我们可以这样来获取数据:

 

看图,如果并发获取,则需要30ms时间,可以提升一倍的性能。

再假设,有数据C依赖数据F(假设5ms),而数据F是在数据C中得到的,此时就可以考虑在取ABD服务数据时,并发读取数据F,那么整体性能就变为25ms

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值