缘起
小亮在公司务农搬砖时,有一个需求是这样:
用户展示微信付款二维码,我们通过设备扫码进行收款,这个收款行为会通过后台与微信方进行交互。此时会有两种情况:1. 用户不需要输入密码,直接支付成功;2.用户需要输入密码才能支付成功。而微信官方提供的付款码接口是一个同步接口,如果是上述情况2,那么返回的数据会告诉你『用户正在支付中』。那行吧,用户支付成功后,微信你就通知我支付结果呗。一通操作下来,什么?没有异步通知?这就尴尬了,只能自己屁颠屁颠拿着订单号去调微信查询接口查最终支付状态。
这里有一个问题就是用户输入密码到支付完成需要一定时间,而且也不确定何时进行支付,所以需要等待一段时间后才可以进行查询订单状态的操作,间隔时间不能太短或太长,太短用户可能还未支付,太长会延后下游系统下一步处理。
分析
通过分析需求,我们需要解决以下两个问题:
1. 延迟查询订单
2.及时处理订单
延迟查询订单即支付动作发生后,需间隔一定时间再查询订单;
及时处理订单即不能积累太多未知状态的订单,以免影响下游系统处理进度。
结合实际,考虑以下方法解决:
1. 定时任务+多线程
2. Redis延迟队列
实现一:定时任务+多线程
我们通过设置定时任务调度接口,将前一段时间未知状态的订单查询出来,之后通过ExecutorService线程池,将查询订单任务通过for循环一个个放入线程池去调用查询接口,从而提高查询效率。
这里需要注意的是,线程池是一个ThreadPoolExecutor对象,初始化的线程数+队列数也是有限的,默认的拒绝策略是中止策略AbortPolicy,不断地往线程池添加查询任务直到队列都爆满,则会抛出RejectedExcutionException异常,因此需要将拒绝策略改为调用者运行策略CallerRunsPolicy,当队列爆满后,不会抛出异常,而是让主线程自己来执行任务,此时无法提交新任务,线程池中的工作线程也有时间处理任务,这是一个比较好的选择。
实现二:Redis延迟队列
Redis有SortedSet集合类型,可以通过每次插入唯一的score进行排序,这就是Redis延迟队列的基础。首先我们提供插入延迟任务到Redis的方法(zadd),每次插入都带上当前Unix时间戳以记录生成时间,并且key值会通过hash后得到一个下标值,再根据下标值把数据分配到对应的Redis队列中,保证数据均匀分配。其次在应用启动时,通过@PostConstruct注解在依赖注入完成后,自动运行拉取延迟任务的线程,线程数量由Redis队列数决定,每个线程只查询一个队列的延迟任务,查询规则是根据当前时间与score对比大小,符合条件则取出该任务(zrangebyscore),并通过zrem原子删除该任务数据。取出任务后,通过kafka或者接口形式,将任务分配到具体的处理单元进行业务处理逻辑。
这里的注意点有两个,一是Redis队列的数量和数据分配要合理,即Redis队列数量太小或数据分配不均可能会出现『大value』导致Redis不堪重负,建议是根据正常的业务量的N倍进行评估;二是在微服务架构中,会有多个拉取延迟任务的实例同时对一个队列进行查询操作,此时需要用到『分布式锁』进行并发控制(后面讲讲我们怎么使用分布式锁,先埋个坑),我们是对每个队列进行加锁,如果没有加锁可能会导致重复拉取到同个任务,即重复消费。
后记
浏览了网上不少资料,发现实现延迟任务的方法还有很多,比如利用jdk的DelayQueue、RabbitMQ死信队列、 Quartz定时任务、Kafka时间轮(这个没看懂...应该是一种算法实现?)等等,还有xxl-job也不错,可以将任务分片到不同实例运行,不过还没有尝试过。
谨以此文作记录学习之用,后端技术博大精深,吾将上下而求索。