欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

性能优化实战-2

程序员文章站 2022-07-13 08:47:19
...

 

我们在做架构设计的时候,会提到几个关键词:高性能、高可用、可扩展、安全性、伸缩性、低成本等等。对于用户量不大、并发量不高的系统,我们没必要去追求高性能,甚至连架构设计都可以免了。

那么什么样的系统需要做性能优化呢?当你发现系统响应越来越慢,慢到已经影响到用户体验的时候;

 

网站性能优化的手段:

1、 web前端优化;

减少http请求;

使用浏览器缓存;

静态资源压缩;

减少cookie传输;

CDN加速;

反向代理;

 

2、 应用服务性能优化;

分布式缓存,通过添加缓存来提高应用层的响应效率;

消息异步化:线程、队列等等;

集群服务;

代码优化:事务粒度调整、算法优化等;

 

3、 数据库层优化;

表结构优化、SQL优化,索引等等;

 

我们先看看交易所币币交易的业务流程:

性能优化实战-2
            
    
    博客分类: java性能优化 java性能优化 

 

拿委托来说,原有的逻辑如下:

用户登录后发起委托申请;

后台进行用户状态、资金密码可用、货币状态等校验;

校验通过后将委托单入库并冻结用户可用余额;

将委托单发送给撮合队列进行撮合;

 

通过梳理委托业务流程,我们发现委托的性能瓶颈主要在数据库层面,包括第二步的校验和第三步的数据持久化。而第四步的入队列操作比较简单,简单来说就只是一个队列消息发送,原则上并不会产生性能瓶颈。

 

第二步的校验如何进行优化?校验是必须的,不校验是不可能的,这辈子都不可能。那那那怎么办?

校验数据读缓存。

首先,用户登录时已经将用户信息放入SESSION,用户状态校验直接拿SESSION信息进行比对就可以了。考虑到用户登录后用户状态信息可能会调整,那么在调整后需要将用户信息及时更新到SESSION。另外,测试在做登录压测的时候,发现登录接口的吞吐量一直上不去,查表发现用户表数据量比较大,登录是通过手机号码进行登录的,所以我们对用户表的手机号码列加了唯一索引。

资金密码可用的校验需要查用户密码策略表进行交易。用户密码策略基本属于较少变更的信息,可以将密码策略加入常驻缓存(一直放在缓存)。另外,为提高用户密码策略表的查询效率,对密码策略表创建用户ID和货币ID联合索引。

我们直接将货币信息、货币对信息加入常驻缓存,后台货币有调整时及时更新至缓存,所以货币状态的校验也改成了缓存读取数据并做校验。

 

第三步的数据持久化怎么办?第三步操作还涉及到用户可用余额的校验,用户可用余额校验必须要在用户发起委托申请时来做,看来这一步不能省。那还有优化的空间么?

我们借鉴了互金资产交易系统中防超投的处理方案,将用户可用余额添加到缓存。有人会问,如果数据库数据和缓存数据不一致怎么办?解决方案是缓存操作和数据库操作都保持同步,如果不能同步更新,那至少也需要保证缓存数据和数据库数据的最终一致。

具体到委托申请这块,我们先冻结缓存中的用户可用余额,然后将委托单加入撮合队列,在进行撮合的时候再将冻结金额持久化到数据库。

 

简单总结一下委托下单的优化点:

数据校验读缓存,以减少频繁查库带来的数据库压力;

数据持久化先入队列,延迟写入数据库,以降低数据库的压力;

为数据库表添加必要的索引,提高查询效率;

 

 

接下来,我们看下挂单撮合的业务流程:

冻结可用金额持久化和委托单持久化;

从对方队列队首取出委托单进行撮合;

撮合成功后将撮合结果添加到撮合持久化队列;

 

我们以挂买委托单为例来了解一下撮合的操作流程:

1、在挂买委托单过来之后,从卖队列(所有未撮合完成的卖委托单组成的集合)中弹出队首的卖委托单。

2、如果无卖委托单或者卖委托单的价格高于买委托单的价格,则不进行撮合,将买委托单加入买队列集合。如果卖委托不为空,将卖委托重新加入卖队列。

3、如果卖委托单价格低于或者等于买委托单价格,则进行撮合,撮合成交量取买卖委托单剩余挂单量的最小值。

4、撮合完成的委托单不再加入队列,未完成的需要重新加入队列,加入队列的规则如下:

买队列中按价格从高到低排列,如果买委托队列为空或队首的挂单价小于当前订单挂单价,将该单加入对队首,否则遍历插入相应位置。

卖队列中按价格从低到高排列,如果卖委托队列为空或队首的挂单价大于当前订单挂单价,将该单加入对队首,否则遍历插入相应位置。

 

 

第一步的可用冻结和委托单持久化操作原本是在委托申请时入库的,现在移到这里排队入库。这里没有较为明显的优化点,要么继续将持久化操作后置,要么是改为批量入库。

第二步操作是内存的操作,优化主要集中在算法上;

第三步操作和委托的第四步操作类似,仅仅是消息入队列,原则上不会产生性能瓶颈。

 

下面重点讲一下第二步撮合操作在算法上的优化:

1、原有的挂买、挂卖未撮合完成的委托单都放在本地内存中,为了更好的支持集群服务,我们首先将买卖队列由本地缓存改为RedisList类型缓存;

2、将撮合队列由原来的单个队列按业务拆分成挂买撮合、挂卖撮合和撤单三个队列,队列拆分类似于服务的横向扩展,可以在一定程度上提高系统的吞吐量,提升队列的处理能力,防止队列中消息堆积过于严重拖慢了整个服务的处理速度;

3、挂买撮合、挂卖撮合开启多线程服务,每个队列开启10个线程,支持单机环境的并发操作;

4、未撮合完成的委托单入缓存的优化,在第一次改版中,我们借助于RedisList集合的加入、弹出等单线程操作,取得了很好的效果。但是在高并发场景下,会出现可以撮合却未进行撮合等问题。

5、针对产生的问题,先是加了自动撮合定时器来自动撮合价格合适的买卖委托单,但是效果不是很明显。

6、我们分析买卖队列数据,发现部分委托单存在排序错乱的情况。我们的算法其实没什么问题,但是在高并发下确实存在该问题。针对这个问题,大家首先想到的是加锁,通过锁来控制队列的进出,进而保证队列集合按价格顺序排列。但是这里又必须支持集群,如果加锁,势必会影响性能。那怎么办?有没有什么办法,既能支持多线程服务,还不需要加锁?

7、启用lua脚本,将委托单的入队列操作单独抽取出来,改为lua脚本实现。

 

 

简单总结一下撮合操作的优化点:

队列拆分将操作频繁的队列(如撮合队列)按业务拆分;

队列多线程     买卖队列开启多线程服务;

使用lua脚本  提高委托单入队列的效率;

 

 

 

然后,我们来看看撮合持久化的业务如何做持久化?

原有的撮合持久化操作,是一个个排队消费处理的。

撮合持久化的第一次优化是将队列中的持久化改为批量处理,如货币资金变更、资金日志、交易记录等等。

压测过后发现消息堆积仍然比较严重,然后尝试将撮合持久化改为多线程处理,发现效果不是很明显,并且偶有死锁产生,这就说明通过开启多线程提高撮合持久化处理能力是行不通的。那么还有没有其他的办法呢?

我们知道消息推送分为两类:推(Push)模式和拉(Pull)模式。RabbitMQ默认的消息推送模式是Push模式。

推模式是长连接模式,能做到实时处理,提高响应速度。推模式缺点也比较明显,一次只能处理一个请求。

而拉模式和推模式刚好相反,不能做到消息实时处理,可以一次拉取多个消息。我们的持久化操作对实时性要求不是那么高,可以通过一次拉取并处理多个消息来提高系统的并发量,进而在一定程度上减少消息堆积的量。

我们在撮合持久化消费者端开启一个线程服务,用来消费撮合持久化队列。线程的消息推送模式改为拉模式,每次拉取20个消息,处理完毕休眠一段时间。休眠时间的长短根据队列中消息有无来进行调整,当队列中没有消息时,让线程休眠时间长一点,比如5s;当队列中有消息堆积时,让线程休眠时间短一点或者不休眠;

 

 

简单总结一下撮合持久化的优化点:

数据持久化单改批,但是批量操作的量不要设置的太大;

消息推送模式推改拉,提高并发处理能力;

 

 

 

性能优化总结:

分布式缓存,通过添加缓存来提高应用层的响应效率;

消息异步化:线程、队列等等;

集群服务;

 

代码优化:事务粒度调整、算法优化等;

 

 

  • 性能优化实战-2
            
    
    博客分类: java性能优化 java性能优化 
  • 大小: 49.1 KB
相关标签: java 性能优化