021:支付定时对账与订单30分钟有效期
1 聚合支付中如何实现对账
今日课程任务
- 支付宝没有及时的将支付结果通知给商户端,如何解决数据一致性问题
- 如何实现解决商户端与支付宝两者间的分布式事务问题
- 秒杀成功了,就是不支付,如何实现库存回滚
- 基于Redis与MQ实现订单30分钟有效期
做对账:
如果是查询支付宝后台交易金额 > 商户端查询交易额金额,没有关系,可以通过补偿实现。在支付宝这边已经支付,但是商户端支付状态没有更新。
支付宝后台交易金额 < 商户端查询交易额金额,如商户端交易额11万,支付宝后台查询为10万,说明代码有严重bug。
2 用户支付了,异步回调产生接口延迟如何解决
网络抖动造成双方数据不一致性很正常,但是可以采取最终一致性的思想,可以根据商户生成的全局id,主动调用支付宝接口查询状态同步到商户端。
补偿接口:
- 根据全局id查询状态是否为已经支付
- 如果不是已支付状态的情况下,调用支付宝接口同步状态,支付的渠道由用户提供。3. 判断支付宝回调接口如果返回状态码为10000,人工比对支付时间、下单金额。
自动补偿:循环调用渠道效率非常低,可以根据之前用户点击支付渠道的日志行为记录,后期做自动化补偿。
3 人工补偿实现订单状态最终一致性
人工补偿接口(查询支付宝订单状态补偿更新订单)
public interface PaymentVerifyOrderService {
/**
* 人工补偿接口
* @return
*/
@GetMapping("/verifyOrder")
BaseResponse<String> verifyOrder(
@RequestParam("paymentId") String paymentId,
@RequestParam("channelId")String channelId);
}
@RestController
public class PaymentVerifyOrderServiceImpl extends BaseApiService implements PaymentVerifyOrderService {
@Autowired
private PaymentChannelMapper paymentChannelMapper;
@Override
public BaseResponse<String> verifyOrder(String paymentId, String channelId) {
// 1.验证参数
if (StringUtils.isEmpty(paymentId)) {
return setResultError("paymentId支付id不能为空!");
}
if (StringUtils.isEmpty(channelId)) {
return setResultError("channelId支付id不能为空!");
}
// 2.使用策略模式
PaymentChannelEntity pce = paymentChannelMapper.selectBychannelId(channelId);
if (pce == null) {
return setResultError("该渠道已关闭或不存在,请联系管理员");
}
// 3.根据beanid 从Spring容器中找到策略类执行
String payBeanId = pce.getPayBeanId();
if (StringUtils.isEmpty(payBeanId)) {
return setResultError("没有配置payBeanId");
}
PayStrategy payStrategy = SpringContextUtils.getBean(payBeanId, PayStrategy.class);
// 调用支付宝查询
return payStrategy.verifyOrder(pce, paymentId);
}
}
@Component
@Slf4j
public class AliPayStrategy extends BaseApiService implements PayStrategy {
@Autowired
private PaymentTransactionMapper paymentTransactionMapper;
……
@Override
public BaseResponse<String> verifyOrder(PaymentChannelEntity pce, String paymentId) {
// 先根据支付id,查询是都已经支付过
PaymentTransactionEntity pte = paymentTransactionMapper.selectByPaymentId(paymentId);
if (pte == null) {
return setResultError("根据支付id没有查询到支付信息");
}
if (!(PayConstant.PAY_STAY_STATUS.equals(pte.getPaymentStatus()))) {
return setResultError("不需要实现补偿");
}
// 3.如果在商户端没有支付过情况下,则调用第三方支付接口查询 同步订单状态
//获得初始化的AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(pce.getRequestAddress(),
pce.getMerchantId(), pce.getPrivateKey(), "json",
AlipayConfig.CHARSET, pce.getPublicKey(), AlipayConfig.SIGN_TYPE);
//设置请求参数
AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
//请二选一设置
alipayRequest.setBizContent("{\"out_trade_no\":\"" + paymentId + "\"," + "\"trade_no\":\"" + "" + "\"}");
//请求
try {
String result = alipayClient.execute(alipayRequest).getBody();
JSONObject jsonObject = JSONObject.parseObject(result);
JSONObject aliPayResponse = jsonObject.getJSONObject("alipay_trade_query_response");
String payCode = aliPayResponse.getString("code");
if (!(PayConstant.ALIPAY_PAY_SUCCESS_CODE.equals(payCode))) {
// 则同步该订单状态
return setResultError("该支付订单没有在支付宝支付");
}
// 修改订单状体
int updateAliPayResult = paymentTransactionMapper.
updatePaymentStatus(PayConstant.PAY_PAID_STATUS + "",
paymentId, "ali_pay");
return updateAliPayResult >= 0 ? setResultSuccess("数据同步为已经支付")
: setResultError("系统错误同步失败");
} catch (Exception e) {
log.error(">>>verifyOrder()<<", e);
return setResultError("系统错误同步失败");
}
}
}
效果测试:
不建议异步回调根据支付id查询状态同步
有可能导致异步回调超时产生其他幂等性等问题
4 基于Redis订单超时30分钟超时设计
下单流程:先下订单 -> 库存-1 -> 跳转到支付接口实现支付。如果用户30分钟未支付如何处理?
订单30分钟超时的设计
- 基于redis的过期key
下订单的时候会生成一个令牌,有效期为30分钟,当该key失效的时候会走客户端监听的方法,查询该笔订单是否已经支付,如果没有支付的情况下,则将该订单设置为已经超时,库存同时回滚+1。
优点:实现简单,监听的客户端也是可以集群的
缺点:没有重试策略,抗并发性能差,大量的key在同一时间失效会影响到redis客户端监听的性能
需要注意的问题:业务与支付令牌key使用的redis建议完全分开,否则有可能监听到非支付令牌相关的key过期。 - 基于MQ的死信队列
下订单的时候向MQ服务器端投递一个消息(支付id或者订单id),设置有效期为30分钟,如果在30分钟内没有被消费的情况下,则将该消息存入到死信队列中,死信队列监听的消费者获取该消息查询是否已经支付。
基于MQ实现性能优于redis实现。
5 Redis过期了,如何获取value值
每次生成token的时候在数据库表中插入一条记录,当key失效的时候根据token查数据库得到支付id,再去查支付状态,如果订单状态为未支付改为已超时同时库存+1。
6 代码整合Redis过期key事件监听
Redis配置
Redis 开启过期key监听
redis-server.exe ./redis.windows.conf
代码配置
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
System.out.println(message.toString());
}
}
测试效果:
7 代码实现人工补偿订单状态
支付令牌日志表
DROP TABLE IF EXISTS `pay_token_log`;
CREATE TABLE `pay_token_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pay_token` varchar(255) DEFAULT NULL,
`payment_id` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
PayTokenServiceImpl生成token以后加上插入令牌支付日志记录代码
Redis过期监听类
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
private PayTokenLogMapper payTokenLogMapper;
@Autowired
private PaymentTransactionMapper paymentTransactionMapper;
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String tokenKey = message.toString();
PayTokenLogEntity payTokenLogEntity = payTokenLogMapper.selectPayTokenLog(tokenKey);
if (payTokenLogEntity == null) {
return;
}
// 获取支付id
String paymentId = payTokenLogEntity.getPaymentId();
// 根据支付id查询支付的详细信息
PaymentTransactionEntity pte = paymentTransactionMapper.selectByPaymentId(paymentId);
if (pte == null) {
return;
}
// 如果是为待支付情况下 ,则将该笔订单改为已经超时
if (!(PayConstant.PAY_STAY_STATUS.equals(pte.getPaymentStatus()))) {
return;
}
// 调用第三方支付接口再次确认下即可,可以根据用户行为判断支付渠道
int result = paymentTransactionMapper.updatePaymentOvertimeStatus(paymentId);
if (result > 0) {
// 调用库存接口增加库存 ---
}
}
}
测试效果: