个人负责开发模块二:订单支付(阿里Pay)
用户对运输信息进行填写完毕后,并且生成相对应的订单,这时该笔支付状态为未支付状态,用户可以在订单页面查看到未支付的订单,并且选择支付方式
(支付方式为立即支付或者货到付款)。
在用户付款成功后,支付宝有两个支付回调,理论上是先执行同步回调再执行异步回调,但是实际上同步回调和异步回调是不分先后的。当用户扫码完成之后支付宝会直接返回给用户支付成功,执行同步回调,同步回调的值我们一般都是让他跳转到一个页面,提示用户已经支付成功了。
在支付成功的异步回调逻辑中,最重要的就是将订单的支付状态修改为已支付,并且将支付宝返回的支付信息如交易流水号添加到我们的订单表中,然后再根据用户实际支付金额以及应付金额对用户的积分进行修改。
由于支付模块以及积分模块是两个不同的服务,所以这时候就涉及到了分布式事务。
保证分布式事务一致性的方案有很多种,我们使用的是 rocketmq来保证事务的一致性。
RocketMQ的设计中呢,Producer端和broker具有双向通信能力,使得 Broker天生可以作为一个事务协调者存在;可以确保本地事务执行与消息发送的原子
性。而 RocketMQ本身提供的存储机制,使得事务消息具有了持久化能力;
RocketMQ的高可用机制以及可靠消息设计,使得事务消息即使在系统发生异常时,依然能够保证分布式事务的最终一致性达成。
我们在支付的异步回调中发送一个事务消息到 MQ,这个消息被叫做 prepare消息。这个消息不会被放入真正的业务 Topic中,而是先被放入 MQ内置的 Half
Topic中,这个消息不会被建立消息索引,这个消息对 Consumer端是不可见的。
MQ收到这个 prepare消息之后会给我们的 Product端一个反馈消息,告诉
Product端消息接收成功了。Product端接收反馈消息,需要实现
RocketMQLocalTransactionListener接口,然后重写里面的两个方法,我们要在其中一个方法中处理本地事务,也就是修改订单状态并且将支付信息保存到记录表中。
如果本地事务处理成功了,那么我们就返回给 MQ事务处理状态为 COMMIT,
此时 MQ就会将 Half Topic中的消息取出来并且生成消息索引,然后将这个消息转存到业务 Topic中,此时消息对消费者可见,消费者就开始处理增加积分的事务了。
如果本地事务处理失败了,那么我们就返回给 MQ事务处理状态为 ROLLBACK,此时 MQ不会生成消息索引,消息对消费者不可见,那么消费者也就不会增加积分。
如果本地事务处理超时或者宕机,那么 MQ就会不断询问其同组的所有
Product来回查事务处理状态。Producer收到回查消息后,到 Redis中检查回查消息对应的本地事务的状态,根据本地事务的状态,重新向 Broker返回 Commit或者 Rollback。
这个事务消息回查也很简单,我们要知道,事务消息的成功投递时需要经历三个 topic的,分别是 half topic、op topic和业务 topic。
首先呢,我们的 broker会维护一个死循环,默认每分钟执行一次,mq就通过 half topic和 op topic来存储事务消息的推进状态,其中 half topic中存放的就是 prepare消息,而 op topic中存放的呢是 prepare消息对应的状态也就是 commit或 rollback。mq呢就是通过对比这两个队列的差值来找到还没有提交事物处理结果的超时或者宕机的事物,然后调用 producer端相关方法来回查事物处理结果。
这个时候仅仅只是保证了本地事务与消息发送的原子性问题,也就是说,我们本地事务只要执行成功,那么消息就一定能够发送出去。
还有一点就是消费者端进行添加积分操作的时候,我们通过 ACK重试机制和死信队列人工干预,来确保消息肯定会被消费成功。Broker为了保证消息一定会被消费成功,只有当消费者明确返回了消息消费成功的信息,也就是返回了
CONSUMER_SUCCESS的时候,RocketMQ才会认为消息消费成功。当然,我们是对消息进行手动 ACK返回的操作。确保在中途断电、抛出异常等情况时都不会认为消息消费成功,即都会发起重试。
RocketMQ默认重试次数是 16次,当达到最大重试次数之后会将该消息投递到死信队列中。我们可以根据业务需要自定义消费的最大重试次数。当死信消息投递到死信队列中后,死信消息的业务就需要进行人工干预。其实重试个三五次就可以认为当前业务存在异常,继续重试下去也没有意义了,那么我们就将当前这条消息进行提交,返回 broker状态为 CONSUME_SUCCESS让消息不再重发,同时将该消息存入我们业务自定义的死信消息表(Redis)中,将业务参数入库,相关的运营通过查询死信消息表来进行对应的业务补偿操作。
正是因为有了消息重试,所以我们还要考虑消息被重复消费的问题。我们需要自定义一张 Redis日志表,将已经消费成功的消息的 id放在这个日志表中,如果新收到的消息的 id已经在日志表中,说明这条消息已经消费过了,就不再消费这条消息。
当用户长时间未支付时
如果用户选择线上支付却一直没有支付,针对于用户长时间不支付的问题。我们是限制用户支付时间是 24小时,在我们订单生成的时候我们通过 rocketMQ发送一个延迟消息,指定消费时间为 24小时,这个消息监听是在 24小时间才能进行消费,消费的时候拿到发送过来的订单号去查询数据的支付状态,如果当前的状态是未支付,我们会将修改为支付超时,然后给他发送短信或者是邮件提醒支付。如果货物送到准备签收前,还没有支付,站点可以打电话询问寄件方是否是货到付款,如果是就让收件方支付,修改他的支付方式为货到付款。如果不是他不选择货到付款就让他付款,成功之后才让取货。
分布式事务为什么要使用 rocketMQ:
因为其他的都具有强一致性,而 rocketMQ的效率相比其他而言来说比较快,而且我们之前团队一直使用的都是 rocketMQ,对它也比较熟悉;