?
class="MsoNormal">我们在做架构设计的时候,会提到几个关键词:高性能、高可用、可扩展、安全性、伸缩性、低成本等等。对于用户量不大、并发量不高的系统,我们没必要去追求高性能,甚至连架构设计都可以免了。
那么什么样的系统需要做性能优化呢?当你发现系统响应越来越慢,慢到已经影响到用户体验的时候;
?
网站性能优化的手段:
1、?web前端优化;
减少http请求;
使用浏览器缓存;
静态资源压缩;
减少cookie传输;
CDN加速;
反向代理;
?
2、 应用服务性能优化;
分布式缓存,通过添加缓存来提高应用层的响应效率;
集群服务;
代码优化:事务粒度调整、算法优化等;
?
3、 数据库层优化;
表结构优化、SQL优化,索引等等;
?
我们先看看交易所币币交易的业务流程:
?
拿委托来说,原有的逻辑如下:
用户登录后发起委托申请;
后台进行用户状态、资金密码可用、货币状态等校验;
校验通过后将委托单入库并冻结用户可用余额;
将委托单发送给撮合队列进行撮合;
?
通过梳理委托业务流程,我们发现委托的性能瓶颈主要在数据库层面,包括第二步的校验和第三步的数据持久化。而第四步的入队列操作比较简单,简单来说就只是一个队列消息发送,原则上并不会产生性能瓶颈。
?
第二步的校验如何进行优化?校验是必须的,不校验是不可能的,这辈子都不可能。那那那怎么办?
校验数据读缓存。
首先,用户登录时已经将用户信息放入SESSION,用户状态校验直接拿SESSION信息进行比对就可以了。考虑到用户登录后用户状态信息可能会调整,那么在调整后需要将用户信息及时更新到SESSION。另外,测试在做登录压测的时候,发现登录接口的吞吐量一直上不去,查表发现用户表数据量比较大,登录是通过手机号码进行登录的,所以我们对用户表的手机号码列加了唯一索引。
资金密码可用的校验需要查用户密码策略表进行交易。用户密码策略基本属于较少变更的信息,可以将密码策略加入常驻缓存(一直放在缓存)。另外,为提高用户密码策略表的查询效率,对密码策略表创建用户ID和货币ID联合索引。
我们直接将货币信息、货币对信息加入常驻缓存,后台货币有调整时及时更新至缓存,所以货币状态的校验也改成了缓存读取数据并做校验。
?
第三步的数据持久化怎么办?第三步操作还涉及到用户可用余额的校验,用户可用余额校验必须要在用户发起委托申请时来做,看来这一步不能省。那还有优化的空间么?
我们借鉴了互金资产交易系统中防超投的处理方案,将用户可用余额添加到缓存。有人会问,如果数据库数据和缓存数据不一致怎么办?解决方案是缓存操作和数据库操作都保持同步,如果不能同步更新,那至少也需要保证缓存数据和数据库数据的最终一致。
具体到委托申请这块,我们先冻结缓存中的用户可用余额,然后将委托单加入撮合队列,在进行撮合的时候再将冻结金额持久化到数据库。
?
简单总结一下委托下单的优化点:
数据校验读缓存,以减少频繁查库带来的数据库压力;
数据持久化先入队列,延迟写入数据库,以降低数据库的压力;
为数据库表添加必要的索引,提高查询效率;
?
?
接下来,我们看下挂单撮合的业务流程:
冻结可用金额持久化和委托单持久化;
从对方队列队首取出委托单进行撮合;
撮合成功后将撮合结果添加到撮合持久化队列;
?
我们以挂买委托单为例来了解一下撮合的操作流程:
1、在挂买委托单过来之后,从卖队列(所有未撮合完成的卖委托单组成的集合)中弹出队首的卖委托单。
2、如果无卖委托单或者卖委托单的价格高于买委托单的价格,则不进行撮合,将买委托单加入买队列集合。如果卖委托不为空,将卖委托重新加入卖队列。
3、如果卖委托单价格低于或者等于买委托单价格,则进行撮合,撮合成交量取买卖委托单剩余挂单量的最小值。
4、撮合完成的委托单不再加入队列,未完成的需要重新加入队列,加入队列的规则如下:
买队列中按价格从高到低排列,如果买委托队列为空或队首的挂单价小于当前订单挂单价,将该单加入对队首,否则遍历插入相应位置。
卖队列中按价格从低到高排列,如果卖委托队列为空或队首的挂单价大于当前订单挂单价,将该单加入对队首,否则遍历插入相应位置。
?
?
第一步的可用冻结和委托单持久化操作原本是在委托申请时入库的,现在移到这里排队入库。这里没有较为明显的优化点,要么继续将持久化操作后置,要么是改为批量入库。
第二步操作是内存的操作,优化主要集中在算法上;
第三步操作和委托的第四步操作类似,仅仅是消息入队列,原则上不会产生性能瓶颈。
?
下面重点讲一下第二步撮合操作在算法上的优化:
1、原有的挂买、挂卖未撮合完成的委托单都放在本地内存中,为了更好的支持集群服务,我们首先将买卖队列由本地缓存改为Redis的List类型缓存;
2、将撮合队列由原来的单个队列按业务拆分成挂买撮合、挂卖撮合和撤单三个队列,队列拆分类似于服务的横向扩展,可以在一定程度上提高系统的吞吐量,提升队列的处理能力,防止队列中消息堆积过于严重拖慢了整个服务的处理速度;
3、挂买撮合、挂卖撮合开启多线程服务,每个队列开启10个线程,支持单机环境的并发操作;
4、未撮合完成的委托单入缓存的优化,在第一次改版中,我们借助于Redis的List集合的加入、弹出等单线程操作,取得了很好的效果。但是在高并发场景下,会出现可以撮合却未进行撮合等问题。
5、针对产生的问题,先是加了自动撮合定时器来自动撮合价格合适的买卖委托单,但是效果不是很明显。
6、我们分析买卖队列数据,发现部分委托单存在排序错乱的情况。我们的算法其实没什么问题,但是在高并发下确实存在该问题。针对这个问题,大家首先想到的是加锁,通过锁来控制队列的进出,进而保证队列集合按价格顺序排列。但是这里又必须支持集群,如果加锁,势必会影响性能。那怎么办?有没有什么办法,既能支持多线程服务,还不需要加锁?
7、启用lua脚本,将委托单的入队列操作单独抽取出来,改为lua脚本实现。
?
?
简单总结一下撮合操作的优化点:
队列拆分将操作频繁的队列(如撮合队列)按业务拆分;
队列多线程???? 买卖队列开启多线程服务;
使用lua脚本? 提高委托单入队列的效率;
?
?
?
然后,我们来看看撮合持久化的业务如何做持久化?
原有的撮合持久化操作,是一个个排队消费处理的。
撮合持久化的第一次优化是将队列中的持久化改为批量处理,如货币资金变更、资金日志、交易记录等等。
压测过后发现消息堆积仍然比较严重,然后尝试将撮合持久化改为多线程处理,发现效果不是很明显,并且偶有死锁产生,这就说明通过开启多线程提高撮合持久化处理能力是行不通的。那么还有没有其他的办法呢?
我们知道消息推送分为两类:推(Push)模式和拉(Pull)模式。RabbitMQ默认的消息推送模式是Push模式。
推模式是长连接模式,能做到实时处理,提高响应速度。推模式缺点也比较明显,一次只能处理一个请求。
而拉模式和推模式刚好相反,不能做到消息实时处理,可以一次拉取多个消息。我们的持久化操作对实时性要求不是那么高,可以通过一次拉取并处理多个消息来提高系统的并发量,进而在一定程度上减少消息堆积的量。
我们在撮合持久化消费者端开启一个线程服务,用来消费撮合持久化队列。线程的消息推送模式改为拉模式,每次拉取20个消息,处理完毕休眠一段时间。休眠时间的长短根据队列中消息有无来进行调整,当队列中没有消息时,让线程休眠时间长一点,比如5s;当队列中有消息堆积时,让线程休眠时间短一点或者不休眠;
?
?
简单总结一下撮合持久化的优化点:
数据持久化单改批,但是批量操作的量不要设置的太大;
消息推送模式推改拉,提高并发处理能力;
?
?
?
性能优化总结:
分布式缓存,通过添加缓存来提高应用层的响应效率;
消息异步化:线程、队列等等;
集群服务;
?
代码优化:事务粒度调整、算法优化等;
?
?