写在开头,好多人都问我在支付宝是如何处理高并发的。哎~~~~,那我今天就说一下,我是怎么解决每天上百万的还款的,顺便介绍一下高并发的思路。
首先,先看一个图(看不懂没关系,稍后我会解释)。
首先,介绍下,我在支付宝主要负责开发信贷核心系统,它是网商银行的核心系统(不是皮系统),通俗的说就是贷款和还款,当然实际上非常复杂,举个例子,现在国内银行的信贷系统都是从IBM或者oracle买的(开发太难了),而网商这边开发了自己的一套系统。
有一天,有这么一个大需求:资金驱动。我来解释一下,首先,如果用户被打上了逾期或者强制扣款标志,那么,如果你的支付宝账号有money,支付宝受托系统就会给我发消息,然后我处理,最后调用支付宝受托扣款。这是为什么,因为怕用户还不上钱或者不还钱,不然你想想贷出去的钱都收不回来,马爸爸不哭晕在厕所~~~~
言归正传,这个需求看起来特别的清楚明了,但是一般恶心都看起来简单(以往的编程经验)。
看到这个,我于是想到了这个处理办法:
1、接收消息
2、消息落地
3、处理消息,掉支付宝受托接口。
ok,思路清晰,并且完成了。
~~~~~~~~~~~~~~~~~~~~~~~沾沾自喜,当自测的时候,哭晕在厕所,妈的,处理消息的时候简直太慢了,因为对于一个用户,要找出他所有未结清的支用(借据),每笔支用还包含支用账单(分期账单),需要先结息,计算每笔分期账单的表内,表外的正常利息,逾期利息,逾本罚,逾利罚,滞纳金,还有宽限期处理。。。。。(业务极其复杂,学过会计学的就懂了)。造成的问题就是时间太长了,一个事务时间非常长,再加上调支付宝受托接口的时间,太长了。而且网络访问放在了事务中,简直是噩梦(原因很简单:时间太长,事务可能会自动终止,连接不能及时归还导致数据库挂了)。
考虑到以上这个问题,我又想了一个方案:
1、处理消息后将消息落地。
2、起一个调度任务处理消息生成扣款指令。
3、再起一个调度任务负责捞取扣款指令掉支付宝接口。
~~~~~~~~~~~~~~~~~~~~~~~~~so easy,但是当调试的时候发现,我太天真,因为对于每个消息处理很慢(上面介绍过),等我生成扣款指令,消息列表都他妈的都满了,导致消息多的处理不过来,怎么解决?只有优化。
换个思路,既然消息堆积,能不能快速处理消息,当然可以,但是又不能真的去处理消息生成扣款指令,因为太耗时间了,哎~~~~~~
我可不可以这样,在处理消息的时候只是生成相应的记录,也就是生成那个用户要被扣钱了,并不是真的去处理它,然后起一个调度器去处理他。
于是方案变成了:
1、接受消息。
2、消息落地
3、起一个调度器处理消息生成用户扣款记录
4、起一个调度器处理用户扣款记录生成扣款指令
5、起一个调度器处理扣款指令调用支付宝受托
~~~~~~~~~~~~~~~~~~~~~~~~~~~完美!哈哈,终于通过了测试可以上线了。线上运行了一段时间,没有什么问题,好开心。直到有一天的来临,大促!!!!!!!!!!妈的,大促的时候,随着用户的购买,卖家的收钱,账上会不断有资金流入,不停地产生资金驱动消息,而且都是小批量的,数量太大了,上千万条,简直崩溃了。看来还是需要优化!
进一步处理,在观察大促的时候发现,单位时间内,一个商户会不停地有少量资金的不断流入,但是观察发现,其实数据都是一样的,简单都说无非是我收了1块钱,2块钱。只有额度不同,其他的都一样。那么我可以把它们都看成重复记录。哇塞,可以这么处理。我可以去重,然后生成还款指令。
继续优化方案:加油!!!
1、接受消息,并消息落地。
2、处理消息生产前置还款指令(aegis_repay_pretreat)
3、因为前置还款指令会有大量的重复记录,去重然后产生还款命令(aeghis_repay_cmd),同时删去aegis_repay_pretreat中的记录。
4、处理还款命令生产扣款命令。
5、处理扣款命令,调用支付宝受托。
大概需要4个定时任务调度。终于解决了问题了,也处理高并发场景,太爽了################################
真的没问题了么??在测试的时候发现,由于调度器会不停地抢锁,扫表,oracle吃不消啊!!!!!!!!!!
进一步优化,一般用的调度任务,除了Linux的crontab,再就是spring的quartz调度,当然支付宝有比较牛逼的分布式调度中心。但是鉴于优化访问oracle的性能,选用自己搞quartz。
开始了:
假设我有10台机器,他们都需要从同一个表中捞取数据,那么需要加锁,然后更改任务状态,别人就捞不到了。但是如果不停地捞取,频繁扫表,数据库性能极具下降,但是这种定时任务都是每时每刻在执行,如何解决?
首先做一个阻塞队列arrayBlockingQueue,然后先加锁然后捞数据,抢不到锁就sleep几秒,然后对捞取的数据判断,如果没有捞到,那么说明此时表中并没有多少数据,那可以再sleep,看起来没什么问题。
可不可以进一步优化,因为我们知道一般用到线程池,都需要关注他的coreThreadCount,maxThreadCount,队列长度。具体处理是当前线程数量小于coreThreadCount,则会创建线程,如果大于,则加入队列,如果放不进去创建线程,如果大于maxThreadCount抛异常。
所以,可以看到,如果线程比较忙的时候,阻塞队列数量会很多,线程压力会比较大。那么可以这样,每次处理判断下队列的长度,如果大于饥饿值就sleep,降低线程压力。
还能不能再进一步优化?
既然所有的处理都围绕着队列长度,捞取的数量,可不可以计算下捞取的数量,也就是因为每次捞取队列当前容量size - 队列当前长度length 的指令数量,那么,如果说捞取后,发现队列中的长度还是低于整个队列容量的1/3是不是表示此时表中并没有多少数据,仍然可以sleep以此降低扫表频率。
get it
------------------------------------------------------------------------
经过一番折腾,终于解决了!这里留下一个问题:如何解决抢锁频繁,更改状态会在机器重启或者发布时卡主指令如何处理?留给大家讨论下,欢迎交流。发完了之后,有些小伙伴问我,为啥处理这么复杂,在这里说一下,一般用户还款大概峰值是几百万比,但是在大促的时候洪峰大概上亿比。所以只能这样处理。