一、概要商品系统作为电商中的核心系统之一,其重要性不言而喻。互联网业务上的高性能、高并发、高可用在商品系统上体现的淋漓尽致。除了引入分布式缓存以及分库分表的优化之外,本文从数据的角度阐述了商品系统的优化,以提高商品系统的并发能力和性能。二、转转商品服务的现状转转在业务架构的划分上采用的是大中台小业务的方式以实现业务的快速迭代,商品系统作为业务中台的核心系统之一,承载了所有业务方的商品业务。在数据库的设计上,商品系统采用了分库分表的策略,共拆分了16库,每个库16表的策略来提高数据库并发操作能力。此外,也根据业务类型对商品表做了一个垂直拆分,以降低索引树高提高查询性能以及降低锁冲突概率提高更新性能。同时引入分布式缓存,提高服务的并发能力。在缓存的使用方式上,采用的是CacheAsidePattern策略。CacheAsidePattern三、背景及存在的问题随着业务的发展,越来越多的业务方接入商品服务,不断拔高的QPS给系统带来的压力越来越大。同时转转作为全品类的二手闲置交易平台,其囊括了包含C2C、B2C、C2B、B2B、C2B2C等多种业务模式,在此场景下商品的数据模型要设计的足够通用化以承载不同的业务模式,而在一些模式下需要展示的信息较多,也就导致了单商品记录的数据比较大。由此带来的三个矛盾越发的突出,即:不断拔高的QPS和系统高可用,高性能之间的矛盾调用端的GC压力和商品数据较大之间的矛盾加机器的成本与降本增效之间的矛盾总的来说核心矛盾就是如何在尽量低的成本下提供更好、更快的服务。四、定位优化点明确了核心矛盾之后,在优化点的定位上,遵循以下原则:抓大放小。在服务治理平台上可以看到,商品读的调用量是远远大于商品写的,因此本次优化只针对商品读完成路径分析。在一次商品读的完成路径上,每个点都可以作为优化的点。可行性分析。对优化点进行可行性分析,以保证优化的效果商品读的完成路径就是商品信息查询的一次RPC调用链路,其简化的流程如下图所示一次RPC调用,本质上就是数据的获取与流动,数据的获取上利用缓存以降低耗时,提高性能。那么在数据流动上,是否可以考虑降低数据包的大小以减少序列化和反序列化的耗时、数据传输耗时以及数据的内存占用呢?如上图,Server收到Client的请求之后,从Redis获取商品数据流,反序列化成对象,接着序列化成数据流,传输到Client,Client获取到数据流之后,反序列化成对象,如果能降低数据包的大小,那么整个过程中Client、Redis、Server都将受益。如何降低数据包的大小?关键的点有两个,那就是数据压缩和减少无效数据的传输在数据压缩上,可操作的空间已然不大,序列化的协议已经承担了很多。那么是否能考虑减少无效数据的传输以降低数据包的大小?换个说法来讲,就是Client的每次调用是用到了接口返回的全部字段数据?还是仅用到了部分字段数据?如果是仅用到部分字段数据的话,是否就可以只返回调用方使用到的字段数据从而减少无效数据的传输以降低数据包的大小?根据上述的分析,我们定位到了减少无效数据返回的优化思路,下面对其可行性进行分析五、优化点可行性分析如下图列出了部分调用方查询商品信息所用到的字段数和接口返回的字段数。可见,大部分调用方用到的字段数实际上远远小于接口返回的字段数。也就是说调用方拿到了类似商品描述这样的大字符串,却没有使用到,这无疑增加了不少系统间的压力。针对这一分析,下面提出两种优化方案六、优化方案方案一:为TOP5调用量的调用方单独提供查询接口如上图,为TOP5调用量的调用方单独提供查询接口,在接口中对调用方不需要的字段数据进行过滤,只返回调用方需要的数据。这里需要明确两个问题:为什么只对TOP5调用量的调用方单独提供接口?在优化上是否能满足预期?对于第一个问题,实际上也是遵守了抓大放小的原则,TOP5的调用量占比全部调用量的50%以上,只对TOP5的调用方提供单独的查询接口,这可以在成本和效果之间取一个均衡。对于第二个问题,是否满足预期从两个角度考虑:无效数据是否被过滤?由于商品信息存储在Redis采用的是String数据结构,只能整存整取,所以从Redis中获取的数据并非全为有效数据。然后从商品服务返回给调用方的数据经过了过滤,可以达到全部为有效数据的效果通用性与扩展性如何?作为商品业务中台,在满足业务方业务的同时,也要保证能力的通用性与扩展性,避免与业务方强耦合从而疲于被动修改。单独给业务方提供接口,返回的字段需要和调用方商定,这显然已经与调用方发生了强耦合。如果调用方需要增加返回的字段信息,那么接口就得跟着改造这个方案的优点是实现简单,只需要在原有的接口上封装一层即可,但是后期维护成本高,且没有做成全链路的无效数据过滤。方案二:标记字段请求法GraphQL是一种用于API的查询语言,它可以不多不少的请求你所需要的数据,并且可以在一个请求中获取多个资源,这样一来,即使是比较慢的移动网络下,使用GraphQL的应用也能表现的足够迅速。image-20220427104321298参考这一设计理念,商品系统的设计方案如下方案概要如上图,调用方标记需要用到的字段,这些字段可以跨表,然后商品系统根据调用方标记的字段去Redis或Mysql去查询,其返回字段的决定权由调用方决定,商品系统只提供通用的查询能力。可以看到这种方案的优点为:减少定制化开发接口带来的成本按需查询,按需返回,减少链路中无效数据传输和GC带来的时间成本多表字段路由,调用方无需调用多个接口来拼接数据缺点就是需要标记请求的字段,这在一定程度上增加了请求数据包大小,下面聊聊该方案的实现细节。请求字段的标记标记一个字段,一个较为可读的表示方式就是传入该字段的名称,但字符串占用内存相对较大,在数据传输和序列化上对性能都会有一定的损耗。针对这一问题,我们采用另一种利用bit位标记一个字段的方式。如下图,long型共64位,前2位表示组的信息,后62位表示字段的信息,这就可以表示4*62=248个字段的信息,完全满足接口当前以及未来的需要当前商品系统共用到57个标记位举个例子,用long型右数第一位表示商品状态,右数第二位表示商品,那么字段表示如下:long status = 1;long title = 1 <<< 1;若调用方请求标记了商品状态和商品两个字段,就可以这么计算:long result = status | title;如此便可让long型的右数第一位和和右数第二位皆为1,当商品系统拿到这个组合long型值的时候,也就知晓了调用方的所需字段。当然用bit位标记字段有一定的复杂性,为保证正确性和易用性,这里可以利用建造者设计模式隔离构造的复杂性,让调用方更方便的使用,如下所示:BitProductFieldRepresentation fieldRepresentation = new BitProductFieldBuilder() .actTypeId() .infoType() .brandIdNew() .build();按需查询的实现要实现按照标记的字段查询数据,在商品信息的请求链路中,需要分两部分来讨论,即Redis的按需查询和Mysql的按需查询。Redis的按需查询目前商品信息在Redis中存储的数据结构是String,其中key为商品id,value是序列化后的整个商品信息,String数据结构是整存整取,无法按需查询。Redis String数据结构适用于每次需要访问大量的字段且存储结构具有多层嵌套的场景,而Hash数据结构更适用于在多数情况下只需要访问少量字段的场景,且需要知道访问哪些字段。在我们的业务场景下,Hash数据结构显然更为合适,因此将商品存储在Redis的数据结构修改为Hash类型,即可实现按需查询字段信息。image-20220427104444335Mysql的按需查询根据统计,在请求中,Redis的命中率可达98.5%以上,Mysql的命中率仅为1.5%,Mysql的命中率较低,对其做按需查询所带来的优势不大。并且Mysql的查询性能不及Redis,因此这里放弃做Mysql的按需查询,避免降低Redis命中率,增大数据库压力。表路由在请求标记法中,可以标记不同表的字段,以实现跨表查询。在批量查询商品信息的逻辑中,需要根据缓存的命中情况,对商品id进行路由,对没有命中缓存的商品id,路由到相应的队列中从数据库中进行数据获取。如下图,status位于基础信息表、price位于价格表、stock位于库存表,在查询三个商品ID的status、price、stock的数据时,在Redis中product1未命中status的数据,product2未命中price的数据,product3均命中,那么就会将product1路由到基础信息表数据待获取队列中,product2路由到价格表数据待获取队列中,后续由单独的线程去对应的数据表并发查询数据,然后组装结果返回。扩展性这里参考Spring的BeanFactoryPostProcessor,BeanPostProcessor提供了一些扩展点,在无需改动主流程情况下,提供扩展能力。如下图,在接收到请求参数之后,参数的校验以及bit位请求字段的解析都是放在扩展类中实现的扩展点七、优化效果从四个角度来看优化的效果,即调用端GC情况,服务端GC次数和GC耗时,网卡流量,接口耗时此数据是在促销调用的场景下,TPS为3500,调优化后的接口返回14个字段的数据与调优化前接口返回50个字段的数据得出需要注意的是促销使用的14个字段在3张不同的表中,在完成一个事务的情况下,需要调用3个原接口。调用端GC情况对比调用端调用优化前接口单位时间内发生547次GC,耗时1.74s调用端调用优化后接口单位时间内发生176次GC,耗时561ms约3倍的提升优化前调用端GC优化后调用端GC服务端GC次数及GC耗时对比调用端调用优化前接口,服务端单位时间内发生10次YGC调用端调用优化后接口,服务端单位时间内发生3次YGC(高点4次,低点2次)约3倍的提升优化前GC次数优化后GC次数调用端调用优化前接口,服务端单位时间内约120ms的GC时间调用端调用优化后接口,服务端单位时间内约40ms的GC时间约3倍的提升优化前GC时间优化后GC时间网卡流量对比调用端调用优化前接口,服务端机器单位时间内经过网卡的流量为90.62MB调用端调用优化后接口,服务端机器单位时间内经过网卡的流量为11.95MB约8倍的提升优化前流量优化后流量接口调用耗时对比调用优化前三个接口,接口平均耗时分别为:1.17ms、1.52ms、1.23ms调用优化后接口,接口平均耗时1.30ms优化前性能优化后性能八、总结本文从数据的角度对商品系统进行了优化,从分析思路到具体实现做了一些简单的介绍。如果大家有任何疑问,也欢迎在文末留言,我们会进行答疑。参考资料https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/http://hessian.caucho.com/doc/hessian-serialization.html/https://graphql.cn