延时订单处理:
流程图:打开文件图看得清除
不是所有的订单都是延迟支付的,比如:交话费,游戏充值等。
商城中大部分具有延迟支付的功能,比如:京东淘宝30分钟内支付有效;
分布式首先要解决的是单点故障问题:通过集群解决。
用RocketMQ消息中间件的延迟消息来实现。
问题:在真实环境下,服务会集群,集群中每一个节点都有库存的标记,如何做到分布式场景下的缓存同步?
RocketMQ的广播消费模式:广播消费模式可以给所有的消费者都消费同一个消息(缓存同步)。集群模式消息只会被其中任意一个消费者消费成功(订单处理)。
延迟订单的处理步骤:
发送时机:如果订单创建成功就发送延迟消息
@RocketMQMessageListener使用的常量:
消息对象:需要订单编号和秒杀 id 用来回补库存,因为这个消息仅在订单服务中使用,不放到common中:
在创建订单成功之后,发送延迟消息:
消费者:监听消息中间件,消费延迟消息。注意
检查超时方法的实现:
修改订单状态的方式:
方式1:根据订单号查询订单状态,如果已支付就不管,如果未支付就做后面的操作。
方式2:(推荐)用乐观锁直接进行更新,当状态为0的时候表示未支付,这里更新的是延时之后广播发送来的消息,超时没有支付的订单更新他的状态为4。这样做的好处是更新操作会返回影响行,后面可以根据返回的rows来检查到是否修改成功,同时还完成了修改动作。
修改订单状态的方法:
回补MySQL中库存的方法:
清除本地售完标记:又是发送消息,注意发送消息的模式为广播模式,让所有的集群都修改到标记。
消息用到的常量:
checkOrderTimeOut方法的实现:事务
// 查订单是否超时未支付 @Override @Transactional(rollbackFor = Exception.class) public void checkOrderTimeOut(String orderNo, Long seckillId) { // 1. 检查订单是否超时未支付 // 2.1 如果已经超时:需要将当前订单的状态修改为超时未支付:更新操作 update xx set status = 4 where status = 0 int ret = orderInfoMapper.updateTimeoutStatus(orderNo); if (ret > 0) { // 修改成功:超时未支付的订单 // 2.2 回补MySQL中的库存 orderInfoMapper.incrStockCount(seckillId); // 2.3 回补Redis中的库存,将MySQL中回补好的库存同步到Redis中 String stockCountStr = (String) redisTemplate.opsForHash() .get(RedisKeys.SECKILL_GOOD_STOCKCOUNT_HASH.join(), seckillId + ""); if (!StringUtils.isEmpty(stockCountStr)) { int stockCount = Integer.parseInt(stockCountStr); // 库存 int updateStockCount = 1; if (stockCount > 0) { // 如果库存还大于0,就是原来的库存 + 1,否则设置为 1 updateStockCount = updateStockCount + stockCount; } // 将新库存设置到Redis中,更新Redis中的库存 redisTemplate.opsForHash() .put(RedisKeys.SECKILL_GOOD_STOCKCOUNT_HASH.join(), seckillId + "", updateStockCount + ""); } // 2.4 清除本地售完标记 // 2.4.1 发送广播消息,通知集群的所有节点清除自己的售完标记 rocketMQTemplate.syncSend(MQConstents.CLEAN_STOCKCOUNT_FLAG_DEST, seckillId); } }
消费者:监听修改库存标记的信息,修改库存标记,库存标记是存在常量类型 ConcurrentHashMap 中的
测试:
清除库存标记的效果要演示集群,启动另一个端口的seckill-server服务,修改远程配置仓库的配置文件。拷贝一个配置,启动即可:
效果:
延迟消息只会有其中一个消费到;
修改库存标记消息两个服务都会消费到(因为是广播模式);
流程梳理:流程一定要懂。
秒杀失败:
秒杀失败时,MySQL中的数据不需要进行处理,因为出错了,doSeckill操作有事务,会进行回滚,MySQL中的数据不会发生改变。需要回补Redis中的库存,清除本地售完标记。
修改售完标记:——> RocketMQ应用场景:分布式缓存同步,如:双十一中不同分会场里相同商品的缓存同步。
创建订单失败需要做的处理:
发送失败消息需要的常量:
handlerFailedseckill方法:回补 Redis 库存,清除本地缓存的售完标记
回补Redis库存和清除售完标记的操作和延时未支付订单中的处理有重复,抽离成一个方法:
秒杀失败的监听器:需要通知到客户端,在websocket-server中实现:
测试:
利用创建秒杀订单时设置的唯一索引来测试,或者没有设置库存为0,或者制造异常都可以测试。
幂等性概念
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数。
更复杂的操作幂等保证是利用唯一交易号(流水号)实现。
我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的
幂等性:产生重复调用的时候,保证多次调用只会处理一次业务逻辑。比如说:支付过程中不成功,反复尝试,不会导致积分会增加。
例如:
前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果。
我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;
发送消息,也应该只发一次,同样的短信发给用户,用户会哭的;
创建业务订单,一次业务请求只能创建一个,创建多个就会出大问题。
等等很多重要的情况,这些逻辑都需要幂等的特性来支持。
解决:支付流水表。通用方案:唯一标识
场景:防止表单重复提交。
支付宝支付流程:
支付宝的SDK怎么找到我们本地内网的ip————> 内网穿透 NATAPP
支付宝演示程序:
eclipse项目导入到idea中需要做的操作,熟悉有用:
选择支付宝演示程序,然后选eclipse一直下一步打开;
项目设置:
modules模块设置:
添加依赖:
确定之后自动添加好了
修改web resource
根据提示创建:
然后lib中就有了我们所依赖的jar包,应用,确认即可:
然后项目就可以使用了;
集成支付宝
流程图:
同步通知:只是在前端告诉用户支付结果;
异步通知:支付宝调用商家后台的接口,由商家自己去进行自己的订单支付成功后的业务处理,比如积分、账单等。此时如果因为业务的原因,导致没有相应 "success" 字符给支付宝,支付宝会不断的重试,25小时内重试8次(4m 10m 10m 1h 2h 6h 15h)。这种情况下,支付宝的多次调用,商家的业务代码多次执行(创建发货订单/添加积分等),因此会出现接口的 ”幂等性“ 问题。
解决幂等性:支付流水表。支付流水号做主键,当某一次调用成功创建一条记录以后,之后再进行的调用,因为主键的唯一性,都会创建失败。在业务中做判断,如果流水表创建失败,就认为已经创建过订单了(可以用Redis来做)。这样就保证了相同参数多次执行业务产生的结果是一样的了。
总结:唯一标识来解决。很多场景:mq中的ack确认接收到消息(重试机制);防止表单重复提交(新增用户,跟前端配合用唯一标识id带过去);
集成实现
添加依赖:
<dependency> <groupId>com.alipay.sdkgroupId> <artifactId>alipay-sdk-javaartifactId> <version>3.4.49.ALLversion> dependency>
从样例中拷贝过来的属性类:命名有误:应该是AlipayProperties才合理,而不是AlipayApp。
代码中很多的配置都是写死的,修改一下放到配置文件中管理起来。
将修改好的配置,存进远程的配置仓库中的alipay-dev.yml文件中:
bootstrap.yml中加上这个:自动拉取配置信息
修改属性类:
配置类注入Bean:不通过Bean注入的方式会报错。
@Configuration public class AlipayConfig { @Autowired private AlipayProperties properties; @Bean public AlipayClient alipayConfig() { return new DefaultAlipayClient(properties.getGatewayUrl(), properties.getAppId(), properties.getMerchantPrivateKey(), "json", properties.getCharset(), properties.getAlipayPublickey(), properties.getSigntype()); } }
controller类:复制样例中的代码来进行修改
@RestController @RequestMapping("/api/pay") public class AlipayController { @Autowired private AlipayProperties properties; @Autowired private IOrderInfoService orderInfoService; @Autowired private AlipayClient alipayClient; @RequestMapping public String pay(String orderNo, @UserParam User user) throws AlipayApiException { //获得初始化的AlipayClient 通过配置类Bean注入的方式 //设置请求参数 AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest(); alipayRequest.setReturnUrl(properties.getReturnUrl()); alipayRequest.setNotifyUrl(properties.getNotifyUrl()); // 查询订单相关信息 OrderInfo orderInfo = orderInfoService.findByIdAndUserId(orderNo, user.getId()); if (orderInfo == null) { // 查不到报个错,应该返回CodeMsg给前台 throw new BusinessException(SeckillServerCodeMsg.OPS_ERROR); } //商户订单号,商户网站订单系统中唯一订单号,必填 String out_trade_no = orderNo; //付款金额,必填 String total_amount = orderInfo.getGoodPrice().toString(); //订单名称,必填 String subject = "WolfCode" + orderInfo.getGoodName(); //商品描述,可空 String body = orderInfo.getGoodName() + "商品秒杀价:" + orderInfo.getSeckillPrice(); alipayRequest.setBizContent("{\"out_trade_no\":\"" + out_trade_no + "\"," + "\"total_amount\":\"" + total_amount + "\"," + "\"subject\":\"" + subject + "\"," + "\"body\":\"" + body + "\"," + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}"); //若想给BizContent增加其他可选请求参数,以增加自定义超时时间参数timeout_express来举例说明 //alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\"," // + "\"total_amount\":\""+ total_amount +"\"," // + "\"subject\":\""+ subject +"\"," // + "\"body\":\""+ body +"\"," // + "\"timeout_express\":\"10m\"," // + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}"); //请求参数可查阅【电脑网站支付的API文档-alipay.trade.page.pay-请求参数】章节 //响应 return alipayClient.pageExecute(alipayRequest).getBody(); } }
前台代码:
异常:Bean的名字重复,显示已经存在。
幂等性问题:创建订单也是存在幂等性的问题的(如果执行多次创建业务,结果不是唯一的,可能有多个订单被创建),我们还没有处理。 支付宝异步通知后台执行修改订单状态的回调有一个重试机制,存在幂等性问题。
同步回调:直接通知用户支付成功或失败
官方文档的时序图:
调用顺序如下:
商户系统请求支付宝接口 alipay.trade.page.pay,支付宝对商户请求参数进行校验,而后重新定向至用户登录页面。
用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。
交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。
若由于网络等问题异步通知没有到达,商户可自行调用交易查询接口 alipay.trade.query 进行查询,根据查询接口获取交易以及支付信息(商户也可以直接调用查询接口,不需要依赖异步通知)。
注意:
由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
商户系统接收到异步通知以后,必须通过验签(验证通知中的 sign 参数)来确保支付通知是由支付宝发送的。详细验签规则参考异步通知验签。
接收到异步通知并验签通过后,一定要检查通知内容,包括通知中的 app_id、out_trade_no、total_amount 是否与请求中的一致,并根据 trade_status 进行后续业务处理。
在支付宝端,partnerId 与 out_trade_no 唯一对应一笔单据,商户端保证不同次支付 out_trade_no 不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。
拓展:退款流程看官方文档里的快速接入。
修改内网穿透的随机生成的最新的网址;修改returnUrl和notifyUrl跳转的路径:
验签:拷贝代码
@RequestParam这个注解是干嘛的:使用非基本数据类型,贴上之后序列化?
修改拷贝过来的代码:
支付成功,重定向到订单详情页;失败重定向到错误提示页面。
异步回调:
对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商户传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统。
如果业务处理成功返回success字符串;
复制代码进行修改;
看官方文档,支付宝返回的参数有很多,开发中需要的参数可以看文档可以获取,比如流水号。
支付宝异步通知的页面特性:
服务器间的交互,不像页面跳转同步通知可以在页面上显示出来,这种交互方式是不可见的;
第一次交易状态改变(即时到账中此时交易状态是交易完成)时,不仅会返回同步处理结果,而且服务器异步通知页面也会收到支付宝发来的处理结果通知;
程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知(此处需要开发人员注意幂等性问题),直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);
程序执行完成后,该页面不能执行页面跳转。如果执行页面跳转,支付宝会收不到 success 字符,会被支付宝服务器判定为该页面程序运行出现异常,而重发处理结果通知;
该方式的作用主要防止订单丢失,即页面跳转同步通知没有处理订单更新,它则去处理;
当商户收到服务器异步通知并打印出 success 时,服务器异步通知参数 notify_id 才会失效。也就是说在支付宝发送同一条异步通知时(包含商户并未成功打印出 success 导致支付宝重发数次通知),服务器异步通知参数 notify_id 是不变的。
处理异步通知的实现:部分注释被收起来,详见项目。
updatePaySuccess修改订单状态的方法:乐观锁
它们只能有一个修改成功,并且先修改的执行完之后,后一个就必定失败,因为状态是不一样的。
测试:
加日志打印信息测试:
查看同步回调和异步的回调日志是否打印。