线程池处理高并发请求

背景

本系统(支付系统)会在每个月特定时间(如账单日某个时间)接收上游系统发起的大量请求并进行处理,并在处理完成后返回结果给上游系统。而本系统接收到请求进行处理的过程是调用第三方(支付公司)进行处理并获取结果。

系统原实现方案没有采用任何控制请求并发数的措施,接收到上游系统的请求后,就发送给支付渠道进行处理。这样实际上就是来一个请求就启动一个tomcat线程进行处理。

上游系统调用本系统,本系统调用第三方公司同步接口,获得结果后本系统将结果同步返回给上游系统。
假设上游系统的并发数为n,则一开始本系统一秒钟接收n个请求,并将这些请求发往第三方进行处理。假设第三方处理请求需要的时间为t,则对于上游系统来讲,本系统的响应时间比t略大。由于整个调用链上第三方处理时间较长,最短1s,最长可达10s,所以一次完整请求的处理时间是比较长的。本系统处理上游系统请求的TPS = n/t(实际略大于t),由于并发数n不大同时响应时间t较大,所以tps不高。本系统和下游第三方基本没什么压力。

此方案存在的问题:系统接口响应时间依赖于第三方接口响应时间,最长可能长达十几秒。另外就是tps过低。实际上游系统并发量为16,响应时间平均为3s,则tps为5左右,支付系统平均每秒处理5个请求,一小时只能处理18000左右的请求数。

改进方案

对本系统进行改造,主要是把本系统的请求处理接口由同步接口改为异步接口。

即同步接口不再等第三方处理完成之后才返回,而是在本系统接受到请求并进行内部处理,在发送给第三方进行处理之前返回。然后再由异步任务将请求发送给第三方进行处理,将处理结果再通过回调方式或者提供查询接口提供给上游系统。

经此改造后,本系统对上游系统的响应时间由平均3s缩短到30ms。此种改进方案的本质是将上游系统的请求接受过来,然后根据下游的处理能力进行请求分发。那么必须要有一种机制能将请求储存起来,然后根据实际情况将请求取出来发送给下游第三方进行处理。

考虑两种方案,其中一种是消息队列,将请求放到消息队列中慢慢处理,但无法保证消息不丢失。故采用第二种方案:将请求全部入库,然后通过线程池来执行后续任务。

问题及解决办法

1.大量请求没有处理

改进后出现的第一个问题是大量请求在任务表中没有得到处理。原因是:大量请求在一定时间内进入任务表,同时通过线程池将请求取出来执行。由于接受请求的接口响应时间极短(30ms-50ms),tps约为400。线程池核心线程数为40,最大线程数为80,四个服务器实例加起来为320,而下游系统的响应时间平均为3s,tps为100左右。导致每秒约有300个请求得不到处理而进入队列等待,很快四个服务器实例的线程池队列全部放满,之后的请求就无法得到处理了。当上游系统的请求全部接受完毕,主线程停止之后,因任务队列满而得不到处理的请求就在任务表中得不到处理。

解决办法是将任务表中被拒绝的请求用一个状态字段来标记,然后通过补偿任务捞出来再次执行。但是在此过程中又出现了第二个问题。

2.并发更新数据库导致死锁

补偿任务处理逻辑是:从任务表中取出请求记录,当该记录状态为被拒绝,且更新为已执行时更新成功,则将请求发送给第三方进行处理。
这里使用数据库乐观锁来更新状态,防止并发更新和重复执行请求。但是是将状态作为乐观锁标识,更新语句以状态为条件来更新。这种做法带来了死锁问题。原因是:mysql的innoDB行锁是通过给索引项加锁实现的。而索引分为主键索引和非主键索引,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。如果两个线程同时来更新一条记录,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。解决办法是加version字段,更新时以主键id和version作为条件来更新。Version上无索引,更新时只会锁定主键索引,就不会造成死锁。

其实这里也可以使用redis分布式锁,但是需要设置合适的锁过期时间。

后续思考

经过上述改进后,一台服务器的tps为100左右,四台加起来400。为了保证请求不丢失,将所有请求全部入库,此时实际上是将压力全部丢给了数据库,每秒400个并发写入操作(当然,实际上为了减少数据库压力,将上游系统的并发降低,tps也更低一点)。当然,这个并发数还不算高,数据库压力不大。但是,如果并发数进一步增大,则数据库压力将大为增加,此时就不合适采用这种方案了。可能还是需要使用rabbitmq消息队列,通过确认机制来保证消息的可靠性。

你可能感兴趣的:(并发编程,后端框架,分布式,高并发)