6.3、支付商品
6.3、支付商品
6.3.1、支付
1、相关概念
1、文档
支付宝电脑网站解锁
文档链接:https://opendocs.alipay.com/open/270/105898?ref=api

文档链接2:https://opendocs.alipay.com/open/270/105898

网址: https://open.alipay.com/develop/pm/create

2、对称秘钥
加密-对称加密
发送方和接收方都拥有同样的秘钥,当发送方想要发送消息时,使用秘钥将明文进行加密,在网络中传输加密后的密文数据,接收方收到密文数据后使用相同的秘钥进行解密

DES
、3DES
(TripleDES
)、AES
、RC2
、RC4
、RC5
和Blowfish
等,加密解密使用同一把钥匙
3、非对称秘钥
加密-非对称加密
非对称加密的公钥都是公开的,只有自己拥有属于自己的秘钥,当发送方想要发送消息时,使用接收方的公钥进行加密(只有接收方的秘钥才能解开使用接收方的公钥加密的密文),在网络中传输加密后的数据,当接收方接收到消息时,使用接收方自己的秘钥进行解密。同理,当接收方想要发送消息时,使用发送方的公钥进行加密(只有发送方的秘钥才能解开使用发送方的公钥加密的密文),在网络中传输加密后的数据,当发送方接收到消息时,使用发送方自己的秘钥进行解密。(即想要发送数据给谁,就是用对方的公钥进行加密,对方收到数据后,可以通过对方的秘钥进行解密)

RSA
、Elgamal
等,加密解密使用不同钥匙
2、支付宝支付
1、电脑网站支付产品介绍
电脑网站支付产品介绍: https://opendocs.alipay.com/open/270

2、SDK & Demo
下载demo: https://opendocs.alipay.com/open/270/106291

3、配置使用沙箱进行测试
1、使用 RSA 工具生成签名 2、下载沙箱版钱包 3、运行官方 demo 进行测试
4、公钥 & 私钥
什么是公钥、私钥、加密、签名和验签?
1、公钥私钥 公钥和私钥是一个相对概念 它们的公私性是相对于生成者来说的。 一对密钥生成后,保存在生成者手里的就是私钥, 生成者发布出去大家用的就是公钥
5、加密 & 数字签名
加密和数字签名概念 加密:
- 我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解密的技术。
- 公钥和私钥都可以用来加密,也都可以用来解密。
- 但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。
- 加密的目的是: 为了确保数据传输过程中的不可读性,就是不想让别人看到。
签名:
- 给我们将要发送的数据,做上一个唯一签名(类似于指纹)
- 用来互相验证接收方和发送方的身份;
- 在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以用来达到数据的明文传输。
验签
- 支付宝为了验证请求的数据是否商户本人发的
- 商户为了验证响应的数据是否支付宝发的
签名是先计算哈希值然后用私钥加密,私钥对外不公开所以不能自己生成签名(下面这个图应该有问题,商户给支付宝发消息应该使用支付宝的公钥进行加密,支付宝收到消息后使用支付宝自己的私钥进行解密;支付宝业务处理完成后,使用商户的公钥对消息进行加密,商户收到消息后使用商户自己的私钥进行解密)

3、使用支付宝提供的demo
1、开启沙箱
在 https://open.alipay.com/develop/sandbox/app 页面里开启沙箱 ,开启沙箱后,在开放平台控制台
-> 沙箱
-> 控制台
->网页/移动应用
-> 应用信息
-> 开发信息
在接口加签方式
里选择系统默认秘钥
,点击公钥模式
右边启用
按钮,然后点击查看
,即可看到应用公钥
、应用私钥
、支付宝公钥
信息

在开放平台控制台
-> 沙箱
-> 控制台
->网页/移动应用
-> 应用信息
-> 基本信息
里复制APPID

2、调试demo
在 https://opendocs.alipay.com/open/270/106291 页面里下载demo
后使用eclipse
打开,我这里使用的软件是Spring Tools 4 for Eclipse
,粘贴刚刚复制的APPID
到com.alipay.config.AlipayConfig
的app_id
字段里

点击控制台
->网页/移动应用
-> 应用信息
-> 开发信息
-> 接口加签方式
里选择系统默认秘钥
,点击公钥模式
的查看
按钮,点击应用私钥
里的复制私钥

粘贴刚刚复制的应用私钥
到com.alipay.config.AlipayConfig
类的merchant_private_key
字段里

点击控制台
->网页/移动应用
-> 应用信息
-> 开发信息
-> 接口加签方式
里选择系统默认秘钥
,点击公钥模式
的查看
按钮,点击支付宝公钥
的复制公钥

粘贴刚刚复制的支付宝公钥
到com.alipay.config.AlipayConfig
类的alipay_public_key
字段里

在开放平台控制台
-> 沙箱
-> 控制台
->网页/移动应用
-> 应用信息
-> 开发信息
里复制支付宝网关地址
的值

粘贴刚刚复制的支付宝网关地址
到com.alipay.config.AlipayConfig
类的gatewayUrl
字段里

3、测试
运行demo项目,访问 http://localhost:8080/alipay.trade.page.pay-JAVA-UTF-8 页面,点demo里的付款
按钮,然后使用沙箱提供的沙箱账号
的买家信息
的买家账号
去支付,可以看到能够支付成功,但是支付成功的回调url如果要是用户访问的话必须是公网可以访问的。(即支付宝在公网上能够访问的接口)

4、再次测试
修改com.alipay.config.AlipayConfig
类的notify_url
字段和return_url
字段,将地址改成本地的,这样使用自己电脑可以跳转页面,但是异步通知会访问不到。(异步通知就是支付成功后,支付宝会不断地向该接口发送支付成功的消息,直到我们的接口返回确认收到后,支付宝才停止通知)

重启项目,支付成功后即可跳转到我们设置的支付成功回调页

4、整合支付宝支付
1、添加依赖
在gulimall-order
模块的pom.xml
文件里添加alipay
的依赖
<!--支付宝的SDK-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>

2、添加代码
复制2.分布式高级篇(微服务架构篇)\资料源码\代码\支付
里的PayVo.java
,粘贴到gulimall-order
模块的com.atguigu.gulimall.order.vo
包下
package com.atguigu.gulimall.order.vo;
import lombok.Data;
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}

复制2.分布式高级篇(微服务架构篇)\资料源码\代码\支付
里的AlipayTemplate.java
,粘贴到gulimall-order
模块的com.atguigu.gulimall.order.config
包下,修改AlipayTemplate
类里导入的PayVo
类的路径

在gulimall-order
模块的com.atguigu.gulimall.order.config.AlipayTemplate
类里,修改app_id
、merchant_private_key
、alipay_public_key
、gatewayUrl
这些字段的值

将gulimall-order
模块的com.atguigu.gulimall.order.vo.PayVo
类里的字段修改为小驼峰命名法
package com.atguigu.gulimall.order.vo;
import lombok.Data;
@Data
public class PayVo {
/**
* 商户订单号 必填
*/
private String outTradeNo;
/**
* 订单名称 必填
*/
private String subject;
/**
* 付款金额 必填
*/
private String totalAmount;
/**
* 商品描述 可空
*/
private String body;
}

修改后的gulimall-order
模块的com.atguigu.gulimall.order.vo.PayVo
类

3、不能调用商品服务
重启GulimallOrderApplication
服务,测试时发现GulimallCartApplication
模块的控制台报不能成功调用gulimall-product
服务的错误
com.netflix.client.ClientException: Load balancer does not have available server for client: gulimall-product
at com.netflix.loadbalancer.LoadBalancerContext.getServerFromLoadBalancer(LoadBalancerContext.java:483) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
at com.netflix.loadbalancer.reactive.LoadBalancerCommand$1.call(LoadBalancerCommand.java:184) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
......
at com.atguigu.gulimall.cart.service.impl.CartServiceImpl.lambda$getUserCartItems$2(CartServiceImpl.java:170) ~[classes/:na]
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) ~[na:1.8.0_301]
at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175) ~[na:1.8.0_301]
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384) ~[na:1.8.0_301]
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482) ~[na:1.8.0_301]
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472) ~[na:1.8.0_301]
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) ~[na:1.8.0_301]
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_301]
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) ~[na:1.8.0_301]
at com.atguigu.gulimall.cart.service.impl.CartServiceImpl.getUserCartItems(CartServiceImpl.java:175) ~[classes/:na]
at com.atguigu.gulimall.cart.controller.CartController.getCurrentUserCartItems(CartController.java:31) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_301]

在gulimall-product
模块的src/main/resources/application.yml
文件里添加如下配置
spring:
application:
name: gulimall-product

改完后,重启GulimallProductApplication
服务,这个GulimallCartApplication
服务就不报错了,还有很多别的服务之间相互调用都失败了,查看nacos
,可以看到这些服务明明都在

重启nacos
后,发现所有的页面都访问不了,查看网关报了以下错误,重启一下网关就好了,什么奇葩bug
java.lang.NullPointerException: null
at com.alibaba.nacos.client.naming.core.PushReceiver.getUDPPort(PushReceiver.java:116) ~[nacos-client-1.1.1.jar:na]
at com.alibaba.nacos.client.naming.core.HostReactor.updateServiceNow(HostReactor.java:270) ~[nacos-client-1.1.1.jar:na]
at com.alibaba.nacos.client.naming.core.HostReactor$UpdateTask.run(HostReactor.java:315) [nacos-client-1.1.1.jar:na]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_301]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_301]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_301]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_301]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_301]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_301]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_301]
2022-08-20 20:22:48.784 ERROR 11596 --- [.naming.updater] com.alibaba.nacos.client.naming : [NA] failed to update serviceName: DEFAULT_GROUP@@gulimall-order
java.lang.NullPointerException: null
at com.alibaba.nacos.client.naming.core.PushReceiver.getUDPPort(PushReceiver.java:116) ~[nacos-client-1.1.1.jar:na]
at com.alibaba.nacos.client.naming.core.HostReactor.updateServiceNow(HostReactor.java:270) ~[nacos-client-1.1.1.jar:na]
at com.alibaba.nacos.client.naming.core.HostReactor$UpdateTask.run(HostReactor.java:315) [nacos-client-1.1.1.jar:na]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_301]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_301]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_301]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_301]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_301]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_301]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_301]
2022-08-20 20:22:49.034 ERROR 11596 --- [.naming.updater] com.alibaba.nacos.client.naming : [NA] failed to update serviceName: DEFAULT_GROUP@@gulimall-product

4、不显示真是应付金额
登陆后,点击 http://order.gulimall.com/toTrade 页面的提交订单
按钮,来到了 http://order.gulimall.com/submitOrder 页面,在这个页面里突然又不显示真实数据了

在gulimall-order
模块的src/main/resources/templates/pay.html
页面里搜索订单号
,修改相关的代码
<dd>
<span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
<span>应付金额<font>[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font>元</span>
</dd>

重启GulimallOrderApplication
服务,在 http://order.gulimall.com/submitOrder 页面里刷新一下,现在成功显示了

5、添加支付逻辑
1、修改页面
在 http://order.gulimall.com/submitOrder 页面里,打开控制台,定位到支付宝
图标,复制支付宝

在gulimall-order
模块的src/main/resources/templates/pay.html
文件里搜索支付宝
,修改相关代码。
<div class="Jd_footer">
<ul>
<li>
<img src="/static/order/pay/img\weixin.png" alt="">微信支付
</li>
<li>
<img src="/static/order/pay/img\zhifubao.png" style="weight:auto;height:30px;" alt="">
<a th:href="'http://order.gulimall.com/payOrder?orderSn='+${submitOrderResp.order.orderSn}">支付宝</a>
</li>
</ul>
</div>

2、修改代码
在gulimall-order
模块的com.atguigu.gulimall.order.web
包里新建PayWebController
类
package com.atguigu.gulimall.order.web;
import com.alipay.api.AlipayApiException;
import com.atguigu.gulimall.order.config.AlipayTemplate;
import com.atguigu.gulimall.order.service.OrderService;
import com.atguigu.gulimall.order.vo.PayVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author 无名氏
* @date 2022/8/20
* @Description:
*/
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
@GetMapping("/payOrder")
@ResponseBody
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payVo = orderService.getOrderPay(orderSn);
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
}

在gulimall-order
模块的com.atguigu.gulimall.order.service.OrderService
接口里太内疚getOrderPay
抽象方法
PayVo getOrderPay(String orderSn);

在gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类里实现getOrderPay
方法
@Override
public PayVo getOrderPay(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity orderEntity = this.getOrderStatusByOrderSn(orderSn);
BigDecimal totalAmount = orderEntity.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotalAmount(totalAmount.toString());
payVo.setOutTradeNo(orderEntity.getOrderSn());
LambdaQueryWrapper<OrderItemEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderItemEntity::getOrderSn,orderSn).last(" limit 1");;
OrderItemEntity orderItemEntity = orderItemService.getOne(lambdaQueryWrapper);
payVo.setSubject(orderItemEntity.getSkuName());
payVo.setBody(orderItemEntity.getSkuAttrsVals());
return payVo;
}

3、测试
重启GulimallOrderApplication
服务,点击 http://order.gulimall.com/toTrade 页面的提交订单
按钮,来到了 http://order.gulimall.com/submitOrder 页面,点击支付宝
图标就跳转到支付宝的页面了,此时查看GulimallOrderApplication
服务的控制台,可以看到已经成功输出了支付宝返回的信息了

支付宝返回了一个<form>
表单和一个<script>
用来提交表单,而且是使用<script>
直接提交的
<form name="punchout_form" method="post" action="https://openapi.alipaydev.com/gateway.do?charset=utf-8&method=alipay.trade.page.pay&sign=QpWCjWl8avWnIjdrY5RFM8dv6TBjQI3escBJcCml%2B2g6tQaWQCjCtm5EgsnNZvlKVFcGl4oBLzpZEP0fFrlvZsinrLX3uIkka6zumCUT246hbd8rhT4utMS%2Bup%2BtsQwVB5Du16UzkE%2Bsd8WC37EUCg%2F%2Bd5%2FtR%2FoS7f2M8RYd%2B5oo0OgpGqEglOxQINIGyD%2Bg%2FCUeC2GmmK1q0a%2F07c8XCxUbPG0LPj3Opya%2F8V2Bn8PlNaQ25iEh%2BbnFjOhk1GPL3YDe8USR4KAluhylW4eHW9EDvSzEwUe3avub2l3Kt6WSdsJRfV8ux6HyIO7QdYFa3cCB3ZKS0xL1QItCs%2B83OA%3D%3D&version=1.0&app_id=2021000117672941&sign_type=RSA2×tamp=2022-08-20+21%3A35%3A11&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
<input type="hidden" name="biz_content" value="{"out_trade_no":"202208202135096131560983780531920898","total_amount":"47401.00","subject":"华为 HUAWEI Mate30Pro 罗兰紫 8GB+128GB 麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄 4G全网通手机","body":"颜色:罗兰紫;版本:8GB+128GB","product_code":"FAST_INSTANT_TRADE_PAY"}">
<input type="submit" value="立即支付" style="display:none" >
</form>
<script>document.forms[0].submit();</script>

在gulimall-order
模块的com.atguigu.gulimall.order.web.PayWebController
类里修改payOrder
方法,将@GetMapping("/payOrder")
修改为@GetMapping(value = "/payOrder",produces = "text/html")
其实我更推荐使用@GetMapping(value = "/payOrder",produces = MediaType.TEXT_HTML_VALUE)

修改gulimall-order
模块的com.atguigu.gulimall.order.config.AlipayTemplate
类里的returnUrl
字段
private String returnUrl="http://member.gulimall.com/memberOrder.html";

4、添加页面
将2.分布式高级篇(微服务架构篇)\资料源码\代码\html\订单页
里的index.html
复制到在gulimall-member
模块的src/main/resources/templates
文件夹里面,并将刚刚粘贴的index.html
重命名为orderList.html

在linux
虚拟机里的/mydata/nginx/html/static
目录下新建member
目录,将2.分布式高级篇(微服务架构篇)\资料源码\代码\html\订单页
里的文件夹全部复制到linux虚拟机里的/mydata/nginx/html/static/member
目录下(不包括index.html
)

修改gulimall-member
模块的src/main/resources/templates/orderList.html
文件,将href="
全部替换为href="/static/member/
,将src="
全部替换为src="/static/member/
href="
href="/static/member/
src="
src="/static/member/

5、添加配置
在gulimall-member
模块的pom.xml
文件里添加thymeleaf
依赖
<!--模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在gulimall-member
模块的src/main/resources/application.properties
文件里添加如下配置,关闭thymeleaf
缓存
spring.thymeleaf.cache=false

在gulimall-member
模块的src/main/resources/templates/orderList.html
文件里,将<html lang="en">
修改为<html lang="en" xmlns:th="http://www.thymeleaf.org">

在gulimall-member
模块的com.atguigu.gulimall.member
包下新建interceptor
文件夹。复制gulimall-order
模块的com.atguigu.gulimall.order.interceptor.LoginUserInterceptor
类,粘贴到gulimall-member
模块的com.atguigu.gulimall.member.interceptor
包下。

在gulimall-member
模块的com.atguigu.gulimall.member.config
包下新建MemberWebConfig
类
package com.atguigu.gulimall.member.config;
import com.atguigu.gulimall.member.interceptor.LoginUserInterceptor;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 无名氏
* @date 2022/8/21
* @Description:
*/
@Controller
public class MemberWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginUserInterceptor()).addPathPatterns("/**");
}
}

复制gulimall-product
模块的com.atguigu.gulimall.product.config.GulimallSessionConfig
类,粘贴到gulimall-member
模块的com.atguigu.gulimall.member.config
包下。
点击查看GulimallSessionConfig类完整代码

在gulimall-gateway
模块的src/main/resources/application.yml
配置文件里添加如下配置
spring:
cloud:
gateway:
routes:
- id: gulimall_member_route
uri: lb://gulimall-member
predicates:
- Host=member.gulimall.com

在hosts
文件里添加member.gulimall.com
网址对应的ip
# gulimall
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
192.168.56.10 auth.gulimall.com
192.168.56.10 cart.gulimall.com
192.168.56.10 order.gulimall.com
192.168.56.10 member.gulimall.com

6、修改页面和配置
在 http://gulimall.com/ 页面里,打开控制台,定位到我的订单
图标,复制我的订单

在gulimall-product
模块的src/main/resources/templates/index.html
文件里,把<a href="http://order.gulimall.com/list.html">我的订单</a>
修改为<a href="http://member.gulimall.com/memberOrder.html">我的订单</a>
<li>
<a href="http://member.gulimall.com/memberOrder.html">我的订单</a>
</li>

在gulimall-member
模块的pom.xml
文件里引入redis
和SpringSession
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--引入SpringSession-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

在gulimall-member
模块的src/main/resources/application.properties
文件里添加redis
配置
spring.redis.host=192.168.56.10
spring.session.store-type=redis

在gulimall-member
模块的com.atguigu.gulimall.member.GulimallMemberApplication
启动文件里添加如下注解,开启Spring Session
功能
@EnableRedisHttpSession

7、测试
在 http://gulimall.com/ 页面里点击我的订单
,来到了 http://auth.gulimall.com/login.html 登录页面,点击gitee
登录,发现GulimallAuthServerApplication
服务报错了,提示访问http://gulimall-member/member/member/giteeLogin
失败了,这应该是拦截器把请求拦截了,让其跳转到登录页了。

GulimallAuthServerApplication
服务控制台报的错误如下所示
feign.RetryableException: cannot retry due to redirection, in streaming mode executing POST http://gulimall-member/member/member/giteeLogin
at feign.FeignException.errorExecuting(FeignException.java:132)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:113)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
at com.sun.proxy.$Proxy95.giteeLogin(Unknown Source)
at com.atguigu.gulimall.auth.service.impl.OAuth2ServiceImpl.giteeRegister(OAuth2ServiceImpl.java:53)
at com.atguigu.gulimall.auth.controller.OAuth2Controller.giteeRegister(OAuth2Controller.java:34)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)

在gulimall-member
模块的com.atguigu.gulimall.member.interceptor.LoginUserInterceptor
类里修改preHandle
方法,把boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
修改为boolean match = new AntPathMatcher().match("/member/member/giteeLogin", uri);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/member/member/giteeLogin", uri);
if (match){
return true;
}
Object attribute = request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
MemberEntityTo memberEntityTo= (MemberEntityTo) attribute;
loginUser.set(memberEntityTo);
return true;
}else {
request.getSession().setAttribute("msg","请先进行登录");
//没登陆就重定向到登录页面
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}

登陆后,在 http://gulimall.com/ 页面里点击我的订单
,就跳转到 http://member.gulimall.com/memberOrder.html 页面了

访问 http://order.gulimall.com/toTrade 页面,打开控制台,点击Network
,查看请求可以发现,以下请求访问失败了
http:/api/ware/wareinfo/fare?addrId=1
返回的响应为:
{"timestamp":"2022-08-21 09-54-39","status":500,"error":"Internal Server Error","message":"Could not extract response: no suitable HttpMessageConverter found for response type [class com.atguigu.common.utils.R] and content type [text/html;charset=UTF-8]","path":"/ware/wareinfo/fare"}

查看失败的 http://gulimall.com/api/ware/wareinfo/fare?addrId=1 这个请求,可以发现报的是500
的错误

查看GulimallWareApplication
服务的控制台,报了如下的错误,显示返回的是text/html
类型
2022-08-21 09:54:39.338 ERROR 2140 --- [io-11000-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.codec.DecodeException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.atguigu.common.utils.R] and content type [text/html;charset=UTF-8]] with root cause
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.atguigu.common.utils.R] and content type [text/html;charset=UTF-8]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:121) ~[spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.cloud.openfeign.support.SpringDecoder.decode(SpringDecoder.java:59) ~[spring-cloud-openfeign-core-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.cloud.openfeign.support.ResponseEntityDecoder.decode(ResponseEntityDecoder.java:62) ~[spring-cloud-openfeign-core-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at feign.optionals.OptionalDecoder.decode(OptionalDecoder.java:36) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:176) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:140) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78) ~[feign-core-10.2.3.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-10.2.3.jar:na]
at com.sun.proxy.$Proxy111.addrInfo(Unknown Source) ~[na:na]
at com.atguigu.gulimall.ware.service.impl.WareInfoServiceImpl.getFare(WareInfoServiceImpl.java:62) ~[classes/:na]
at com.atguigu.gulimall.ware.service.impl.WareInfoServiceImpl$$FastClassBySpringCGLIB$$55890fdc.invoke

8、修改代码
在gulimall-ware
模块的com.atguigu.gulimall.ware.feign.MemberFeignService
接口里的addrInfo
方法调用了gulimall-member
模块的/member/memberreceiveaddress/info/{id}
返回了text/html

修改gulimall-member
模块的com.atguigu.gulimall.member.interceptor.LoginUserInterceptor
类的preHandle
方法
boolean match = new AntPathMatcher().match("/member/**", uri);

重启GulimallMemberApplication
服务,在 http://order.gulimall.com/toTrade 页面里点击提交订单
后,来到 http://order.gulimall.com/submitOrder 页面,然后点击支付宝支付,就跳转到了支付宝的支付页面,支付成功后也能正确跳转到 http://member.gulimall.com/memberOrder.html 我的订单页

6.3.2、完善支付功能
1、添加接口
1、查询订单项
在gulimall-order
模块的com.atguigu.gulimall.order.controller.OrderController
类里添加listWithItem
方法
@RequestMapping("/listWithItem")
public R listWithItem(@RequestParam Map<String, Object> params) {
PageUtils page = orderService.queryPageWithItem(params);
return R.ok().put("page", page);
}

在gulimall-order
模块的com.atguigu.gulimall.order.service.OrderService
接口里添加queryPageWithItem
抽象方法
PageUtils queryPageWithItem(Map<String, Object> params);

在gulimall-order
模块的com.atguigu.gulimall.order.entity.OrderEntity
类里添加itemEntities
字段
private List<OrderItemEntity> itemEntities;

在gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类里添加queryPageWithItem
方法
@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
LambdaQueryWrapper<OrderEntity> orderQueryWrapper = new LambdaQueryWrapper<>();
orderQueryWrapper.eq(OrderEntity::getMemberId,memberEntityTo.getId());
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
orderQueryWrapper
);
List<OrderEntity> collect = page.getRecords().stream().map(orderEntity -> {
LambdaQueryWrapper<OrderItemEntity> orderItemQueryWrapper = new LambdaQueryWrapper<>();
orderItemQueryWrapper.eq(OrderItemEntity::getOrderSn, orderEntity.getOrderSn());
orderItemService.list(orderItemQueryWrapper);
return orderEntity;
}).collect(Collectors.toList());
page.setRecords(collect);
return new PageUtils(page);
}

在gulimall-order
模块的com.atguigu.gulimall.order.controller.OrderController
类修改listWithItem
方法,将@RequestParam
修改为@RequestBody
,@RequestMapping("/listWithItem")
修改为@PostMapping("/listWithItem")

在gulimall-member
模块的com.atguigu.gulimall.member.feign
包里新建OrderFeignService
接口,用于远程调用订单服务
package com.atguigu.gulimall.member.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;
/**
* @author 无名氏
* @date 2022/8/21
* @Description:
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
@PostMapping("/order/order/listWithItem")
public R listWithItem(@RequestBody Map<String, Object> params);
}

在gulimall-member
模块的com.atguigu.gulimall.member.web.MemberWebController
类里修改memberOrderPage
方法
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){
//查出当前登录的用户的所有订单列表数据
Map<String,Object> page = new HashMap<>();
page.put("page",pageNum);
R r = orderFeignService.listWithItem(page);
model.addAttribute("orders",r);
return "orderList";
}

2、不能够重定向
重启GulimallMemberApplication
服务,浏览器打开 http://member.gulimall.com/memberOrder.html 页面,可以看到报了如下的错误
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Aug 21 11:24:28 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
cannot retry due to redirection, in streaming mode executing POST http://gulimall-order/order/order/listWithItem

查看GulimallMemberApplication
服务的控制台,报了如下的错误
2022-08-21 11:24:28.500 ERROR 2248 --- [nio-8000-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.RetryableException: cannot retry due to redirection, in streaming mode executing POST http://gulimall-order/order/order/listWithItem] with root cause
java.net.HttpRetryException: cannot retry due to redirection, in streaming mode
at sun.net.www.protocol.http.HttpURLConnection.followRedirect0(HttpURLConnection.java:2665) ~[na:1.8.0_301]
at sun.net.www.protocol.http.HttpURLConnection.followRedirect(HttpURLConnection.java:2651) ~[na:1.8.0_301]
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1830) ~[na:1.8.0_301]
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498) ~[na:1.8.0_301]
at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480) ~[na:1.8.0_301]
at feign.Client$Default.convertResponse(Client.java:143) ~[feign-core-10.2.3.jar:na]
at feign.Client$Default.execute(Client.java:68) ~[feign-core-10.2.3.jar:na]

调试程序发现template
里的target
为 http://gulimall-order ,uriTemplate
里的template
值为/order/order/listWithItem

然后放行response = client.execute(request, options);
,显示响应的responseCode
为302
,detailMessage
的值为cannot retry due to redirection, in streaming mode
,cause
-> location
的值为http://auth.gulimall.com/login.html

复制gulimall-order
模块的com.atguigu.gulimall.order.config.GuliFeignConfig
类,粘贴到gulimall-member
模块的com.atguigu.gulimall.member.config
包下。

3、内部服务异常
重启GulimallMemberApplication
服务,浏览器访问 http://member.gulimall.com/memberOrder.html 页面,报了如下的错误。
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Aug 21 11:38:34 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
status 500 reading OrderFeignService#listWithItem(Map)

查看GulimallMemberApplication
服务的控制台,可以看到是远程调用的问题。
2022-08-21 11:38:34.967 ERROR 7032 --- [nio-8000-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$InternalServerError: status 500 reading OrderFeignService#listWithItem(Map)] with root cause
feign.FeignException$InternalServerError: status 500 reading OrderFeignService#listWithItem(Map)
at feign.FeignException.errorStatus(FeignException.java:114) ~[feign-core-10.2.3.jar:na]
at feign.FeignException.errorStatus(FeignException.java:86) ~[feign-core-10.2.3.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:93) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:149) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78) ~[feign-core-10.2.3.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-10.2.3.jar:na]
at com.sun.proxy.$Proxy111.listWithItem(Unknown Source) ~[na:na]
at com.atguigu.gulimall.member.web.MemberWebController.memberOrderPage(MemberWebController.java:30) ~[classes/:na]

查看GulimallOrderApplication
服务的控制台,发生了Integer
类型不能被强转成String
类型的异常
2022-08-21 11:38:34.902 ERROR 9820 --- [nio-9000-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String] with root cause
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.atguigu.common.utils.Query.getPage(Query.java:37) ~[classes/:na]
at com.atguigu.common.utils.Query.getPage(Query.java:28) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.queryPageWithItem(OrderServiceImpl.java:256) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl$$FastClassBySpringCGLIB$$99092a92.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl$$EnhancerBySpringCGLIB$$309f4bf3.queryPageWithItem(<generated>) ~[classes/:na]
at com.atguigu.gulimall.order.controller.OrderController.listWithItem(OrderController.java:55) ~[classes/:na]

修改gulimall-member
模块的com.atguigu.gulimall.member.web.MemberWebController
类的memberOrderPage
方法,将pageNum
修改为pageNum.toString()
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){
//查出当前登录的用户的所有订单列表数据
Map<String,Object> page = new HashMap<>();
page.put("page",pageNum.toString());
R r = orderFeignService.listWithItem(page);
model.addAttribute("orders",r);
return "orderList";
}

在gulimall-member
模块的com.atguigu.gulimall.member.web.MemberWebController
类里修改memberOrderPage
方法,在R r = orderFeignService.listWithItem(page);
这行代码后面添加System.out.println(JSON.toJSON(r));
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){
//查出当前登录的用户的所有订单列表数据
Map<String,Object> page = new HashMap<>();
page.put("page",pageNum.toString());
R r = orderFeignService.listWithItem(page);
System.out.println(JSON.toJSON(r));
model.addAttribute("orders",r);
return "orderList";
}

item_entities
字段
4、不知道重启GulimallMemberApplication
服务,重新测试,报了Unknown column 'item_entities' in 'field list'
异常,这个错误是MybatisPlus
找不到Java
实体类的某个字段在数据库对应的列
2022-08-21 11:43:31.542 ERROR 4852 --- [nio-9000-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.jdbc.BadSqlGrammarException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'item_entities' in 'field list'
### The error may exist in com/atguigu/gulimall/order/dao/OrderDao.java (best guess)
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT id,note,delivery_time,integration_amount,order_sn,bill_receiver_email,discount_amount,receiver_province,bill_content,coupon_id,receiver_city,auto_confirm_day,delivery_sn,coupon_amount,modify_time,receiver_phone,pay_type,pay_amount,receiver_region,receiver_post_code,delete_status,member_username,confirm_status,payment_time,bill_header,item_entities,member_id,freight_amount,receiver_name,bill_type,use_integration,receiver_detail_address,delivery_company,comment_time,receive_time,bill_receiver_phone,total_amount,source_type,create_time,integration,growth,promotion_amount,status FROM oms_order WHERE (member_id = ?)
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'item_entities' in 'field list'
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Unknown column 'item_entities' in 'field list'] with root cause
java.sql.SQLSyntaxErrorException: Unknown column 'item_entities' in 'field list'
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120) ~[mysql-connector-java-8.0.17.jar:8.0.17]

在gulimall-order
模块的com.atguigu.gulimall.order.entity.OrderEntity
类的itemEntities
字段上面添加@TableField(exist = false)
注解,告诉MybatisPlus
这个字段mysql对应的表里不存在
@TableField(exist = false)
private List<OrderItemEntity> itemEntities;

5、添加配置
复制gulimall-product
模块的com.atguigu.gulimall.product.config.MyBatisConfig
类,粘贴到gulimall-member
模块的com.atguigu.gulimall.member.config
包下。修改@MapperScan("com.atguigu.gulimall.product.dao")
为@MapperScan("com.atguigu.gulimall.member.dao")

重新启动GulimallMemberApplication
服务,再次测试,这时GulimallMemberApplication
服务的控制台就输出了如下的json
{"msg":"success","code":0,"page":{"totalCount":0,"pageSize":10,"totalPage":0,"currPage":1,"list":[{"id":1,"memberId":7,"orderSn":"202208211151528791561199381468717057","memberUsername":"无名氏","totalAmount":47392.0,"payAmount":47401.0,"freightAmount":9.0,"promotionAmount":0.0,"integrationAmount":0.0,"couponAmount":0.0,"status":0,"autoConfirmDay":7,"integration":47392,"growth":47392,"receiverPhone":"12345678910","receiverProvince":"上海市","receiverDetailAddress":"上海市松江区大厦6层","deleteStatus":0,"modifyTime":"2022-08-21T03:51:53.000+0000"},{"id":2,"memberId":7,"orderSn":"202208211153075861561199694800003073","memberUsername":"无名氏","totalAmount":47392.0,"payAmount":47406.0,"freightAmount":14.0,"promotionAmount":0.0,"integrationAmount":0.0,"couponAmount":0.0,"status":0,"autoConfirmDay":7,"integration":47392,"growth":47392,"receiverPhone":"12345678910","receiverProvince":"北京市","receiverDetailAddress":"北京市昌平区宏福科技园","deleteStatus":0,"modifyTime":"2022-08-21T03:53:08.000+0000"},{"id":4,"memberId":7,"orderSn":"202208211154011001561199919253987330","memberUsername":"无名氏","totalAmount":47392.0,"payAmount":47406.0,"freightAmount":14.0,"promotionAmount":0.0,"integrationAmount":0.0,"couponAmount":0.0,"status":0,"autoConfirmDay":7,"integration":47392,"growth":47392,"receiverPhone":"12345678910","receiverProvince":"北京市","receiverDetailAddress":"北京市昌平区宏福科技园","deleteStatus":0,"modifyTime":"2022-08-21T03:54:01.000+0000"}]}}

格式化后的json
文件如下图所示

在gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类里,修改queryPageWithItem
方法,忘记把订单项设置给orderEntity
了,加上即可。
@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
LambdaQueryWrapper<OrderEntity> orderQueryWrapper = new LambdaQueryWrapper<>();
orderQueryWrapper.eq(OrderEntity::getMemberId,memberEntityTo.getId());
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
orderQueryWrapper
);
List<OrderEntity> collect = page.getRecords().stream().map(orderEntity -> {
LambdaQueryWrapper<OrderItemEntity> orderItemQueryWrapper = new LambdaQueryWrapper<>();
orderItemQueryWrapper.eq(OrderItemEntity::getOrderSn, orderEntity.getOrderSn());
List<OrderItemEntity> list = orderItemService.list(orderItemQueryWrapper);
orderEntity.setItemEntities(list);
return orderEntity;
}).collect(Collectors.toList());
page.setRecords(collect);
return new PageUtils(page);
}

重新启动GulimallOrderApplication
服务,再次测试,这时GulimallMemberApplication
服务的控制台就输出了如下的json

格式化后的json
文件如下图所示

6、修改我的订单页
在 http://member.gulimall.com/memberOrder.html 页面里,打开控制台,定位到一个订单项,复制class="table"

在gulimall-member
模块的src/main/resources/templates/orderList.html
文件里搜索class="table"
,只保留一个<table>

然后修改这个<table>
,将内容修改为动态的数据,代码内容如下图所示

重启GulimallMemberApplication
服务,浏览器访问 http://member.gulimall.com/memberOrder.html 页面

2、内网穿透
1、原理
付款成功后,支付宝会多次向配置的异步通知接口
发请求,直到该接口返回success
才停止
查看文档: https://opendocs.alipay.com/open/270/105902

别人的电脑
可以访问公网的京东商城
,而别人的电脑
不能访问我的电脑
,这是因为我的电脑
没有公网ip,别人无法通过公网访问。

两个用户却可以发送QQ
消息,这是因为我们都能够访问QQ服务器
,别人的电脑
发送QQ
消息时会发送给QQ服务器
,QQ服务器
先把消息存着,当我的电脑连接QQ服务器
后,QQ服务器
就将存着的消息发给我,因此我们可以相互聊天。(相当于我们都可以访问QQ服务器
,QQ服务器
代理我们发送的消息,当我们连接QQ服务器
后,QQ服务器
将别的用户想要发送给我们的消息发送给我们)

内网穿透的原理类似,我们两个电脑不能访问,但是我们都可以访问内网穿透服务商
,内网穿透服务商做代理,将消息转发给我们。

2、添加异步通知地址
在gulimall-order
模块的com.atguigu.gulimall.order.config.AlipayTemplate
类里,修改notifyUrl
的值为内网穿透服务商提供的域名
+/payed/notify
private String notifyUrl="http://1661133527191.free.aeert.com/payed/notify";

在gulimall-order
模块的com.atguigu.gulimall.order.listener
包里新建OrderPayedListener
类
package com.atguigu.gulimall.order.listener;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @author 无名氏
* @date 2022/8/21
* @Description:
*/
@Controller
public class OrderPayedListener {
@GetMapping("/payed/notify")
@ResponseBody
public String handleAlipayed(HttpServletRequest request){
Map<String, String[]> map = request.getParameterMap();
System.out.println("收到了支付宝的通知:"+map);
return "success";
}
}

访问http://order.gulimall.com/payed/notify
页面,重定向到了登录页。这一看就是拦截器将这个请求拦截了,在拦截器里将这个请求直接放行即可。
http://order.gulimall.com/payed/notify
http://auth.gulimall.com/login.html

在gulimall-order
模块的com.atguigu.gulimall.order.interceptor.LoginUserInterceptor
类里修改preHandle
方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match2 = antPathMatcher.match("/payed/notify", uri);
if (match || match2){
return true;
}
....
}

再次访问 http://order.gulimall.com/payed/notify 页面,这次访问成功了,返回的内容为success
,此时的请求的URL为 http://order.gulimall.com/payed/notify ,请求头的Host为order.gulimall.com
http://order.gulimall.com/payed/notify
order.gulimall.com

在内网穿透服务商那里配置要穿透的内网服务为order.gulimall.com:80
,也就是我们本地访问的地址。通过此配置公网上访问 http://1661133527191.free.aeert.com/payed/notify 就能访问到我们本地的 http://order.gulimall.com:80
order.gulimall.com:80

3、请求头Host不匹配
直接访问内网穿透服务商提供的域名,可以正确访问nginx
里设置的访问 http://order.gulimall.com:80 网址的默认页面(文件为/mydata/nginx/html/index.html
)
http://1661133527191.free.aeert.com/

访问 http://1661133527191.free.aeert.com/payed/notify 页面,相当于访问 http://order.gulimall.com/payed/notify 页面,此时却显示404 Not Found
,这是因为访问 http://1661133527191.free.aeert.com/payed/notify 页面时,请求头的Host为1661133527191.free.aeert.com
http://1661133527191.free.aeert.com/payed/notify
1661133527191.free.aeert.com

而我们直接访问 http://order.gulimall.com/payed/notify 时,请求头的Host为order.gulimall.com
,我们在windows系统的C:\Windows\System32\drivers\etc\hosts
文件里配置了Host为order.gulimall.com
时应访问的ip为192.168.56.10
,而并没有配Host
为1661133527191.free.aeert.com
时应访问的ip
,如果nginx
不需要转发到192.168.56.10
里的服务直接自己处理是可以正常访问的;但是如果C:\Windows\System32\drivers\etc\hosts
文件里没有设置1661133527191.free.aeert.com
域名应访问的ip
,niginx
就不知道要转发到192.168.56.10
,niginx
就会找到DNS
里配置的1661133527191.free.aeert.com
域名对应的ip
,然后将消息转发给其对应的ip
,很显然1661133527191.free.aeert.com
域名对应ip
的机器是没有我们想要访问的tomcat
服务(其实如果没有配置代理1661133527191.free.aeert.com
域名下的请求时,不会转发给对应的服务,只会在nginx
里的文件里查找,在nginx
里配置代理1661133527191.free.aeert.com
域名下的请求后才会转发给对应的服务。)

访问失败的原因如下图所示,本质上就是niginx
转发请求头Host
为order.gulimall.com
的请求时,能够正常转发到192.168.56.10
,而请求头Host
为1661133527191.free.aeert.com
时,由于我们本地C:\Windows\System32\drivers\etc\hosts
文件没有配置1661133527191.free.aeert.com
域名应该访问的ip
,此时就只能从DNS
里寻找对应的ip
,因此就错误的将请求转发给提供1661133527191.free.aeert.com
域名的域名服务商的对应ip
了,因此访问失败了。(其实如果没有配置代理1661133527191.free.aeert.com
域名下的请求时,不会转发给对应的服务,只会在nginx
里的文件里查找,在nginx
里配置代理1661133527191.free.aeert.com
域名下的请求后才会转发给对应的服务。)
因此根据上面的描述,主要有两种解决办法,一种是修改本地的C:\Windows\System32\drivers\etc\hosts
文件,由于优先寻找本地的C:\Windows\System32\drivers\etc\hosts
因此能够正确找到我们想要其访问的ip
,即192.168.56.10
(但是还是要在nginx
里配置代理1661133527191.free.aeert.com
域名下的服务);一种是手动更正nginx
里转发请求时发送的请求头的Host
。第一种方式需要修改本地的C:\Windows\System32\drivers\etc\hosts
文件并在nginx
里配置代理1661133527191.free.aeert.com
域名下的请求;第二种方式需要在nginx
里配置手动修改/payed/
下的Host
为order.gulimall.com
并配置代理1661133527191.free.aeert.com
域名下的请求。

4、修改请求头的Host
在/mydata/nginx/conf/conf.d/gulimall.conf
文件里手动修改/payed/
下的Host
为order.gulimall.com
,然后重启noginx
服务
[root@localhost ~]# cd /mydata/nginx/conf/
[root@localhost conf]# ls
conf.d fastcgi_params koi-utf koi-win mime.types modules nginx.conf scgi_params uwsgi_params win-utf
[root@localhost conf]# cd conf.d/
[root@localhost conf.d]# ls
default.conf gulimall.conf
[root@localhost conf.d]# vi gulimall.conf
[root@localhost conf.d]# docker restart nginx
nginx

由于Host
不匹配,导致没有转给订单服务,因此可以在location / {
配置的前面添加如下代码,将/payed/
下的所有请求都手动设置Host
location /payed/ {
proxy_set_header Host order.gulimall.com;
proxy_pass http://gulimall;
}

重启noginx
服务后,再次访问 http://1661133527191.free.aeert.com/payed/notify 页面,此时niginx
还是没有正确转发给192.168.56.10

5、添加代理服务
查看niginx
的日志,可以看到host: "1661133527191.free.aeert.com"
访问了http://1661133527191.free.aeert.com/payed/notify
页面,而nginx
去静态资源/usr/share/nginx/html/payed/notify
里面去找了 ,我们想要让nginx
转发给192.168.56.10
对应的服务,结果却在niginx
自己的文件里去找,这是因为我们没有配置代理1661133527191.free.aeert.com
域名下的请求。
[root@localhost conf.d]# cd ../../
[root@localhost nginx]# ls
conf html logs
[root@localhost nginx]# cd logs/
[root@localhost logs]# ls
access.log error.log
[root@localhost logs]# cat access.log |grep 'payed'
192.168.56.1 - - [22/Aug/2022:02:56:59 +0000] "GET /payed/notify HTTP/1.1" 200 7 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "-"
192.168.56.1 - - [22/Aug/2022:02:56:59 +0000] "GET /favicon.ico HTTP/1.1" 200 946 "http://order.gulimall.com/payed/notify" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "-"
192.168.56.1 - - [22/Aug/2022:02:57:01 +0000] "GET /payed/notify HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "202.103.46.13"
192.168.56.1 - - [22/Aug/2022:02:57:18 +0000] "GET /payed/notify HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "202.103.46.13"
[root@localhost logs]# cat error.log |grep 'payed'
2022/08/22 02:57:01 [error] 7#7: *6 open() "/usr/share/nginx/html/payed/notify" failed (2: No such file or directory), client: 192.168.56.1, server: localhost, request: "GET /payed/notify HTTP/1.1", host: "1661133527191.free.aeert.com"
2022/08/22 02:57:18 [error] 7#7: *6 open() "/usr/share/nginx/html/payed/notify" failed (2: No such file or directory), client: 192.168.56.1, server: localhost, request: "GET /payed/notify HTTP/1.1", host: "1661133527191.free.aeert.com"

在/mydata/nginx/conf/conf.d/gulimall.conf
文件里的server_name
后面再加一个1661133527191.free.aeert.com
server_name gulimall.com *.gulimall.com 1661133527191.free.aeert.com;

再次重启nginx
服务
[root@localhost logs]# cd ../conf/conf.d/
[root@localhost conf.d]# ls
default.conf gulimall.conf
[root@localhost conf.d]# vi gulimall.conf
[root@localhost conf.d]# docker restart nginx
nginx

再次访问 http://1661133527191.free.aeert.com/payed/notify 页面,可以看到这次nginx
成功转发给192.168.56.10
了

查看nginx
日志,可以看到访问日志已经显示状态为200
了,错误日志也没有打印错误消息了
[root@localhost conf.d]# cd ../../logs/
[root@localhost logs]# cat access.log |grep 'payed'
192.168.56.1 - - [22/Aug/2022:02:56:59 +0000] "GET /payed/notify HTTP/1.1" 200 7 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "-"
192.168.56.1 - - [22/Aug/2022:02:56:59 +0000] "GET /favicon.ico HTTP/1.1" 200 946 "http://order.gulimall.com/payed/notify" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "-"
192.168.56.1 - - [22/Aug/2022:02:57:01 +0000] "GET /payed/notify HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "202.103.46.13"
192.168.56.1 - - [22/Aug/2022:02:57:18 +0000] "GET /payed/notify HTTP/1.1" 404 571 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "202.103.46.13"
192.168.56.1 - - [22/Aug/2022:03:09:18 +0000] "GET /payed/notify HTTP/1.1" 200 7 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36" "202.103.46.13"
[root@localhost logs]# cat error.log |grep 'payed'
2022/08/22 02:57:01 [error] 7#7: *6 open() "/usr/share/nginx/html/payed/notify" failed (2: No such file or directory), client: 192.168.56.1, server: localhost, request: "GET /payed/notify HTTP/1.1", host: "1661133527191.free.aeert.com"
2022/08/22 02:57:18 [error] 7#7: *6 open() "/usr/share/nginx/html/payed/notify" failed (2: No such file or directory), client: 192.168.56.1, server: localhost, request: "GET /payed/notify HTTP/1.1", host: "1661133527191.free.aeert.com"

3、处理支付结果
1、修改支付宝异步通知代码
修改gulimall-order
模块的com.atguigu.gulimall.order.listener.OrderPayedListener
类的handleAlipayed
方法
/**
* 支付宝成功异步回调
* @param request
* @return
*/
@PostMapping("/payed/notify")
public String handleAlipayed(HttpServletRequest request){
//只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success, 支付宝就再也不通知
Map<String, String[]> map = request.getParameterMap();
for (String key : map.keySet()) {
String value = request.getParameter(key);
System.out.println("key==>" + key +" value=>"+value);
}
System.out.println(JSON.toJSONString(map));
return "success";
}

重新支付一个商品,控制台输出如下内容

格式后的json如下图所示

2、处理支付结果
复制2.分布式高级篇(微服务架构篇)\资料源码\代码\支付
里的PayAsyncVo.java
文件,粘贴到gulimall-order
模块的com.atguigu.gulimall.order.vo
包下

gulimall-order
模块的com.atguigu.gulimall.order.vo.PayAsyncVo
类完整代码如下图所示

在gulimall-order
模块的com.atguigu.gulimall.order.listener.OrderPayedListener
类里修改handleAlipayed
方法
@Autowired
OrderService orderService;
/**
* 支付宝成功异步回调
* @param vo
* @return
*/
@PostMapping("/payed/notify")
@ResponseBody
public String handleAlipayed(PayAsyncVo vo){
//只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success, 支付宝就再也不通知
try {
boolean result = orderService.handlePayResult(vo);
if (result) {
return "success";
}
}catch (Exception e){
e.printStackTrace();
}
return "false";
}

在gulimall-order
模块的com.atguigu.gulimall.order.service.OrderService
接口里添加handlePayResult
方法
boolean handlePayResult(PayAsyncVo vo);

首先需要把订单保存到支付宝交易流水里(是gulimall_oms
数据库的oms_payment_info
表),方便对账

在gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类里添加handlePayResult
方法
@Override
public boolean handlePayResult(PayAsyncVo vo) {
//保存交易流水
PaymentInfoEntity infoEntity = new PaymentInfoEntity();
//设置支付宝流水号
infoEntity.setAlipayTradeNo(vo.getTradeNo());
//设置订单号
infoEntity.setOrderSn(vo.getOutTradeNo());
//设置交易状态
infoEntity.setPaymentStatus(vo.getTradeStatus());
//设置回调时间
infoEntity.setCallbackTime(vo.getNotifyTime());
paymentInfoService.save(infoEntity);
String status = vo.getTradeStatus();
if ("TRADE_SUCCESS".equals(status) || "TRADE_FINISHED".equals(status)) {
//支付成功
String orderSn = vo.getOutTradeNo();
this.baseMapper.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode());
}
return true;
}

在gulimall-order
模块的com.atguigu.gulimall.order.vo.PayAsyncVo
类里将notifyTime
字段的类型修改为Date

在gulimall_oms
数据库的oms_payment_info
表里,给order_sn
和alipay_trade_no
添加唯一索引

在gulimall_oms
数据库的oms_payment_info
表里,把order_sn
字段的长度从32
修改为64

3、更新等单状态
在gulimall-order
模块的com.atguigu.gulimall.order.dao.OrderDao
接口里添加updateOrderStatus
抽象方法,注意把Integer code
改为Integer status
后,再生成@Param("status")
void updateOrderStatus(@Param("orderSn") String orderSn, @Param("status") Integer status);

在gulimall-order
模块的src/main/resources/mapper/order/OrderDao.xml
文件里添加如下sql
<update id="updateOrderStatus">
update gulimall_oms.oms_order set `status`=#{status} where order_sn =#{orderSn}
</update>

在gulimall-order
模块的com.atguigu.gulimall.order.listener.OrderPayedListener
类里添加验证签名的checkSignVerified
方法,并让handleAlipayed
方法在执行orderService.handlePayResult(vo)
之前调用该方法来验证签名
@Autowired
OrderService orderService;
@Autowired
AlipayTemplate alipayTemplate;
/**
* 支付宝成功异步回调
*
* @param vo
* @return
*/
@PostMapping("/payed/notify")
@ResponseBody
public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) {
//只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success, 支付宝就再也不通知
try {
boolean signVerified = checkSignVerified(request);
//验证签名
if (signVerified) {
boolean result = orderService.handlePayResult(vo);
if (result) {
return "success";
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "error";
}
private boolean checkSignVerified(HttpServletRequest request) throws AlipayApiException{
//获取支付宝POST过来反馈信息
Map<String, String[]> requestParams = request.getParameterMap();
Map<String,String> params = new HashMap<>();
requestParams.forEach((k,v)->{
String value = StringUtils.collectionToDelimitedString(Arrays.asList(v), ",");
//乱码解决,这段代码在出现乱码时使用
//value = new String(value.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
params.put(k,value);
});
//调用SDK验证签名
return AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipayPublicKey(), alipayTemplate.getCharset(), alipayTemplate.getSignType());
}

4、数据无法成功封装
重新支付商品后,在GulimallOrderApplication
服务的控制台报了下图所示的错误

在gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类的handlePayResult
方法的第一行打上断点,可以看到很多都没封装进去,但有些封装成功了(封装成功的都是一个单词的)

返回的信息的值为数组,如果使用蛇形命名法可以正确处理,但是使用驼峰命名法不能正确处理
{
"gmt_create": ["2022-08-22 11:36:24"],
"charset": ["utf-8"],
"gmt_payment": ["2022-08-22 11:36:39"],
"notify_time": ["2022-08-22 11:36:41"],
"subject": ["华为 HUAWEI Mate30Pro 罗兰紫 8GB+128GB 麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄 4G全网通手机"],
"sign": ["bHkpXeAUM+egCByULY9b0rHefVrJr/ivJjH5vMVxr+JnJn795JdoaPn7vi8iMBxe674eh0x/dHYzc8WHoZrHGbAKrkkqhxgvbYw3VmMGVATjy2VvdqRjCXksKhCikU+uK6/aPn7xkodvcBokbgpBc4yz+vqRuPw/Paxp6WvofyMRgl3Yi6Zx4HG96yXQP1BgfKyTEHwq8QCUTsnE4UlRgOggTubBEG3Z4oXJIYSbM1pEWT6V065wOYIE4pDXCXo3kLEQdnLmlUK/i4u1CaEhuG6ScUBlRJ8hm6T8TgDHMXMiYUkTHGZg3pC9IH71ZZbSxBoYEVWJBHOs5CEcFKgCCA=="],
"buyer_id": ["2088622956116255"],
"body": ["颜色:罗兰紫;版本:8GB+128GB"],
"invoice_amount": ["11807.00"],
"version": ["1.0"],
"notify_id": ["2022082200222113640016250520715284"],
"fund_bill_list": ["[{\"amount\":\"11807.00\",\"fundChannel\":\"ALIPAYACCOUNT\"}]"],
"notify_type": ["trade_status_sync"],
"out_trade_no": ["202208221136062801561557798993534977"],
"total_amount": ["11807.00"],
"trade_status": ["TRADE_SUCCESS"],
"trade_no": ["2022082222001416250501868104"],
"auth_app_id": ["2021000117672941"],
"receipt_amount": ["11807.00"],
"point_amount": ["0.00"],
"app_id": ["2021000117672941"],
"buyer_pay_amount": ["11807.00"],
"sign_type": ["RSA2"],
"seller_id": ["2088621955944878"]
}
在gulimall-order
模块的com.atguigu.gulimall.order.vo
包里新建一个PayAsyncVo2
类,修改gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类的handlePayResult
方法的参数为PayAsyncVo2
类型,然后修改对应的controller
、service
接口等,都使用PayAsyncVo2
类来封装信息

再次调试发现即使全指定别名也不行,多个单词的字段还是封装不进去

由于返回的字段的值都是数组,因此修改gulimall-order
模块的com.atguigu.gulimall.order.vo.PayAsyncVo2
类,全部字段都改用List
来接收,然后调试发现使用List
来接收也不行

5、最终封装方式
最后还是妥协了,还是使用老师的方式接收
修改gulimall-order
模块的com.atguigu.gulimall.order.listener.OrderPayedListener
类
@Autowired
OrderService orderService;
@Autowired
AlipayTemplate alipayTemplate;
/**
* 支付宝成功异步回调
*
* @param vo
* @return
*/
@PostMapping("/payed/notify")
@ResponseBody
public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) {
//只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success, 支付宝就再也不通知
try {
boolean signVerified = checkSignVerified(request);
//验证签名
if (signVerified) {
boolean result = orderService.handlePayResult(vo);
if (result) {
return "success";
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "error";
}
private boolean checkSignVerified(HttpServletRequest request) throws AlipayApiException{
//获取支付宝POST过来反馈信息
Map<String, String[]> requestParams = request.getParameterMap();
Map<String,String> params = new HashMap<>();
requestParams.forEach((k,v)->{
String value = StringUtils.collectionToDelimitedString(Arrays.asList(v), ",");
//乱码解决,这段代码在出现乱码时使用
//value = new String(value.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
params.put(k,value);
});
//调用SDK验证签名
return AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipayPublicKey(), alipayTemplate.getCharset(), alipayTemplate.getSignType());
}

还是使用gulimall-order
模块的com.atguigu.gulimall.order.vo.PayAsyncVo
类来封装数据,修改该类的notify_time
字段的类型为Date

修改gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类的handlePayResult
方法
@Override
public boolean handlePayResult(PayAsyncVo vo) {
//保存交易流水
PaymentInfoEntity infoEntity = new PaymentInfoEntity();
//设置支付宝流水号
infoEntity.setAlipayTradeNo(vo.getTrade_no());
//设置订单号
infoEntity.setOrderSn(vo.getOut_trade_no());
//设置交易状态
infoEntity.setPaymentStatus(vo.getTrade_status());
//设置回调时间
infoEntity.setCallbackTime(vo.getNotify_time());
paymentInfoService.save(infoEntity);
String status = vo.getTrade_status();
if ("TRADE_SUCCESS".equals(status) || "TRADE_FINISHED".equals(status)) {
//支付成功
String orderSn = vo.getOut_trade_no();
this.baseMapper.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode());
}
return true;
}

notify_time
字段无法封装
6、测试后,报了payAsyncVo
类的notify_time
字段无法成功封装
Field error in object 'payAsyncVo' on field 'notify_time': rejected value [2022-08-22 17:19:31]; codes [typeMismatch.payAsyncVo.notify_time,typeMismatch.notify_time,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [payAsyncVo.notify_time,notify_time]; arguments []; default message [notify_time]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'notify_time'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2022-08-22 17:19:31'; nested exception is java.lang.IllegalArgumentException]]

在gulimall-order
模块的src/main/resources/application.properties
配置文件里添加如下配置,全局指定使用的日期格式。
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss

或在gulimall-order
模块的com.atguigu.gulimall.order.vo.PayAsyncVo
类的notify_time
字段上添加如下注解,指定该字段使用的日期格式。
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date notify_time;

再次测试,可以看到数据已经全部封装成功了

4、支付时设置收单
收单(超过允许的时间后不允许用户支付)场景:
订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。
- 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
由于时延等问题,订单解锁完成,正在解锁库存的时候,异步通知才到
- 订单解锁,手动调用收单
网络阻塞问题,订单支付成功的异步通知一直不到达
- 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态
其他各种问题
- 每天晚上闲时下载支付宝对账单,一 一进行对账
文档地址: https://opendocs.alipay.com/open/028r8t?scene=22

在gulimall-order
模块的com.atguigu.gulimall.order.config.AlipayTemplate
文件里添加private String timeout = "1m";
字段,并在alipayRequest.setBizContent
方法里添加+ "\"timeout_express\":\"" + timeout + "\","

重启GulimallOrderApplication
服务后,再次支付商品,此时就会显示正在使用即时到账交易[?] 交易将在46秒后关闭,请及时付款!

超过时间再支付,此时就会显示抱歉,您的交易因超时已失败。
抱歉,您的交易因超时已失败。
您订单的最晚付款时间为: 2022-08-23 10:47:16,目前已过期,交易关闭。
'
5、手动调用收单
再gulimall-order
模块的com.atguigu.gulimall.order.listener.OrderCloseListener
类的listener
方法里,关闭订单后,手动调用收单接口,防止用户在订单关闭后才支付。(其实我认为应该先手动调用收单接口
再关闭订单
,因为如果关闭订单
后,用户此时支付了,而我们手动调用收单接口
的请求还没有发给支付宝,此时就会出现订单关闭了,但是用户成功支付的情况,而先先手动调用收单接口
再关闭订单
就不会出现这种情况了)

手动调用收单接口
如下图所示,叫统一收单交易关闭接口

相关代码如下图所示

七、秒杀
7.1、秒杀
秒杀( 高并发) 系统关注的问题


7.1.1、新增秒杀场次
1、添加秒杀场次
1、秒杀业务:秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化) + 独立部署。
限流方式:
- 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
nginx
限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法- 网关限流,限流的过滤器
- 代码中使用分布式信号量
- rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。
1、配置每日秒杀请求
启动后台管理系统,访问 http://localhost:8001/#/coupon-seckillsession 页面,在优惠营销
-> 每日秒杀
里,打开控制台,然后点击查询
按钮,即可看到请求的接口为: http://localhost:88/api/coupon/seckillsession/list?t=1661218666831&page=1&limit=10&key=
http:/api/coupon/seckillsession/list?t=1661218666831&page=1&limit=10&key=

在gulimall-gateway
模块的src/main/resources/application.yml
文件里,添加如下配置,将/api/coupon/**
开头的请求全部负载均衡到gulimall-coupon
模块
spring:
cloud:
gateway:
routes:
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
# (?<segment>/?.*) 和 $\{segment} 为固定写法
#http://localhost:88/api/coupon/seckillsession/list 变为 http://localhost:7000/coupon/seckillsession/list
- RewritePath=/api/(?<segment>/?.*),/$\{segment}

重启GulimallGatewayApplication
服务,在 http://localhost:8001/#/coupon-seckillsession 页面里,打开控制台,刷新页面,可以看到这次请求的状态码为200

2、添加秒杀场次
打开 http://localhost:8001/#/coupon-seckillsession 页面,在优惠营销
-> 每日秒杀
里,点击新增,添加一个秒杀场次。

在优惠营销
-> 每日秒杀
页面里,点击新增,再添加一个秒杀场次。

这里的时区好像不太对,开始时间和结束时间都早了8
小时,而且格式不符合国人审美。

查看gulimall_sms
数据库的sms_seckill_session
表,可以看到这里的时区有问题。

3、关联商品
在优惠营销
-> 每日秒杀
页面里,点击第一个秒杀场次的操作的关联商品
,此时会发一个http://localhost:88/api/coupon/seckillskurelation/list?t=1661219427384&page=1&limit=10&key=&promotionSessionId=1
请求, 这个promotionSessionId
就是这个秒杀的id

在gulimall_sms
数据库的sms_seckill_sku_relation
表里,给1
和2
号promotionSessionId
都新增一下数据

在优惠营销
-> 每日秒杀
页面里,点击操作的关联商品
,这时就能显示这个秒杀id的所有关联的信息了,请求的接口如下
http://localhost:88/api/coupon/seckillskurelation/list?t=1661220555869&page=1&limit=10&key=&promotionSessionId=1

然后点击参数名
的输入框,输入1
,按回车,此时又发了如下请求,并查询到了相关的关联商品信息
http://localhost:88/api/coupon/seckillskurelation/list?t=1661220676407&page=1&limit=10&key=1&promotionSessionId=1

查看GulimallCouponApplication
服务的控制台,可以看到输出了如下的sql
SELECT id,seckill_sort,promotion_session_id,seckill_count,seckill_price,seckill_limit,sku_id,promotion_id FROM sms_seckill_sku_relation WHERE (( (id = ? OR promotion_id = ? OR sku_id = ?) ) AND promotion_session_id = ?)

在优惠营销
-> 每日秒杀
页面里,点击第2个秒杀场次的操作的关联商品
,此时会发如下请求
http://localhost:88/api/coupon/seckillskurelation/list?t=1661220577315&page=1&limit=10&key=&promotionSessionId=2

4、添加关联商品
在优惠营销
-> 每日秒杀
页面里,点击第1个秒杀场次的操作的关联商品
,在关联秒杀商品
里点击新增
,新增如下关联商品

然后自动回到在优惠营销
-> 每日秒杀
-> 第1个秒杀场次的操作的关联秒杀商品
页面,此时刚刚新增的秒杀商品已经关联进去了

7.1.2、秒杀模块
1、新建秒杀模块
1、新建模块
选中IDEA
里Project
的gulimall
,右键依次点击New
->Module
->Spring Initializr
->Next
在New Module
对话框里Group
里输入com.atguigu.gulimall
,Artifact
里输入gulimall-seckill
,Java Version
选择8
,Description
里输入秒杀
,Package
里输入com.atguigu.gulimall.seckill
,然后点击Next
com.atguigu.gulimall
gulimall-seckill
秒杀
com.atguigu.gulimall.seckill

选择Devloper Tools
里的Spring Boot DevTools
和Lombox
,选择Web
里的Spring Web
,选择NoSQL
里的Spring Data Redis (Access+ Driver)
,选择Spring Cloud Routing
里的OpenFeign
,然后点击Next

最后点击Finish

2、修改依赖
复制gulimall-seckill
模块的pom.xml
文件的dependencies
和项目信息
的部分,(properties
里的不要)
然后复制gulimall-product
模块的pom.xml
文件,粘贴到gulimall-seckill
模块的pom.xml
文件里,删除dependencies
和项目信息
的部分,替换为刚刚复制的gulimall-seckill
模块的pom.xml
文件的dependencies
和项目信息
如果pom.xml
文件颜色为赤橙色,可以选中pom.xml
文件,右键选择Add as Maven Project
就好了(最好先替换文件,再加入到项目)

在gulimall-seckill
模块的pom.xml
文件里,添加gulimall-common
依赖
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

3、修改测试类
修改gulimall-seckill
模块的com.atguigu.gulimall.seckill.GulimallSeckillApplicationTests
测试类为junit4
package com.atguigu.gulimall.seckill;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSeckillApplicationTests {
@Test
public void contextLoads() {
System.out.println("hello");
}
}

4、修改配置
修改gulimall-seckill
模块的src/main/resources/application.properties
配置文件
spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.56.10

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication
启动类里添加@EnableDiscoveryClient
注解,修改@SpringBootApplication
,让其在启动的时候排除DataSourceAutoConfiguration
然后启动GulimallSeckillApplication
服务,然后限制内存-Xmx100m
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

2、cron表达式
cron表达式: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

CronTrigger Tutorial
CronTrigger TutorialIntroductionFormatSpecial charactersExamplesNotes
Introduction
cron
is a UNIX tool that has been around for a long time, so its scheduling capabilities are powerful and proven. The CronTrigger
class is based on the scheduling capabilities of cron.
CronTrigger
uses “cron expressions”, which are able to create firing schedules such as: “At 8:00am every Monday through Friday” or “At 1:30am every last Friday of the month”.
Cron expressions are powerful, but can be pretty confusing. This tutorial aims to take some of the mystery out of creating a cron expression, giving users a resource which they can visit before having to ask in a forum or mailing list.
Format
A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:
Field Name | Mandatory | Allowed Values | Allowed Special Characters |
---|---|---|---|
Seconds | YES | 0-59 | , - * / |
Minutes | YES | 0-59 | , - * / |
Hours | YES | 0-23 | , - * / |
Day of month | YES | 1-31 | , - * ? / L W |
Month | YES | 1-12 or JAN-DEC | , - * / |
Day of week | YES | 1-7 or SUN-SAT | , - * ? / L # |
Year | NO | empty, 1970-2099 | , - * / |
So cron expressions can be as simple as this: * * * * ? *
or more complex, like this: 0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010
Special characters
*****
(“all values”) - used to select all values within a field. For example, “*****” in the minute field means “every minute”.**?**
(“no specific value”) - useful when you need to specify something in one of the two fields in which the character is allowed, but not the other. For example, if I want my trigger to fire on a particular day of the month (say, the 10th), but don’t care what day of the week that happens to be, I would put “10” in the day-of-month field, and “?” in the day-of-week field. See the examples below for clarification.**-**
- used to specify ranges. For example, “10-12” in the hour field means “the hours 10, 11 and 12”.**,**
- used to specify additional values. For example, “MON,WED,FRI” in the day-of-week field means “the days Monday, Wednesday, and Friday”.**/**
- used to specify increments. For example, “0/15” in the seconds field means “the seconds 0, 15, 30, and 45”. And “5/15” in the seconds field means “the seconds 5, 20, 35, and 50”. You can also specify ‘/’ after the ‘’ character - in this case ‘’ is equivalent to having ‘0’ before the ‘/’. ‘1/3’ in the day-of-month field means “fire every 3 days starting on the first day of the month”.**L**
(“last”) - has different meaning in each of the two fields in which it is allowed. For example, the value “L” in the day-of-month field means “the last day of the month” - day 31 for January, day 28 for February on non-leap years. If used in the day-of-week field by itself, it simply means “7” or “SAT”. But if used in the day-of-week field after another value, it means “the last xxx day of the month” - for example “6L” means “the last friday of the month”. You can also specify an offset from the last day of the month, such as “L-3” which would mean the third-to-last day of the calendar month. When using the ‘L’ option, it is important not to specify lists, or ranges of values, as you’ll get confusing/unexpected results.**W**
(“weekday”) - used to specify the weekday (Monday-Friday) nearest the given day. As an example, if you were to specify “15W” as the value for the day-of-month field, the meaning is: “the nearest weekday to the 15th of the month”. So if the 15th is a Saturday, the trigger will fire on Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you specify “1W” as the value for day-of-month, and the 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not ‘jump’ over the boundary of a month’s days. The ‘W’ character can only be specified when the day-of-month is a single day, not a range or list of days.
The 'L' and 'W' characters can also be combined in the day-of-month field to yield 'LW', which translates to "last weekday of the month".
**#**
- used to specify “the nth” XXX day of the month. For example, the value of “6#3” in the day-of-week field means “the third Friday of the month” (day 6 = Friday and “#3” = the 3rd one in the month). Other examples: “2#1” = the first Monday of the month and “4#5” = the fifth Wednesday of the month. Note that if you specify “#5” and there is not 5 of the given day-of-week in the month, then no firing will occur that month.
The legal characters and the names of months and days of the week are not case sensitive.
MON
is the same asmon
.
Examples
Here are some full examples:
Expression | Meaning |
---|---|
0 0 12 * * ? | Fire at 12pm (noon) every day |
0 15 10 ? * * | Fire at 10:15am every day |
0 15 10 * * ? | Fire at 10:15am every day |
0 15 10 * * ? * | Fire at 10:15am every day |
0 15 10 * * ? 2005 | Fire at 10:15am every day during the year 2005 |
0 * 14 * * ? | Fire every minute starting at 2pm and ending at 2:59pm, every day |
0 0/5 14 * * ? | Fire every 5 minutes starting at 2pm and ending at 2:55pm, every day |
0 0/5 14,18 * * ? | Fire every 5 minutes starting at 2pm and ending at 2:55pm, AND fire every 5 minutes starting at 6pm and ending at 6:55pm, every day |
0 0-5 14 * * ? | Fire every minute starting at 2pm and ending at 2:05pm, every day |
0 10,44 14 ? 3 WED | Fire at 2:10pm and at 2:44pm every Wednesday in the month of March. |
0 15 10 ? * MON-FRI | Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday |
0 15 10 15 * ? | Fire at 10:15am on the 15th day of every month |
0 15 10 L * ? | Fire at 10:15am on the last day of every month |
0 15 10 L-2 * ? | Fire at 10:15am on the 2nd-to-last last day of every month |
0 15 10 ? * 6L | Fire at 10:15am on the last Friday of every month |
0 15 10 ? * 6L | Fire at 10:15am on the last Friday of every month |
0 15 10 ? * 6L 2002-2005 | Fire at 10:15am on every last friday of every month during the years 2002, 2003, 2004 and 2005 |
0 15 10 ? * 6#3 | Fire at 10:15am on the third Friday of every month |
0 0 12 1/5 * ? | Fire at 12pm (noon) every 5 days every month, starting on the first day of the month. |
0 11 11 11 11 ? | Fire every November 11th at 11:11am. |
Pay attention to the effects of '?' and '*' in the day-of-week and day-of-month fields!
解释
特殊字符: ,
:枚举; (cron="7,9,23 * * * * ?"):任意时刻的 7,9,23 秒启动这个任务; -
:范围: (cron="7-20 * * * * ?"):任意时刻的 7-20 秒之间,每秒启动一次 *
:任意; 指定位置的任意时刻都可以 /
:步长; (cron="7/5 * * * * ?"):第 7 秒启动,每 5 秒一次; (cron="/5 * * * * ?"):任意秒启动,每 5 秒一次; ?
:(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使 用? (cron=" * * 1 * ?"):每月的 1 号,启动这个任务; L
:(出现在日和周的位置)”, last:最后一个 (cron="* * * ? * 3L"):每月的最后一个周二(1L为周日) W
: Work Day:工作日 (cron="* * * W * ?"):每个月的工作日触发 (cron="* * * LW * ?"):每个月的最后一个工作日触发 #
:第几个 (cron="* * * ? * 5#2"):每个月的第 2 个周
3、整合cron表达式
1、简单测试
在gulimall-seckill
模块的com.atguigu.gulimall.seckill
包下,新建scheduled
文件夹,在scheduled
文件夹里新建HelloSchedule
类,用于测试定时任务
package com.atguigu.gulimall.seckill.scheduled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
* @EnableScheduling 开启定时任务
* @Scheduled 开启一个定时任务
*/
@Slf4j
@Component
@EnableScheduling
public class HelloSchedule {
/**
* 在Spring中的不同
* 1、cron由6位组成,不允许第7位的年
* 2、在周几的位置,1-7代表周一到周日; MON- SUN
*/
@Scheduled(cron = "*/5 * * ? * 2")
public void hello(){
log.info("hello...");
}
}

运行GulimallSeckillApplication
服务,可以看到这个定时任务每5
秒执行一次
2022-08-23 11:38:00.002 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:05.002 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:10.001 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:15.000 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:20.000 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:25.001 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:30.002 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:35.001 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:40.001 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:45.001 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:50.001 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:55.001 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 11:38:00.004 INFO 18428 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...

2、测试业务执行时间长
修改gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.HelloSchedule
类的hello
方法和该方法上的@Scheduled
注解的cron
表达式参数,设置其每一秒执行一次,但是业务执行时间为3s
可以看到定时任务设置每秒执行一次,但该业务需要执行3秒,结果却是近乎每4s执行一次,可见此默认该定时任务是阻塞的
/**
* 在Spring中的不同
* 1、cron由6位组成,不允许第7位的年
* 2、在周几的位置,1-7代表周一到周日; MON- SUN
* 3、定时任务不应该阻塞。默认是阻塞的
*/
@Scheduled(cron = "* * * ? * 2")
public void hello() throws InterruptedException {
log.info("hello...");
TimeUnit.SECONDS.sleep(3);
}
重启GulimallSeckillApplication
服务,控制台的输出如下
2022-08-23 15:52:54.001 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:52:58.000 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:02.001 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:06.002 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:10.001 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:14.001 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:18.001 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:22.001 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:26.002 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:30.001 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:34.000 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 15:53:38.002 INFO 8284 --- [ scheduling-1] c.a.g.seckill.scheduled.HelloSchedule : hello...

4、解决定时任务阻塞
1、手动创建线程池
方法一:手动创建线程池,修改gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.HelloSchedule
类的hello
方法如下所示,使用我们自己配置的线程池(这种方法可行,这里我就不测试了,只需将别的线程池的配置粘过来即可使用)
@Scheduled(cron = "* * * ? * 2")
public void hello(){
CompletableFuture.runAsync(()->{
log.info("hello...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, executor);
}

2、指定定时任务线程池大小(不生效)
方法二:指定定时任务线程池大小(不生效)
定时任务有自己的线程池,不过默认大小为1
,所以不能异步执行。(在org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
自动配置类里引入了TaskSchedulingProperties
配置类)

查看该org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
配置类,定时任务线程池默认设置的大小为1

在gulimall-seckill
模块的src/main/resources/application.properties
配置文件里添加如下配置,使用此配置不会生效
spring.task.scheduling.pool.size=5

重启GulimallSeckillApplication
服务,控制台输出如下信息,可以看到还是间隔4s
,可见配置了线程池大小定时任务仍然会阻塞
2022-08-23 16:21:03.000 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:07.002 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:11.002 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:15.002 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:19.001 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:23.002 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:27.001 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:31.002 INFO 22620 --- [ scheduling-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:35.002 INFO 22620 --- [ scheduling-5] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:39.002 INFO 22620 --- [ scheduling-5] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:43.000 INFO 22620 --- [ scheduling-5] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:47.001 INFO 22620 --- [ scheduling-5] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:51.001 INFO 22620 --- [ scheduling-5] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:21:55.000 INFO 22620 --- [ scheduling-5] c.a.g.seckill.scheduled.HelloSchedule : hello...

3、使用Spring自带的异步任务
方法三:Spring的异步任务
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication
启动类上添加@EnableAsync
注解,开启异步任务
@EnableAsync

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.HelloSchedule
类的hello
方法上添加@Async
注解
@Async

重启GulimallSeckillApplication
服务,控制台输出如下信息,可以看到间隔变为1s
,可见使用任务后就不会阻塞了
2022-08-23 16:14:01.003 INFO 16996 --- [ task-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:02.002 INFO 16996 --- [ task-2] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:03.001 INFO 16996 --- [ task-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:04.001 INFO 16996 --- [ task-4] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:05.001 INFO 16996 --- [ task-5] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:06.000 INFO 16996 --- [ task-6] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:07.001 INFO 16996 --- [ task-7] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:08.001 INFO 16996 --- [ task-8] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:09.001 INFO 16996 --- [ task-1] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:10.001 INFO 16996 --- [ task-2] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:11.001 INFO 16996 --- [ task-3] c.a.g.seckill.scheduled.HelloSchedule : hello...
2022-08-23 16:14:12.001 INFO 16996 --- [ task-4] c.a.g.seckill.scheduled.HelloSchedule : hello...

5、源码
1、自动配置
查看TaskExecutionAutoConfiguration
类的taskExecutorBuilder
方法可以发现,该方法返回的是TaskExecutorBuilder
任务执行建造者,在下方的applicationTaskExecutor
方法里获取这个TaskExecutorBuilder
任务执行建造者,然后调用 builder.build()
方法,返回ThreadPoolTaskExecutor
,因此容器中放的是ThreadPoolTaskExecutor
线程池
(定时任务的自动配置为TaskSchedulingAutoConfiguration
任务调度自动配置,异步线程池的自动配置为TaskExecutionAutoConfiguration
任务执行自动配置)
@Bean
@ConditionalOnMissingBean
public TaskExecutorBuilder taskExecutorBuilder() {
TaskExecutionProperties.Pool pool = this.properties.getPool();
TaskExecutorBuilder builder = new TaskExecutorBuilder();
builder = builder.queueCapacity(pool.getQueueCapacity());
builder = builder.corePoolSize(pool.getCoreSize());
builder = builder.maxPoolSize(pool.getMaxSize());
builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
builder = builder.keepAlive(pool.getKeepAlive());
builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix());
builder = builder.customizers(this.taskExecutorCustomizers);
builder = builder.taskDecorator(this.taskDecorator.getIfUnique());
return builder;
}
@Lazy
@Bean(name = { APPLICATION_TASK_EXECUTOR_BEAN_NAME,
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
@ConditionalOnMissingBean(Executor.class)
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build();
}

ThreadPoolTaskExecutor
继承关系
2、ThreadPoolTaskExecutor
类的继承关系如下图所示

点击ThreadPoolTaskExecutor
类可以看到其实现了AsyncListenableTaskExecutor
public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor
public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor
public interface AsyncTaskExecutor extends TaskExecutor
public interface TaskExecutor extends Executor
public interface java.util.concurrent.Executor

点击AsyncListenableTaskExecutor
接口,可以看到其继承了AsyncTaskExecutor
接口

点击AsyncTaskExecutor
接口,可以看到其继承了TaskExecutor
接口

点击TaskExecutor
接口,可以看到其继承了Executor
接口

点击Executor
接口,可以看到这个接口是jdk
自带的java.util.concurrent.Executor
类

3、修改配置
默认核心线程大小是8
,但是最大线程数和队列长度都是Integer.MAX_VALUE
,这样的话到时候并发上来了肯定撑不住这么多线程的。

在gulimall-seckill
模块的src/main/resources/application.properties
配置文件里,修改核心线程数
和最大线程数
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50

删除gulimall-seckill
模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication
类上的@EnableAsync
注解

删除gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.HelloSchedule
类上的@EnableScheduling
注解

6、执行定时任务
1、将最近3天需要秒杀的商品添加到redis
在gulimall-seckill
模块的com.atguigu.gulimall.seckill
包里新建config
文件夹,在config
文件夹里新建ScheduledConfig
类
package com.atguigu.gulimall.seckill.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
*/
@Configuration
@EnableScheduling
@EnableAsync
public class ScheduledConfig {
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled
包里新建SeckillSkuScheduled
类
package com.atguigu.gulimall.seckill.scheduled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
* 秒杀商品的定时上架;
* 每天晚上3点;上架最近三天需要秒杀的商品。
* 当天00:00:00 - 23:59:59
* 明天00:00:00 - 23:59:59
* 后天00:00:00 - 23:59:59
*/
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
/**
* 每天晚上3点,上架最近3天需要秒杀的商品
*/
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days(){
//1、重复上架无需处理
seckillService.uploadSeckillSkuLatest3Days();
}
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill
包里新建service
文件夹,在service
文件夹里新建SeckillService
接口,在该接口里添加uploadSeckillSkuLatest3Days
抽象方法用户获取最近3
天需要秒杀的商品
package com.atguigu.gulimall.seckill.service;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
*/
public interface SeckillService {
/**
* 上架最近3天需要秒杀的商品
*/
public void uploadSeckillSkuLatest3Days();
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service
包里新建impl
文件夹,在impl
文件夹里新建SeckillServiceImpl
类,在该类里实现uploadSeckillSkuLatest3Days
抽象方法
package com.atguigu.gulimall.seckill.service.impl;
import com.atguigu.gulimall.seckill.service.SeckillService;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
*/
public class SeckillServiceImpl implements SeckillService {
@Override
public void uploadSeckillSkuLatest3Days() {
//1.扫描需要参与秒杀的活动
}
}

2、远程获取秒杀活动场次
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication
类上添加@EnableFeignClients
注解,用于开启Feign
的远程调用功能
@EnableFeignClients

在gulimall-seckill
模块的com.atguigu.gulimall.seckill
包里新建feign
文件夹,在feign
文件夹里新建CouponFeignService
接口,用于远程调用优惠模块
package com.atguigu.gulimall.seckill.feign;
import org.springframework.cloud.openfeign.FeignClient;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
}

在gulimall-coupon
模块的com.atguigu.gulimall.coupon.controller.SeckillSessionController
类里添加getLatest3DaySession
方法,用于获取最近3天的秒杀活动场次信息
@GetMapping("/latest3DaySession")
public R getLatest3DaySession(){
List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
return R.ok().put("data",sessions);
}

在gulimall-coupon
模块的com.atguigu.gulimall.coupon.service.SeckillSessionService
接口里添加getLatest3DaySession
抽象方法
List<SeckillSessionEntity> getLatest3DaySession();

在gulimall_sms
数据库里执行如下sql,可以看到已经成功获取到最近3天的秒杀场次信息了
select * from sms_seckill_session where start_time between '2022-08-23 00:00:00' and '2022-08-25 23:59:59'

3、测试日期
修改gulimall-coupon
模块的com.atguigu.gulimall.coupon.GulimallCouponApplicationTests
类
package com.atguigu.gulimall.coupon;
import org.junit.Test;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
//@RunWith(SpringRunner.class)
//@SpringBootTest(classes = GulimallCouponApplication.class)
public class GulimallCouponApplicationTests {
@Test
public void contextLoads() {
LocalDate now = LocalDate.now();
LocalDate plus2 = now.plusDays(2);
System.out.println(now);
System.out.println(plus2);
System.out.println("=============================");
LocalTime min = LocalTime.MIN;
LocalTime max = LocalTime.MAX;
System.out.println(min);
System.out.println(max);
System.out.println("=============================");
LocalDateTime nowDateTime = LocalDateTime.of(now, min);
LocalDateTime plus2DateTime = LocalDateTime.of(plus2, max);
System.out.println(nowDateTime);
System.out.println(plus2DateTime);
System.out.println("=============================");
}
}

执行该类的contextLoads
测试方法,可以看到如下输出,这里已经成功计算了时间范围,只是时间格式不符合国人审美,如果不要紧,反正又不用看这时间,只要秒杀的活动场次日期在这两个日期的范围之内即可。当然格式化也行,反正也不难。
2022-08-23
2022-08-25
=============================
00:00
23:59:59.999999999
=============================
2022-08-23T00:00
2022-08-25T23:59:59.999999999
=============================

4、实现getLatest3DaySession
在gulimall-coupon
模块的com.atguigu.gulimall.coupon.service.impl.SeckillSessionServiceImpl
类里,添加startTime
方法和endTime
方法,修改getLatest3DaySession
方法
@Override
public List<SeckillSessionEntity> getLatest3DaySession() {
LambdaQueryWrapper<SeckillSessionEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.between(SeckillSessionEntity::getStartTime,startTime(),endTime());
return this.list(lambdaQueryWrapper);
}
private String startTime(){
LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
return start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
private String endTime(){
LocalDateTime endTime = LocalDateTime.of(LocalDate.now().plusDays(2), LocalTime.MAX);
return endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

在gulimall-coupon
模块的com.atguigu.gulimall.coupon.entity.SeckillSessionEntity
类里添加如下字段。
@TableField(exist = false)
private List<SeckillSkuRelationEntity> relationSkus;

在gulimall-coupon
模块的com.atguigu.gulimall.coupon.service.impl.SeckillSessionServiceImpl
类里,修改getLatest3DaySession
方法
@Autowired
SeckillSkuRelationService seckillSkuRelationService;
@Override
public List<SeckillSessionEntity> getLatest3DaySession() {
LambdaQueryWrapper<SeckillSessionEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.between(SeckillSessionEntity::getStartTime,startTime(),endTime());
List<SeckillSessionEntity> list = this.list(lambdaQueryWrapper);
if (CollectionUtils.isEmpty(list)){
return null;
}
return list.stream().map(seckillSessionEntity -> {
Long id = seckillSessionEntity.getId();
LambdaQueryWrapper<SeckillSkuRelationEntity> skuRelationQueryWrapper = new LambdaQueryWrapper<>();
skuRelationQueryWrapper.eq(SeckillSkuRelationEntity::getPromotionSessionId, id);
List<SeckillSkuRelationEntity> skuRelationEntities = seckillSkuRelationService.list(skuRelationQueryWrapper);
seckillSessionEntity.setRelationSkus(skuRelationEntities);
return seckillSessionEntity;
}).collect(Collectors.toList());
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.feign.CouponFeignService
接口里,添加getLatest3DaySession
方法
@GetMapping("/coupon/seckillsession/latest3DaySession")
public R getLatest3DaySession();

在gulimall-seckill
模块的com.atguigu.gulimall.seckill
包里新建vo
文件夹,在vo
文件夹里添加SeckillSessionSkusVo
类
package com.atguigu.gulimall.seckill.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
*/
@Data
public class SeckillSessionSkusVo {
/**
* id
*/
private Long id;
/**
* 场次名称
*/
private String name;
/**
* 每日开始时间
*/
private Date startTime;
/**
* 每日结束时间
*/
private Date endTime;
/**
* 启用状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
private List<SeckillSkuRelationVo> relationSkus;
@Data
public static class SeckillSkuRelationVo{
/**
* id
*/
private Long id;
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private BigDecimal seckillCount;
/**
* 每人限购数量
*/
private BigDecimal seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
}
}

在gulimall-common
模块的com.atguigu.common.utils.R
类里添加如下代码
public Object getData(){
return this.get("data");
}
public boolean isOk(){
return this.getCode() == 0;
}
public boolean hasError(){
return this.getCode() != 0;
}
public <T> T getData(Class<T> clazz){
String s = JSON.toJSONString(this.getData());
return JSON.parseObject(s,clazz);
}
public <T> List<T> getDataArray(Class<T> clazz){
String s = JSON.toJSONString(this.getData());
return JSON.parseArray(s,clazz);
}
public <T> T getData(TypeReference<T> tTypeReference) {
Object data = get("data");
String s = JSON.toJSONString(data);
return JSON.parseObject(s,tTypeReference);
}

saveSessionInfos
5、添加在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里添加saveSessionInfos
方法,修改uploadSeckillSkuLatest3Days
方法
@Override
public void uploadSeckillSkuLatest3Days() {
//1.扫描需要参与秒杀的活动
R r = couponFeignService.getLatest3DaySession();
if (r.isOk()){
//上架商品
List<SeckillSessionSkusVo> sessionSkusVos = r.getDataArray(SeckillSessionSkusVo.class);
//缓存活动信息
saveSessionInfos(sessionSkusVos);
//缓存活动的关联商品信息
saveSessionSkuInfos(sessionSkusVos);
}
}
private void saveSessionInfos(List<SeckillSessionSkusVo> sessionSkusVos){
if (StringUtils.isEmpty(sessionSkusVos)){
return;
}
sessionSkusVos.forEach(session->{
long start = session.getStartTime().getTime();
long end = session.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + start + "_" + end;
List<String> values = session.getRelationSkus().stream()
.map(item -> item.getSkuId().toString()).collect(Collectors.toList());
//缓存活动信息
redisTemplate.opsForList().leftPushAll(key,values);
});
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill
包里新建to
文件夹,在to
文件夹里新建SeckillSkuRedisTo
类

在gulimall-product
模块的com.atguigu.gulimall.product.controller.SkuInfoController
类里,已经有一个查询商品信息的info
方法了

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.feign
包里新建ProductFeignService
接口
package com.atguigu.gulimall.seckill.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author 无名氏
* @date 2022/8/23
* @Description:
*/
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
public R getSkuInfo(@PathVariable("skuId") Long skuId);
}

修改gulimall-common
模块的com.atguigu.common.utils.R
类,添加get(String key,Class<T> clazz)
、getArray(String key,Class<T> clazz)
、getObjectStr(String key)
等方法,修改getData(Class<T> clazz)
、getDataArray(Class<T> clazz)
、getData(TypeReference<T> tTypeReference)
方法

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里修改saveSessionSkuInfos
方法
@Autowired
ProductFeignService productFeignService;
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
private void saveSessionSkuInfos(List<SeckillSessionSkusVo> sessionSkusVos){
if (StringUtils.isEmpty(sessionSkusVos)){
return;
}
Map<String,String> seckillSkuInfos = new HashMap<>();
sessionSkusVos.forEach(session->{
Map<String, String> map = session.getRelationSkus().stream().map(
seckillSkuRelationVo -> {
SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
BeanUtils.copyProperties(seckillSkuRelationVo,seckillSkuRedisTo);
R r = productFeignService.getSkuInfo(seckillSkuRelationVo.getSkuId());
if (r.isOk()){
SeckillSkuRedisTo.SkuInfoVo skuInfoVo = r.get("skuInfo", SeckillSkuRedisTo.SkuInfoVo.class);
seckillSkuRedisTo.setSkuInfoVo(skuInfoVo);
}
//设置开始和结束时间
seckillSkuRedisTo.setStartTime(session.getStartTime().getTime());
seckillSkuRedisTo.setEndTime(session.getEndTime().getTime());
//设置随机码(只有秒杀开始的那一刻,才暴露随机码)(防止活动还没开始就准备好脚本,开始时直接抢购)
String token = UUID.randomUUID().toString().replace("-","");
seckillSkuRedisTo.setRandomCode(token);
return seckillSkuRedisTo;
}
).collect(Collectors.toMap(k -> k.getSkuId().toString(), JSON::toJSONString));
seckillSkuInfos.putAll(map);
});
redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX).putAll(seckillSkuInfos);
}

7.1.3、分布式信号量
redisson
1、引入1、添加依赖和配置
由于秒杀的请求量大,不可能查数据库,因此可以使用分布式信号量机制。
在gulimall-seckill
模块的pom.xml
文件里添加如下依赖,引入redisson
<!-- 引入redisson,做分布式锁和分布式对象 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>

复制gulimall-product
模块的com.atguigu.gulimall.product.config.MyRedissonConfig
类,到gulimall-seckill
模块的com.atguigu.gulimall.seckill.config
包下
package com.atguigu.gulimall.seckill.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* @author 无名氏
* @date 2022/7/16
* @Description:
*/
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
//Redis url should start with redis:// or rediss:// (for SSL connection)
//config.useSingleServer().setAddress("192.168.56.10:6379").setPassword("");
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
//2、根据Config创建出RedissonClient示例
return Redisson.create(config);
}
}

redisson
2、使用在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里修改saveSessionSkuInfos
方法,在seckillSkuRedisTo.setRandomCode(token);
这行下面加上使用分布式信号量限流
的相关代码
@Autowired
RedissonClient redissonClient;
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
//使用分布式信号量限流
//信号量的key为`前缀+token` value为商品的库存
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
semaphore.trySetPermits(seckillSkuRelationVo.getSeckillSort());

3、准备测试
修改gulimall_sms
数据库的sms_seckill_session
表,修改这两场秒杀商品的开始时间和结束时间,将第一个秒杀场次设为当前时间之后的近三天的时间,第二个秒杀场次设为当前时间之前的时间

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled
类里修改uploadSeckillSkuLatest3Days
方法,将@Scheduled(cron = "0 0 3 * * ?")
修改为@Scheduled(cron = "0 * * * * ?")
(秒为0
时执行一次,即每分钟执行一次),并添加log.info("上架秒杀的商品信息...");
/**
* 每天晚上3点,上架最近3天需要秒杀的商品
*/
@Scheduled(cron = "0 * * * * ?")
public void uploadSeckillSkuLatest3Days(){
//1、重复上架无需处理
log.info("上架秒杀的商品信息...");
seckillService.uploadSeckillSkuLatest3Days();
}

修改gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.HelloSchedule
类的hello
方法上的注解,将@Scheduled(cron = "* * * ? * 2")
里的2
修改为*
@Async
@Scheduled(cron = "* * * ? * *")
public void hello() throws InterruptedException {
log.info("hello...");
TimeUnit.SECONDS.sleep(3);
}

4、测试
重启GulimallSeckillApplication
服务和GulimallCouponApplication
服务
提示SeckillSkuScheduled
类里注入SeckillService
失败,在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类上加个@Service
注解就好了
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-08-24 10:58:56.436 ERROR 9896 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Field seckillService in com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled required a bean of type 'com.atguigu.gulimall.seckill.service.SeckillService' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'com.atguigu.gulimall.seckill.service.SeckillService' in your configuration.

重启GulimallSeckillApplication
服务,只让上架秒杀的商品信息
的定时任务执行一次,然后立马关掉GulimallSeckillApplication
服务

查看redis
里seckill:sessions:
的信息,可以看到2022-08-26 00:00:00
点秒杀场次的商品id
已经显示出来了

打开 http://localhost:8001/#/coupon-seckillsession 页面,在优惠营销
-> 每日秒杀
里,点击开始时间为2022-08-26 00:00:00
的这个场次的操作里的关联商品
,在关联秒杀商品
弹出框里可以看到商品id
与redis
里的存储的秒杀商品的值一致

查看redis
里seckill:skus:
的信息,成功查询到了2个促销信息和关联的商品信息(这里应该有3个促销信息的,因为2个活动总共有3款促销,但其中两款促销是同一种商品,由于使用的是seckill:skus:
+SkuId
作为key
,因此这两款相同商品的促销信息只保存了一份,因此只有两个促销信息)

查看redis
里seckill:stock:
的信息,发现这个库存有问题,所有的库存都为1

5、修改代码重新测试
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类的saveSessionSkuInfos
方法里,把semaphore.trySetPermits(seckillSkuRelationVo.getSeckillSort()):
修改为semaphore.trySetPermits(seckillSkuRelationVo.getSeckillCount().intValue());
。应该设为商品库存的,这里不小心设成排序字段了

删除删除redis
里前缀为seckill
的数据,重新运行GulimallSeckillApplication
服务,只让上架秒杀的商品信息
的定时任务执行一次,然后关闭GulimallSeckillApplication
服务

查看redis
里seckill:stock:
的信息,可以看到秒杀总量已经正常了

打开 http://localhost:8001/#/coupon-seckillsession 页面,在优惠营销
-> 每日秒杀
里,点击开始时间为2022-08-26 00:00:00
的这个场次的操作里的关联商品
,在关联秒杀商品
弹出框里可以看到秒杀总量
与redis
里存储的秒杀总量的值一致

如果让上架秒杀的商品信息
的定时任务执行两次,可以看到在redis
里的seckill:sessions:
里,seckill:sessions:1661472000000_1661479200000
里有4
条数据,而其实多次上架应该也还是2
条,应该覆盖旧的数据而不是添加新的数据

而在redis
里的seckill:skus:
里,使用的是map
,所以没啥影响

在redis
里的seckill:stock:
里有6
条数据,而其实应该有3
条数据,商品库存信息也应该是覆盖而不是添加

2、定时任务-分布式下的问题
有可能多台机器同时执行定时任务,因此可以加一个分布式锁,只让一个机器执行,执行完后,别的机器判断该定时任务是否已经完成,如果已经做了,就不再向redis
里保存数据了

1、修改代码
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled
类里修改uploadSeckillSkuLatest3Days
方法
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
/**
* 每天晚上3点,上架最近3天需要秒杀的商品
*/
@Scheduled(cron = "0 * * * * ?")
public void uploadSeckillSkuLatest3Days(){
//1、重复上架无需处理
log.info("上架秒杀的商品信息...");
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try {
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里,修改saveSessionInfos
方法
private void saveSessionInfos(List<SeckillSessionSkusVo> sessionSkusVos) {
if (StringUtils.isEmpty(sessionSkusVos)) {
return;
}
sessionSkusVos.forEach(session -> {
long start = session.getStartTime().getTime();
long end = session.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + start + "_" + end;
//缓存活动信息
Boolean hasKey = redisTemplate.hasKey(key);
if (hasKey == null || !hasKey) {
List<String> values = session.getRelationSkus().stream()
.map(item -> item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key, values);
}
});
}

(但是我感觉这有问题,有可能2个活动都上架了该商品,这两个都应该设置不同的促销信息和库存,而这个库存是判断skuId存不存在 因此不同活动不能上架同一款商品,我觉得应该放skuId+活动时间/随机码,这样才能区分是哪个活动,才能让不同的活动上架同一款商品,保证每个活动的促销信息和库存不一样)
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里,再次修改saveSessionInfos
方法
private void saveSessionSkuInfos(List<SeckillSessionSkusVo> sessionSkusVos) {
if (StringUtils.isEmpty(sessionSkusVos)) {
return;
}
sessionSkusVos.forEach(session -> {
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
session.getRelationSkus().forEach(seckillSkuRelationVo -> {
String skuKey = seckillSkuRelationVo.getSkuId().toString();
Boolean hasSkuKey = operations.hasKey(skuKey);
if (hasSkuKey == null || !hasSkuKey) {
SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
BeanUtils.copyProperties(seckillSkuRelationVo, seckillSkuRedisTo);
R r = productFeignService.getSkuInfo(seckillSkuRelationVo.getSkuId());
if (r.isOk()) {
SeckillSkuRedisTo.SkuInfoVo skuInfoVo = r.get("skuInfo", SeckillSkuRedisTo.SkuInfoVo.class);
seckillSkuRedisTo.setSkuInfoVo(skuInfoVo);
}
//设置开始和结束时间
seckillSkuRedisTo.setStartTime(session.getStartTime().getTime());
seckillSkuRedisTo.setEndTime(session.getEndTime().getTime());
//设置随机码(只有秒杀开始的那一刻,才暴露随机码)(防止活动还没开始就准备好脚本,开始时直接抢购)
String token = UUID.randomUUID().toString().replace("-", "");
seckillSkuRedisTo.setRandomCode(token);
operations.put(skuKey, JSON.toJSONString(seckillSkuRedisTo));
//使用分布式信号量限流(只有上架了商品,才有库存信息)
//(但是我感觉这有问题,有可能2个活动都上架了该商品,这两个都应该设置不同的促销信息和库存,而这个库存是判断skuId存不存在
// 因此不同活动不能上架同一款商品,我觉得应该放skuId+活动时间/随机码,这样才能区分是哪个活动,才能让不同的活动上架同一款商品,保证每个活动的促销信息和库存不一样)
//信号量的key为`前缀+token` value为商品的库存
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
semaphore.trySetPermits(seckillSkuRelationVo.getSeckillCount().intValue());
}
}
);
});
}

2、测试
删除redis
里以seckill
开头的数据,重启GulimallSeckillApplication
服务,让上架秒杀的商品信息
的定时任务执行多次
可以看到seckill:sessions:
里多次执行上架秒杀的商品信息
的定时任务后并没有多次添加了

seckill:skus:
里还是两个商品数据(这里应该有3个促销信息的,因为2个活动总共有3款促销,但其中两款促销是同一种商品,由于使用的是seckill:skus:
+SkuId
作为key
,因此这两款相同商品的促销信息只保存了一份,因此只有两个促销信息)

在seckill:stock:
里多次执行上架秒杀的商品信息
的定时任务后也没有多次添加了,但此时只有2
条数据了,而原本应该有3条的。原先由于多次促销信息的randomCode
随机码不一样,因此可以保存多次。而现在使用的是seckill:skus:
+SkuId
作为key
,即使是不同的促销活动的相同sku,如果该key存在了也不执行向redis
里添加库存的操作了,因此seckill:skus:
和seckill:stock:
里的数据数量是一样的

3、修改代码后再次测试
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里,修改saveSessionInfos
方法
将
String skuKey = seckillSkuRelationVo.getSkuId().toString();
替换为
String skuKey = session.getStartTime().getTime() +"_"+ seckillSkuRelationVo.getSkuId().toString();
用于区分不同活动的促销信息。

可以看到seckill:sessions:
里多次执行上架秒杀的商品信息
的定时任务后并没有多次添加了

seckill:skus:
里变为了3
个商品数据(因为使用session.getStartTime().getTime() +"_"+ seckillSkuRelationVo.getSkuId().toString();
作为key
即开始时间id+商品id
,即使这两款是相同商品,只要场次不一样,还是分开保存的)

在seckill:stock:
里多次执行上架秒杀的商品信息
的定时任务后也没有多次添加了,而且也是正确的3
条数据

4、修改为老师所用的id
我用的是开始时间id+商品id
,老师用的是场次id+商品id
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里,修改saveSessionInfos
方法
把
String skuKey = session.getStartTime().getTime() +"_"+ seckillSkuRelationVo.getSkuId().toString();
改为
String skuKey = seckillSkuRelationVo.getPromotionSessionId().toString() +"_"+ seckillSkuRelationVo.getSkuId().toString();
效果都一样,不过老师讲的方法显得更清晰一些

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里,修改saveSessionInfos
方法
把
List<String> values = session.getRelationSkus().stream()
.map(item -> item.getSkuId().toString()).collect(Collectors.toList());
改为,为了更好区分一下
List<String> values = session.getRelationSkus().stream()
.map(item ->item.getPromotionSessionId().toString()+"_"+ item.getSkuId().toString())
.collect(Collectors.toList());

删除redis
里以seckill
开头的数据,重启GulimallSeckillApplication
服务,再让上架秒杀的商品信息
的定时任务执行多次
可以看到seckill:sessions:
里多次执行上架秒杀的商品信息
的定时任务后并没有多次添加了,而且也更容易区分场次信息了

seckill:skus:
里变为了3
个商品数据(因为使用seckillSkuRelationVo.getPromotionSessionId().toString() +"_"+ seckillSkuRelationVo.getSkuId().toString();
作为key
即场次id+商品id
,即使这两款是相同商品,只要场次不一样,还是分开保存的)

在seckill:stock:
里多次执行上架秒杀的商品信息
的定时任务后也没有多次添加了,而且也是正确的3
条数据

都设置好了,不过好像都没设置过期时间
3、获取当前秒杀商品
1、添加getCurrentSeckillSkus方法
在gulimall-seckill
模块的com.atguigu.gulimall.seckill
包里添加controller
文件夹,在controller
文件夹里新建SeckillController
类
package com.atguigu.gulimall.seckill.controller;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.seckill.service.SeckillService;
import com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author 无名氏
* @date 2022/8/24
* @Description:
*/
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 返回当前时间可以参与的秒杀商品信息
* @return
*/
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.SeckillService
接口里添加getCurrentSeckillSkus
抽象方法
List<SeckillSkuRedisTo> getCurrentSeckillSkus();

2、查看文档
可以看到org.springframework.data.redis.core.ListOperations
接口的List<V> range(K key, long start, long end);
方法相当于redis
里的lrange
命令(从左开始查找指定范围的数据)

redis文档: LRANGE | Redis
redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LRANGE mylist 0 0
1) "one"
redis> LRANGE mylist -3 2
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist -100 100
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist 5 10
(empty array)
redis> LRANGE mylist 0 -1
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist 0 -2
1) "one"
2) "two"
redis> LRANGE mylist -1 0
(empty array)
redis> LRANGE mylist -1 1
(empty array)
redis> LRANGE mylist -1 2
1) "three"
redis> LRANGE mylist -1 3
1) "three"
redis> LRANGE mylist -1 100
1) "three"
redis> LRANGE mylist -2 10
1) "two"
2) "three"
redis> LRANGE mylist -3 0
1) "one"

3、修改代码
修改gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类的getCurrentSeckillSkus
方法
我发现一个奇怪的事情,operations.multiGet();
方法参数类型为Collection<HK> keys
,不能传List<String>
类型的range
我能理解;但是Collections.singleton(range)
这个就很奇怪,其类型明明为Set<List<String>>
,直接传不报错,接收成一个变量再传竟然就报错了
List<String> range = redisTemplate.opsForList().range(key, 0, -1);
List<Object> list = Arrays.asList(range.toArray());
operations.multiGet(list);
operations.multiGet(range);
Set<List<String>> singleton = Collections.singleton(range);
operations.multiGet(singleton);
operations.multiGet(Collections.singleton(range));
operations.multiGet()

这是使用Alt+Enter
快捷键提示的解决报错的建议

这是使用range.
后的提示,看来使用.
比报错提示
有效些😛

4、测试
Collections.singleton(range)
1、使用修改gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类的getCurrentSeckillSkus
方法,先使用Collections.singleton(range)
测试看看
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
long time = System.currentTimeMillis();
Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
for (String key : keys) {
String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] split = replace.split("_");
Long start = Long.parseLong(split[0]);
Long end = Long.parseLong(split[1]);
if (time>=start && time <=end){
//这里的 -1相当于length-1,即最后一个元素。取出的结果为[0,length-1] 包含开始和最后的元素,即所有元素
List<String> range = redisTemplate.opsForList().range(key, 0, -1);
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
if (range != null) {
List<Object> list = operations.multiGet(Collections.singleton(range));
if (!CollectionUtils.isEmpty(list)) {
List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(item.toString(), SeckillSkuRedisTo.class);
//秒杀开始后可以查看到随机码
//seckillSkuRedisTo.setRandomCode(null);
return seckillSkuRedisTo;
}).collect(Collectors.toList());
return collect;
}
}
//只要找到了在当前时间范围内的秒杀,不管range是否为null都退出循环(当然如果range不为null,直接就return了)
break;
}
}
return null;
}

2、添加秒杀场次
打开 http://localhost:8001/#/coupon-seckillsession 页面,在优惠营销
-> 每日秒杀
里,点击新增,添加一个最近的正在秒杀的秒杀场次。(即现在的时间在新建的秒杀场次的开始时间和结束时间之内)

在优惠营销
-> 每日秒杀
页面里,点击刚刚创建的秒杀场次里的操作的关联商品
,在关联秒杀商品
里点击新增
,新增如下关联商品

点击确定后,就看看到关联的秒杀商品已经添加进来了

ArrayList
不能强转成String
3、然后启动GulimallSeckillApplication
服务,等待刚刚创建的秒杀活动保存到redis
,访问 http://localhost:25000/currentSeckillSkus 页面,报了强转的错误

打开GulimallSeckillApplication
服务的控制台,报了ArrayList
不能强转成String
的错误
2022-08-24 18:46:30.998 ERROR 5916 --- [io-25000-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.lang.String] with root cause
java.lang.ClassCastException: java.util.ArrayList cannot be cast to java.lang.String
at org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:36) ~[spring-data-redis-2.1.10.RELEASE.jar:2.1.10.RELEASE]
at org.springframework.data.redis.core.AbstractOperations.rawHashKey(AbstractOperations.java:165) ~[spring-data-redis-2.1.10.RELEASE.jar:2.1.10.RELEASE]
at org.springframework.data.redis.core.DefaultHashOperations.multiGet(DefaultHashOperations.java:172) ~[spring-data-redis-2.1.10.RELEASE.jar:2.1.10.RELEASE]
at org.springframework.data.redis.core.DefaultBoundHashOperations.multiGet(DefaultBoundHashOperations.java:74) ~[spring-data-redis-2.1.10.RELEASE.jar:2.1.10.RELEASE]
at com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl.getCurrentSeckillSkus(SeckillServiceImpl.java:75) ~[classes/:na]
at com.atguigu.gulimall.seckill.controller.SeckillController.getCurrentSeckillSkus(SeckillController.java:29)

4、方法一
可以将Collections.singleton(range)
修改为Arrays.asList(range.toArray())

重启GulimallSeckillApplication
服务后,再次访问 http://localhost:25000/currentSeckillSkus 页面,可以看到如下json
{"msg":"success","code":0,"data":[{"promotionId":null,"promotionSessionId":3,"skuId":1,"randomCode":"bdc4f396f9ac49539bd1668d908488da","seckillPrice":999,"seckillCount":50,"seckillLimit":1,"seckillSort":0,"startTime":1661335200000,"endTime":1661346000000,"skuInfoVo":{"skuId":1,"spuId":1,"skuName":"华为 HUAWEI Mate30Pro 星河银 8GB+128GB","skuDesc":null,"catalogId":225,"brandId":1,"skuDefaultImg":"https://gulimall-anonymous.oss-cn-beijing.aliyuncs.com/2022-05-21//b90b1cb4-edd9-4c91-8418-18594da32471_0d40c24b264aa511.jpg","skuTitle":"华为 HUAWEI Mate30Pro 星河银 8GB+128GB 麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄 4G全网通手机","skuSubtitle":"[现货抢购!享白条12期免息!]麒麟990, OLED环幕屏双4000万徕卡电影四摄:Mate30系列享12期免息》","price":5799.0,"saleCount":0}}]}

5、方法二(推荐)
修改BoundHashOperations
的泛型,把BoundHashOperations<String, Object, Object> operations
改为BoundHashOperations<String, String, Object> operations
。把operations.multiGet(Arrays.asList(range.toArray()));
改为operations.multiGet(range);
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
long time = System.currentTimeMillis();
Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
for (String key : keys) {
String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] split = replace.split("_");
Long start = Long.parseLong(split[0]);
Long end = Long.parseLong(split[1]);
if (time>=start && time <=end){
//这里的 -1相当于length-1,即最后一个元素。取出的结果为[0,length-1] 包含开始和最后的元素,即所有元素
List<String> range = redisTemplate.opsForList().range(key, 0, -1);
BoundHashOperations<String, String, Object> operations = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
if (range != null) {
List<Object> list = operations.multiGet(range);
if (!CollectionUtils.isEmpty(list)) {
List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(item.toString(), SeckillSkuRedisTo.class);
//秒杀开始后可以查看到随机码
//seckillSkuRedisTo.setRandomCode(null);
return seckillSkuRedisTo;
}).collect(Collectors.toList());
return collect;
}
}
//只要找到了在当前时间范围内的秒杀,不管range是否为null都退出循环(当然如果range不为null,直接就return了)
break;
}
}
return null;
}

删除reids
里seckill
开头的key
,重新启动GulimallSeckillApplication
服务,等待刚刚创建的秒杀活动保存到redis
可以看到这样也可以保存成功 http://localhost:25000/currentSeckillSkus

6、原因
鼠标放到operations.multiGet();
的方法的括号里,使用ctrl+p
快捷键查看参数的类型,此时类型为Collection<Object>

这是因为我们最开始使用的BoundHashOperations
的泛型是BoundHashOperations<String, Object, Object> operations
,此时的HK
为Object
public interface BoundHashOperations<H, HK, HV> extends BoundKeyOperations<H> {
/**
* Delete given hash {@code keys} at the bound key.
*
* @param keys must not be {@literal null}.
* @return {@literal null} when used in pipeline / transaction.
*/
@Nullable
Long delete(Object... keys);
/**
* Determine if given hash {@code key} exists at the bound key.
*
* @param key must not be {@literal null}.
* @return {@literal null} when used in pipeline / transaction.
*/
@Nullable
Boolean hasKey(Object key);
/**
* Get value for given {@code key} from the hash at the bound key.
*
* @param member must not be {@literal null}.
* @return {@literal null} when member does not exist or when used in pipeline / transaction.
*/
@Nullable
HV get(Object member);
/**
* Get values for given {@code keys} from the hash at the bound key.
*
* @param keys must not be {@literal null}.
* @return {@literal null} when used in pipeline / transaction.
*/
@Nullable
List<HV> multiGet(Collection<HK> keys);
......
}

List<HV> multiGet(Collection<HK> keys);
方法的参数类型为Collection<HK>
,由于我们使用的HK
为Object
,所以该方法的参数的类型为Collection<Object>

而修改BoundHashOperations
的泛型为BoundHashOperations<String, String, Object> operations
后, List<HV> multiGet(Collection<HK> keys);
方法的参数类型就为Collection<String>
了

我们可以将BoundHashOperations
的泛型全改为String
,即BoundHashOperations<String, String, String>

7.1.4、显示秒杀商品
1、添加配置
在gulimall-gateway
模块的src/main/resources/application.yml
配置文件里添加如下配置,将seckill.gulimall.com
域名的请求全部负载均衡到gulimall-seckill
模块
spring:
cloud:
gateway:
routes:
- id: gulimall_seckill_route
uri: lb://gulimall-seckill
predicates:
- Host=seckill.gulimall.com

打开SwitchHosts
软件,依次点击hosts
->本地方案
->gulimall
,在后面添加192.168.56.10 seckill.gulimall.com
,然后点击对勾
图标
# gulimall
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
192.168.56.10 auth.gulimall.com
192.168.56.10 cart.gulimall.com
192.168.56.10 order.gulimall.com
192.168.56.10 member.gulimall.com
192.168.56.10 seckill.gulimall.com

重启GulimallGatewayApplication
服务,访问 http://seckill.gulimall.com/currentSeckillSkus ,可以看到通过网关也可以访问了

2、显示秒杀商品
在 http://gulimall.com/ 页面里,打开控制台,定位到秒杀的某个图片
位置,复制/static/index/img/section_second_list_img1.jpg

在gulimall-product
模块的src/main/resources/templates/index.html
文件夹搜索/static/index/img/section_second_list_img1.jpg
,复制第一个<li>
标签,将四个<li>
标签全部删掉,一个也不保留,并给其父<ul>
标签加上id="seckillContent"

修改后的代码如下

在gulimall-order
模块的src/main/resources/templates/detail.html
文件里的<script>
标签里,添加如下代码
$.get("http://seckill.gulimall.com/currentSeckillSkus",function (resp){
if (resp.code==0 && resp.data.length>0){
resp.data.forEach(function (item) {
$("<li></li>")
.append("<img style='width: 130px;height: 130px;' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
.append("<p>"+item.skuInfoVo.skuTitle+"</p>")
.append("<span>"+item.seckillPrice+"</span>")
.append("<s>"+item.skuInfoVo.price+"</s>")
.appendTo("#seckillContent");
})
}
})

打开 http://gulimall.com/ 页面,可以看到秒杀的商品已经显示出来了

3、查询当前sku是否参与秒杀优惠
1、获取sku秒杀信息
在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl
类的item
方法添加查询当前sku是否参与秒杀优惠
功能,这个等会再做

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.controller.SeckillController
类里添加getSkuSeckillInfo
方法
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId){
SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.SeckillService
接口里添加getSkuSeckillInfo
抽象方法
SeckillSkuRedisTo getSkuSeckillInfo(Long skuId);

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里实现getSkuSeckillInfo
方法
(这里有问题,因为只查询了一个活动的该商品信息,如果一个包含该商品的秒杀活动已经过去了,而新的还未开始的活动又包含该商品,有可能查询到已经过去的秒杀活动,导致没有出现秒杀信息)
/**
* 查询指定sku的一个秒杀信息
* @param skuId
* @return
*/
@Override
public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
Set<String> keys = hashOps.keys();
if (!CollectionUtils.isEmpty(keys)) {
String regx = "\\d_" + skuId;
for (String key : keys) {
if (Pattern.matches(regx, key)) {
String s = hashOps.get(key);
if (s == null) {
return null;
}
SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);
long now = System.currentTimeMillis();
if (now < seckillSkuRedisTo.getStartTime() || now > seckillSkuRedisTo.getEndTime()) {
//不返回随机码
seckillSkuRedisTo.setRandomCode(null);
}
return seckillSkuRedisTo;
}
}
}
return null;
}

2、远程调用秒杀模块
在gulimall-product
模块的com.atguigu.gulimall.product.feign
包里新建SearchFeignService
接口
package com.atguigu.gulimall.product.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author 无名氏
* @date 2022/8/24
* @Description:
*/
@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}

复制gulimall-seckill
模块的com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo
类里除private SkuInfoVo skuInfoVo;
的字段,粘贴到gulimall-product
模块的com.atguigu.gulimall.product.vo
包里

在gulimall-product
模块的com.atguigu.gulimall.product.vo.SkuItemVo
类里添加seckillInfo
字段(不是刚刚新添加的seckillInfo
类,别添加错了)
/**
* 当前商品的秒杀优惠信息
*/
SeckillInfoVo seckillInfo;

在gulimall-product
模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl
类里,修改item
方法,在CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();
上面添加如下异步执行的请求,并将其添加到CompletableFuture.allOf
的参数里面
@Autowired
SeckillFeignService seckillFeignService;
//查询当前sku是否参与秒杀优惠
CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
R r = seckillFeignService.getSkuSeckillInfo(skuId);
if (r.isOk()) {
SeckillInfoVo seckillInfoVo = r.getData(SeckillInfoVo.class);
skuItemVo.setSeckillInfo(seckillInfoVo);
}
}, executor);
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();

7.1.5、商品页面添加秒杀提醒
1、商品页添加秒杀信息
1、商品页添加秒杀开始时间
在 http://gulimall.com/5.html 页面里,打开控制台,定位到预约享资格
位置,复制预约享资格

在gulimall-product
模块的src/main/resources/templates/item.html
配置文件里搜索预约享资格
,将预约享资格
修改为[[${item.seckillInfo.startTime}]]
<li style="color: red" th:if="${item.seckillInfo!=null}">
<!--预约享资格-->
[[${item.seckillInfo.startTime}]]
</li>

找一个有秒杀的sku
的商品,访问其url,例如 http://item.gulimall.com/1.html ,此时已经显示开始时间了,只不过显示的是时间戳

在gulimall-product
模块的src/main/resources/templates/item.html
配置文件里,将[[${item.seckillInfo.startTime}]]
修改为 [[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]]
<li style="color: red" th:if="${item.seckillInfo!=null}">
<!--预约享资格-->
[[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]]
</li>

刷新 http://item.gulimall.com/1.html 页面,此时已经显示正常格式的开始时间了

2、商品页添加秒杀价
打开 http://localhost:8001/#/coupon-seckillsession 页面,在优惠营销
-> 每日秒杀
里,点击前面创建的2022-08-24 18:00:00
点场的秒杀场次里的操作的关联商品
,在关联秒杀商品
里可以看到商品的id为1

在gulimall-product
模块的src/main/resources/templates/item.html
配置文件里搜索预约享资格
,修改预约享资格
对应的<li>
标签对应的代码,以显示秒杀价格
<li style="color: red" th:if="${item.seckillInfo!=null}">
<span th:if="${#dates.createNow().getTime() < item.seckillInfo.startTime}">
<!--预约享资格-->
商品将会在[[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀
</span>
<span th:if="${#dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
</span>
</li>

这里显示秒杀价了(不过这里有问题,因为只查询了一个活动的该商品信息,如果一个包含该商品的秒杀活动已经过去了,而新的还未开始的活动又包含该商品,有可能查询到已经过去的秒杀活动,导致没有出现新的秒杀信息)

在gulimall-product
模块的src/main/resources/templates/index.html
文件里的<script>
标签里,添加如下代码
function to_href(skuId){
location.href = "http://item.gulimall.com/"+skuId+".html"
}
$.get("http://seckill.gulimall.com/currentSeckillSkus",function (resp){
if (resp.code==0 && resp.data.length>0){
resp.data.forEach(function (item) {
var href = "http://item.gulimall.com/"+item.skuId+".html"
$("<li onclick='to_href("+item.skuId+")'></li>")
.append("<img style='width: 130px;height: 130px;' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
.append("<p>"+item.skuInfoVo.skuTitle+"</p>")
.append("<span>"+item.seckillPrice+"</span>")
.append("<s>"+item.skuInfoVo.price+"</s>")
.appendTo("#seckillContent");
})
}
})

在 http://gulimall.com/ 页面里,点击一个秒杀商品,来到了 http://item.gulimall.com/1.html 页面,此时页面已经显示秒杀价
了

2、处理秒杀逻辑
1、高并发系统关注的问题
秒杀( 高并发) 系统应关注以下问题


2、修改秒杀场次时间
在 http://item.gulimall.com/1.html 页面里,打开控制台,定位到加入购物车
位置,复制加入购物车

在gulimall-product
模块的src/main/resources/templates/item.html
文件里搜索加入购物车
,修改相关代码,如果该商品正在秒杀了就显示立即抢购
,如果该商品没有正在秒杀就显示加入购物车
<div class="box-btns-two" th:if="${item.seckillInfo!=null && #dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
<a href="#" id="secKillA" th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfo.promotionSessionId},code=${item.seckillInfo.randomCode}">
<!--立即预约-->
立即抢购
</a>
</div>
<div class="box-btns-two" th:if="${item.seckillInfo==null || #dates.createNow().getTime() < item.seckillInfo.startTime || #dates.createNow().getTime() > item.seckillInfo.endTime}">
<a href="#" id="addToCart" th:attr="skuId=${item.info.skuId}">
<!--立即预约-->
加入购物车
</a>
</div>

重启GulimallProductApplication
服务和GulimallSeckillApplication
服务,打开 http://item.gulimall.com/1.html 页面,此时显示的是加入购物车
,这是因为此时已经过了秒杀时间了

打开 http://localhost:8001/#/coupon-seckillsession 页面,在优惠营销
-> 每日秒杀
里,点击前面创建的2022-08-24 18:00:00
秒杀场次里的操作的修改
,在修改
对话框里修改为最近的正在秒杀的秒杀场次。(即现在的时间在修改的秒杀场次的开始时间和结束时间之内)

点击刚刚修改为2022-08-28 15:00:00
的秒杀场次里的操作的关联商品
,在关联秒杀商品
对话框里点击新增
,新增如下商品。

点击确定
后即可看到刚刚新关联的商品已经显示到关联秒杀商品
里了

3、值不能为空
重启GulimallProductApplication
服务和GulimallSeckillApplication
服务,在GulimallSeckillApplication
服务的控制台报了如下的错误。
2022-08-28 15:10:00.056 ERROR 3008 --- [ scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler : Unexpected error occurred in scheduled task.
java.lang.IllegalArgumentException: Values must not be 'null' or empty.
at org.springframework.util.Assert.notEmpty(Assert.java:464) ~[spring-core-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.data.redis.core.AbstractOperations.rawValues(AbstractOperations.java:147) ~[spring-data-redis-2.1.10.RELEASE.jar:2.1.10.RELEASE]
at org.springframework.data.redis.core.DefaultListOperations.leftPushAll(DefaultListOperations.java:122) ~[spring-data-redis-2.1.10.RELEASE.jar:2.1.10.RELEASE]
at com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl.lambda$saveSessionInfos$2(SeckillServiceImpl.java:139) ~[classes/:na]
at java.util.ArrayList.forEach(ArrayList.java:1259) ~[na:1.8.0_301]
at com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl.saveSessionInfos(SeckillServiceImpl.java:129) ~[classes/:na]
at com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl.uploadSeckillSkuLatest3Days(SeckillServiceImpl.java:56) ~[classes/:na]
at com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled.uploadSeckillSkuLatest3Days(SeckillSkuScheduled.java:46) ~[classes/:na]

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里,修改saveSessionInfos
方法
if (hasKey == null || !hasKey && CollectionUtils.isEmpty(session.getRelationSkus())) {

重启GulimallSeckillApplication
服务,打开 http://item.gulimall.com/1.html 页面,此时已经显示立即抢购
了,打开控制台,定位到立即抢购
这,可以看到该标签已经有skuid
(商品id)、sessionid
(秒杀场次id)、code
(令牌随机码)信息了
<a href="#" id="secKillA" skuid="1" sessionid="4" code="0b41ce397be0476c9cfd604924546a56">立即抢购</a>

在gulimall-product
模块的src/main/resources/templates/item.html
文件里的<script>
标签里添加如下代码
$("#secKillA").click(function () {
var killId = $(this).attr("sessionid") + "_" + $(this).attr("skuid")
var key = $(this).attr("code")
var num = $("#numInput").val()
location.href = "http://seckill.gulimall.com/kill?killId="+killId+"&key="+key+"&num="+num
return false;
})

在 http://item.gulimall.com/1.html 页面里,点击立即抢购
, 此时跳转到了 http://seckill.gulimall.com/kill?killId=4_1&key=0b41ce397be0476c9cfd604924546a56&num=1 页面,不过报了404
的错误,这是正常的,因为这个接口还没写。但是没有登录页跳转了,应该登陆后才能进行跳转。

4、登录后才能抢购
在gulimall-product
模块的src/main/resources/templates/item.html
文件里的<script>
标签里添加如下代码
$("#secKillA").click(function () {
var isLogin = [[${session.loginUser!=null}]]
if (isLogin){
var killId = $(this).attr("sessionid") + "_" + $(this).attr("skuid")
var key = $(this).attr("code")
var num = $("#numInput").val()
location.href = "http://seckill.gulimall.com/kill?killId="+killId+"&key="+key+"&num="+num
}else {
alert("秒杀前请先登录")
}
return false;
})

在 http://item.gulimall.com/1.html 页面里,点击立即抢购
,此时如果没有登录就会弹出秒杀前请先登录
的提示,登录后在 http://item.gulimall.com/1.html 页面里,再次点击立即抢购
,就会来到 http://seckill.gulimall.com/kill?killId=4_1&key=0b41ce397be0476c9cfd604924546a56&num=1 页面,不过报了404
的错误,这是正常的,因为这个接口还没写。

3、引入SpringSession
1、引入SpringSession
在gulimall-seckill
模块的pom.xml
文件里引入SpringSession
<!--引入SpringSession-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

2、添加配置
在gulimall-seckill
模块的src/main/resources/application.properties
文件里添加如下配置,指定使用redis
来存储SpringSession
的信息
spring.session.store-type=redis

复制gulimall-product
模块的com.atguigu.gulimall.product.config.GulimallSessionConfig
类,粘贴到gulimall-seckill
模块的com.atguigu.gulimall.seckill.config
包下。
点击查看GulimallSessionConfig
类完整代码

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.config.GulimallSessionConfig
配置类上添加如下注解
@EnableRedisHttpSession

复制gulimall-order
模块的com.atguigu.gulimall.order
里的interceptor
文件夹(里面有LoginUserInterceptor
类),粘贴到gulimall-seckill
模块的com.atguigu.gulimall.seckill
包下。

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor
类里,修改preHandle
方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
//只有`/kill`接口需要登录
boolean match = antPathMatcher.match("/kill", uri);
if (match){
Object attribute = request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
MemberEntityTo memberEntityTo= (MemberEntityTo) attribute;
loginUser.set(memberEntityTo);
return true;
}else {
request.getSession().setAttribute("msg","请先进行登录");
//没登陆就重定向到登录页面
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
};
return true;
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.config
包里新建SeckillWebConfig
类,用于添加刚刚的拦截器
package com.atguigu.gulimall.seckill.config;
import com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 无名氏
* @date 2022/8/28
* @Description:
*/
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginUserInterceptor()).addPathPatterns("/**");
}
}

在 http://item.gulimall.com/1.html 页面里,点击立即抢购
,此时如果没有登录就会弹出秒杀前请先登录
的提示,没有登录直接访问 http://seckill.gulimall.com/kill?killId=4_1&key=0b41ce397be0476c9cfd604924546a56&num=1 页面,此时会跳转到 http://auth.gulimall.com/login.html 登录页面

3、添加秒杀接口
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.controller.SeckillController
类里添加seckill
方法
/**
* 秒杀接口
* @return
*/
@GetMapping("/kill")
public R seckill(@RequestParam("killId") String killId,@RequestParam("key") String key,@RequestParam("num") Integer num){
//判断用户是否登录
//创建订单号
String orderSn = seckillService.kill(killId,key,num);
if (StringUtils.hasText(orderSn)){
return R.ok().setData(orderSn);
}
return R.error();
}

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.SeckillService
接口里添加kill
抽象方法
String kill(String killId, String key, Integer num);

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类里实现kill
方法
@Override
public String kill(String killId, String key, Integer num) {
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
//获取秒杀商品的详细信息
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
//4_1 (sessionId_skuId)
String s = hashOps.get(killId);
if (StringUtils.isEmpty(s)) {
return null;
}
SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);
//校验合法性
//1、校验开始时间和结束时间
Long startTime = seckillSkuRedisTo.getStartTime();
Long endTime = seckillSkuRedisTo.getEndTime();
long now = System.currentTimeMillis();
if (now < startTime || now > endTime) {
return null;
}
//2、校验随机码、商品id、购买数量(我感觉商品id没必要校验)
String randomCode = seckillSkuRedisTo.getRandomCode();
String skuCode = seckillSkuRedisTo.getPromotionSessionId() + "_" + seckillSkuRedisTo.getSkuId();
int limitNum = seckillSkuRedisTo.getSeckillLimit().intValue();
if (!randomCode.equals(key) || !skuCode.equals(killId) || num > limitNum) {
return null;
}
//3、校验该用户是否已经购买过,防止无限次购买(幂等性) userId_sessionId_skuId
String userKey = memberEntityTo.getId() + "_" + killId;
long ttl = endTime - startTime;
Boolean firstBuy = redisTemplate.opsForValue().setIfAbsent(userKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
//用户已经买过了
if (firstBuy!=null && !firstBuy) {
return null;
}
//占位成功,用户从未购买该商品
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//阻塞获取信号量,一直等待别人释放信号量(不能使用此方式获取信号量)
//semaphore.acquire();
//100毫秒内试一下,看是否能获取指定数量的信号量
try {
boolean b = semaphore.tryAcquire(num,100,TimeUnit.MILLISECONDS);
//秒杀成功,快速生成订单,给mq发送一个消息
if (b){
String orderSn = IdWorker.getTimeId();
return orderSn;
}
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
return null;
}

SETNX
只能一个抢到
4、在org.springframework.data.redis.core.ValueOperations
接口里有个Boolean setIfAbsent(K key, V value);
方法,只能有一个人能够抢到该key
,并将值设置进去。抢到了返回true
(redis
里返回1
),没抢到返回false
(redis
里返回0
)
/**
* Set {@code key} to hold the string {@code value} if {@code key} is absent.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="https://redis.io/commands/setnx">Redis Documentation: SETNX</a>
*/
@Nullable
Boolean setIfAbsent(K key, V value);

SETNX
Syntax
SETNX key value
Available since:
1.0.0
Time complexity:
O(1)
ACL categories:
@write
,@string
,@fast
Set key
to hold string value
if key
does not exist. In that case, it is equal to SET
. When key
already holds a value, no operation is performed. SETNX
is short for "SET if Not eXists".
Return
Integer reply, specifically:
1
if the key was set0
if the key was not set
Examples
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"

RabbitMQ
4、秒杀中使用1、秒杀架构图
秒杀业务的RabbitMQ
架构图如下图红色方框圈住的部分所示。

RabbitMQ
2、引入在gulimall-seckill
模块的pom.xml
文件里引入amqp
场景,使用RabbitMQ
<!--引入amqp场景,使用RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

在gulimall-seckill
模块的src/main/resources/application.properties
文件里添加如下依赖
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.virtual-host=/

复制gulimall-order
模块的com.atguigu.gulimall.order.config.MyRabbitConfig
类,粘贴到gulimall-seckill
模块的com.atguigu.gulimall.seckill.config
包里。

删掉该类的initRabbitTemplate
方法和setReturnCallback
方法、以及rabbitTemplate
字段。只保留messageConverter
方法。

如果gulimall-seckill
模块不监听队列,只向Rabblt MQ
发送消息,不需要在gulimall-seckill
模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication
主类上添加@EnableRabbit
注解,因此这里可以什么都不做

3、接收消息
复制gulimall-seckill
模块的com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo
类的部分字段(skuInfoVo
字段不复制,删除一些字段),添加orderSn
、memberId
字段

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类的kill
方法里的String orderSn = IdWorker.getTimeId();
下面添加如下代码,向RabbitMq
发送消息。
SecKillOrderTo secKillOrderTo = new SecKillOrderTo();
secKillOrderTo.setMemberId(memberEntityTo.getId());
secKillOrderTo.setOrderSn(orderSn);
secKillOrderTo.setSkuId(seckillSkuRedisTo.getSkuId());
secKillOrderTo.setNum(num);
secKillOrderTo.setPromotionSessionId(seckillSkuRedisTo.getPromotionSessionId());
secKillOrderTo.setSeckillPrice(seckillSkuRedisTo.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",secKillOrderTo);

在gulimall-order
模块的com.atguigu.gulimall.order.config.MyMQConfig
类里添加如下两个方法,创建order.seckill.order.queue
队列和绑定关系
@Bean
public Queue orderSeckillOrderQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete
return new Queue("order.seckill.order.queue",true,false,false);
}
@Bean
public Binding orderSeckillOrderBinding(){
//String destination, DestinationType destinationType, String exchange, String routingKey,Map<String, Object> arguments
return new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE,
"order-event-exchange", "order.seckill.order", null);
}

4、创建订单
在gulimall-order
模块的com.atguigu.gulimall.order.listener
包里新建OrderSeckillListener
类,用于监听秒杀的消息并创建订单。
package com.atguigu.gulimall.order.listener;
import com.atguigu.common.to.SecKillOrderTo;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.service.OrderService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author 无名氏
* @date 2022/8/28
* @Description:
*/
@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SecKillOrderTo secKillOrderTo, Channel channel, Message message) throws IOException{
try {
log.info("准备创建秒杀单的详细信息...");
orderService.createSeckillOrder(secKillOrderTo);
//手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
e.printStackTrace();
}
}
}

修改gulimall-order
模块的com.atguigu.gulimall.order.listener.OrderSeckillListener
接口的createSeckillOrder
抽象方法
void createSeckillOrder(SecKillOrderTo secKillOrderTo);

在gulimall-order
模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl
类里实现createSeckillOrder
方法
/**
* 简略的创建秒杀单
* @param secKillOrderTo 秒杀单数据
*/
@Override
public void createSeckillOrder(SecKillOrderTo secKillOrderTo) {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(secKillOrderTo.getOrderSn());
orderEntity.setMemberId(secKillOrderTo.getMemberId());
BigDecimal payAmount = secKillOrderTo.getSeckillPrice().multiply(new BigDecimal("" + secKillOrderTo.getNum()));
orderEntity.setPayAmount(payAmount);
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
this.save(orderEntity);
//保存订单项信息(秒杀的订单只有一个订单项)
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setOrderSn(orderEntity.getOrderSn());
orderItemEntity.setRealAmount(payAmount);
orderItemEntity.setSkuQuantity(secKillOrderTo.getNum());
//TODO 获取当前sku的详细信息
//productFeignService.getSpuInfoBySkuId(secKillOrderTo.getSkuId());
orderItemService.save(orderItemEntity);
}

5、测试
重启GulimallOrderApplication
订单服务和GulimallSeckillApplication
秒杀服务,可以看到创建秒杀活动后,点击立即抢购
已经可以获得订单号
了

5、完善功能
1、修改页面
将gulimall-cart
模块的src/main/resources/templates/success.html
文件复制到gulimall-seckill
模块的src/main/resources/templates
文件夹里

2、修改代码
将gulimall-seckill
模块的com.atguigu.gulimall.seckill.controller.SeckillController
类上的@RestController
注解修改为@Controller
,然后在getCurrentSeckillSkus
和getSkuSeckillInfo
方法上添加@ResponseBody
注解

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.controller.SeckillController
类里修改seckill
方法,将返回类型修改为String
,然后修改返回值为return "success";

thymeleaf
3、使用在gulimall-seckill
模块的pom.xml
文件里,引入thymeleaf
模板引擎
<!--模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在gulimall-seckill
模块的src/main/resources/application.properties
文件里添加如下配置,经用thymeleaf
缓存
spring.thymeleaf.cache=false

4、修改页面
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.controller.SeckillController
类里,修改seckill
方法
/**
* 秒杀接口
* @return
*/
@GetMapping("/kill")
public String seckill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num,
Model model){
//判断用户是否登录
//创建订单号
String orderSn = seckillService.kill(killId,key,num);
model.addAttribute("orderSn",orderSn);
return "success";
}

在gulimall-seckill
模块的src/main/resources/templates/success.html
文件里,将src="
全部替换为src="http://cart.gulimall.com/
,将href="
部分替换为href="http://cart.gulimall.com/
( href="/javascript:;"
和href="/#none"
这些不替换)
src="/
src="http://cart.gulimall.com/
href="/
href="http://cart.gulimall.com/

在http://search.gulimall.com/list.html
里随便挑选一个商品,把它加入购物车,然后打开控制台,定位带这个商品,然后复制m succeed-box

在gulimall-seckill
模块的src/main/resources/templates/success.html
文件里搜索m succeed-box
,替换里面的代码
<div class="main">
<div class="success-wrap">
<div class="w" id="result">
<div class="m succeed-box">
<div th:if="${orderSn!=null}" class="mc success-cont">
<h1>恭喜,秒杀成功。订单号:[[${orderSn}]]</h1>
<h2>正在准备订单数据,10秒后自动跳转到支付页面
<a style="color: red" th:href="${'http://order.gulimall.com/payOrder?orderSn='+orderSn}">去支付</a>
</h2>
</div>
<div th:if="${orderSn==null}">
<h1>手气不好,秒杀失败,请下次再来</h1>
</div>
</div>
</div>
</div>
</div>

5、测试
重启GulimallSeckillApplication
服务,刷新 http://item.gulimall.com/1.html 页面,可以看到由于秒杀时间过了,又变成加入购物车
了

秒杀时间又过了,可以在Windows
系统里面设置一下系统时间为秒杀范围内的时间即可以解决这个问题。

再次刷新 http://item.gulimall.com/1.html 页面,可以看到已经变成立即抢购
了

测试以下立即抢购
的完整流程,可以看到逻辑都没啥问题
其实上线秒杀后,应该把秒杀上架的库存提前在库存服务锁定住,等秒杀结束后如果redis
里还有库存,再解锁redis
里剩余数量的库存
上架秒杀商品的时候,每一个数据都有过期时间。 秒杀后续的流程,简化了收货地址等信息。

7.2、SpringCloud Alibaba-Sentinel
7.2.1、简介
1、熔断降级限流
什么是熔断
A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影响到 A。
什么是降级
整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。
熔断与降级异同
相同点:
1、为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我
2、用户最终都是体验到某个功能不可用不同点:
异同点
1、熔断是被调用方故障,触发的系统主动规则
2、降级是基于全局考虑,停止一些正常服务,释放资源
什么是限流
对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力
项目地址:https://github.com/alibaba/Sentinel
官方文档:https://github.com/alibaba/Sentinel/wiki/介绍

官方网址:https://sentinelguard.io/zh-cn/

2、Sentinel: 分布式系统的流量防卫兵

Sentinel 是什么?
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。
Sentinel 特征
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 的主要特性:

Sentinel 的开源生态:

Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
Sentinel 基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下, 可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规 则。所有规则可以动态实时调整。
Hystrix 与 Sentinel 比较

3、使用Sentinel文档
使用Sentinel在线文档:https://github.com/alibaba/Sentinel/wiki/如何使用
使用Sentinel离线文档: Sentinel使用

4、整合文档
整合SpringBoot在线文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
整合SpringBoot离线文档:整合SpringBoot

7.2.2、整合Sentinel
1、添加配置
1、引入依赖
在gulimall-common
模块的pom.xml
文件里添加sentinel
依赖
由于主流框架的默认适配,因此可以不配置受保护的资源,默认都是受保护的的
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2、下载仪表盘
点击1.Project
,让其展开,直接输入sentinel
即可搜索,可以看到com. alibaba.csp:sentinel-core
的版本为1.6.3

在 https://github.com/alibaba/Sentinel/releases?page=1 页面里找到与该版本对应的jar
包,然后下载

使用如下命令,启动sentinel
仪表盘
java -jar sentinel-dashboard-1.6.3.jar

3、添加配置
在gulimall-seckill
模块的src/main/resources/application.properties
文件里添加如下配置,指定sentinel
仪表盘的域名+端口
,然后随便配一个本服务与sentinel控制台建立连接所用端口(随便指定一个端口,只要没被占用就行,默认为8719)
#dashboard所用端口
spring.cloud.sentinel.transport.dashboard=localhost:8080
#本服务与sentinel控制台建立连接所用端口(随便指定一个端口,只要没被占用就行,默认为8719)
spring.cloud.sentinel.transport.port=8719

4、添加代码
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.controller.SeckillController
类的getCurrentSeckillSkus
方法的开头添加log.info("currentSeckillSkus正在执行...");
,用于测试是否访问了该方法
@ResponseBody
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
log.info("currentSeckillSkus正在执行...");
List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}

将gulimall-seckill
模块的com.atguigu.gulimall.seckill.scheduled.HelloSchedule
类的hello
方法注释掉,以取消这个定时任务

5、测试
启动GulimallGatewayApplication
服务和GulimallSeckillApplication
服务,打开 http://localhost:8080 页面,可以看到已经进入到了Sentinel
的登录页面了

用户名和密码都为sentinel
,输入用户名和密码后,进入到了 http://localhost:8080/#/dashboard/home 页面,可以看到什么都没有,因为这是懒加载方式

访问 http://seckill.gulimall.com/currentSeckillSkus 请求并不断刷新,刷新 http://localhost:8080/#/dashboard/home 页面,可以看待已经显示gulimall-seckill
模块了,点击gulimall-seckill
模块,再点击簇点电路
,即可看到/currentSeckillSkus
这个请求了。给该请求添加单机阈值
为1
,再次访问 http://seckill.gulimall.com/currentSeckillSkus 请求并不断刷新,可以看到只有一次请求成功了。(一个资源可以同时有多个限流规则,检查规则时会依次检查。)

6、流量控制
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 模式(1)或并发线程数模式(0) | QPS 模式 |
limitApp | 流控针对的调用来源 | default ,代表不区分调用来源 |
strategy | 调用关系限流策略:直接、链路、关联 | 根据资源本身(直接) |
controlBehavior | 流控效果(直接拒绝/WarmUp/匀速+排队等待),不支持按调用关系限流 | 直接拒绝 |
clusterMode | 是否集群限流 | 否 |
2、添加健康管理
1、数据的实时监控、审计、健康及指标信息
查看文档: Endpoint 支持

在使用 Endpoint 特性之前需要在 Maven 中添加 spring-boot-starter-actuator
依赖,并在配置中允许 Endpoints 的访问。
- Spring Boot 1.x 中添加配置
management.security.enabled=false
。暴露的 endpoint 路径为/sentinel
- Spring Boot 2.x 中添加配置
management.endpoints.web.exposure.include=*
。暴露的 endpoint 路径为/actuator/sentinel
Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。

2、启用SpringBoot健康管理
在gulimall-seckill
模块的pom.xml
文件里添加actuator
健康管理依赖
(如果在gulimall-order
设置会循环依赖,后面会说)
<!--健康管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

以jmx
方式暴露健康信息(即在JConsole
、jvisualvm
等扩展上可以看到健康信息,默认只使用这种方式暴露健康信息。还可以配置以web
方式暴露健康信息,这样不仅可以在扩展上看到这些信息,也可以通过浏览器看到这些健康信息)
JMX(英语:Java Management Extensions,即Java管理扩展)是Java平台上为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。
#健康管理暴露所有端点
management.endpoints.jmx.exposure.include='*'

3、修改限流后返回的数据
在gulimall-common
模块的com.atguigu.common.exception.BizCodeException
枚举类里添加如下枚举,用于返回限流数据
/**
* 同一个接口QPS每秒发送的请求数过多
*/
TOO_MANY_REQUEST(10003,"请求流量过大异常"),

在gulimall-seckill
模块的com.atguigu.gulimall.seckill.config
包里添加SeckillSentinelConfig
配置类,用于定制Sentinel
限流后返回的数据
package com.atguigu.gulimall.seckill.config;
import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler;
import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.exception.BizCodeException;
import com.atguigu.common.utils.R;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* @author 无名氏
* @date 2022/8/30
* @Description:
*/
@Configuration
public class SeckillSentinelConfig {
public SeckillSentinelConfig(){
WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler() {
@Override
public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {
R error = R.error(BizCodeException.TOO_MANY_REQUEST);
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.getWriter().write(JSON.toJSONString(error));
}
});
}
}

访问 http://seckill.gulimall.com/currentSeckillSkus 请求,然后在 http://localhost:8080/#/dashboard/home 里点击gulimall-seckill
里的流控规则
,点击新增流控规则
,在新增流控规则
里资源名
输入/currentSeckillSkus
,单机阈值
输入1
,点新建
,多刷新 http://seckill.gulimall.com/currentSeckillSkus 请求,已经返回了自定义的流控后请求失败的结果了, 返回Sentinel
仪表盘可以看到实时监控等信息

3、其他模块添加健康管理
1、订单模块添加健康管理
在gulimall-order
模块的pom.xml
文件里添加actuator
健康管理依赖
<!--健康管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在gulimall-order
模块的src/main/resources/application.properties
配置文件里,添加Sentinel dashboard
配置和健康管理
配置
#dashboard所用端口
spring.cloud.sentinel.transport.dashboard=localhost:8080
#本服务与sentinel控制台建立连接所用端口(随便指定一个端口,只要没被占用就行,默认为8719)
spring.cloud.sentinel.transport.port=8719
#健康管理暴露所有端点
management.endpoints.jmx.exposure.include='*'

2、循环依赖
重启GulimallOrderApplication
模块,报了The dependencies of some of the beans in the application context form a cycle
(应用上下文中的一些bean的依赖关系形成了一个循环
),也就是循环依赖。
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-08-30 20:53:52.664 ERROR 14640 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
servletEndpointRegistrar defined in class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration.class]
↓
healthEndpoint defined in class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]
↓
healthIndicatorRegistry defined in class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.class]
↓
org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthIndicatorAutoConfiguration
┌─────┐
| rabbitTemplate defined in class path resource [org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration$RabbitTemplateConfiguration.class]
↑ ↓
| myRabbitConfig (field org.springframework.amqp.rabbit.core.RabbitTemplate com.atguigu.gulimall.order.config.MyRabbitConfig.rabbitTemplate)
└─────┘

上面可能不够明显,让其本来是同一行的信息都一行显示(一行显示不下,不显示到下一行)后,可以看到如下依赖关系,很明显rabbitTemplate
和myRabbitConfig
形成了一个循环

这是因为我们在gulimall-order
模块的com.atguigu.gulimall.order.config.MyRabbitConfig
类里,注入了RabbitTemplate
,还定制了MessageConverter

而在org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
自动配置类里,向容器中放RabbitTemplate
的方法中使用了MessageConverter
,这样就导致,我们注入RabbitTemplate
时向容器中找该类,然后调用该rabbitTemplate(ConnectionFactory connectionFactory)
方法向容器中存放RabbitTemplate
,而在该类里向容器中要MessageConverter
,而MessageConverter
被我们定制化为了Jackson2JsonMessageConverter
,于是我们写的MyRabbitConfig
类和别框架写的RabbitAutoConfiguration
类就产生了循环依赖。

在gulimall-order
模块的com.atguigu.gulimall.order.config.MyRabbitConfig
类里添加rabbitTemplate(ConnectionFactory connectionFactory)
方法,我们自己向容器中存放RabbitTemplate
,自己设置我们定制的MessageConverter
删除RabbitTemplate rabbitTemplate;
字段上的@Autowired
注解,和initRabbitTemplate
方法上的@PostConstruct
注解
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}

3、其他模块添加配置
给其他模块(gulimall-auth-server
、gulimall-cart
、gulimall-coupon
、gulimall-gateway
、gulimall-member
、gulimall-product
、gulimall-search
、gulimall-third-party
、gulimall-ware
)添加配置

在其他模块的pom.xml
文件里添加actuator
健康管理依赖
<!--健康管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在其他模块的src/main/resources/application.properties
配置文件里,添加Sentinel dashboard
配置和健康管理
配置
#dashboard所用端口
spring.cloud.sentinel.transport.dashboard=localhost:8080
#本服务与sentinel控制台建立连接所用端口(随便指定一个端口,只要没被占用就行,默认为8719)
spring.cloud.sentinel.transport.port=8719
#健康管理暴露所有端点
management.endpoints.jmx.exposure.include='*'

走一遍下单流程,在Sentinel
的控制台里,可以看到这些服务都显示出来了

重启GulimallThirdPartyApplication
服务,报了如下异常
2022-08-31 17:22:01.689 ERROR 13128 --- [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplication]; nested exception is java.lang.IllegalStateException: Error processing condition on org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration

这是由于版本不匹配导致的,将gulimall-third-party
模块的pom.xml
文件里的project -> parent -> version
父依赖的版本修改为2.1.8.RELEASE
,将project -> properties -> spring-cloud.version
微服务的版本修改为Greenwich.SR3
<version>2.1.8.RELEASE</version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>

打开gulimall-third-party
模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests
测试类,可以看到该类报错了,这是因为2.2.1.RELEASE
版本的Spring Boot
使用的是junit5
,而2.1.8.RELEASE
版本使用的是junit4

将该测试类修改为junit4
即可
点击查看GulimallThirdPartyApplicationTests
类完整代码

4、流控规则
1、集群阈值模式--单机均摊
单机均摊:该模式下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);
设置的阈值为10
,则每台机器的阈值都为10
全局模式:配置的阈值等同于整个集群的总阈值。
每台机器分得的QPS/线程数
不一样,但这些机器分得的总数为配置的阈值。
参考链接:https://zhuanlan.zhihu.com/p/364009386

2、流控模式--直连
流量控制 :只对该服务进行流控

3、流控模式--关联
假设a
是读数据,b
是写数据。假设a
和b
的并发都很大。此时设置a
的阈值为100
。并设置a
关联b
,则b
的并发大了就对a
限流,b
并发不大就不对a
限流

4、流控模式--链路
c1
设置入口资源
为a
,则只有从a
经过一系列链路到达c
,对c
的流控才生效,从b2
(或其他链路)访问到c
则不生效
a
/ \
b1 b2
/ \
c c

5、流控效果--快速失败
达到阈值后,直接失败。假设设置阈值为500
,有700
个请求,则多出的200
个请求直接丢弃。

6、流控效果--Warm Up
设置阈值是500
,预热时间是10s
,则在10s
内缓慢增加流量,直到10s
后才达到峰值500

7、流控效果--排队等待
达到阈值后,排队等待。假设设置阈值为500
,有700
个请求,则多出的200
个请求排队进行等待。同时可以设置超时时间,到达超时时间的请求还是丢弃。

8、参考文档
在线文档: https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
离线文档: 流量控制概述

7.2.3、Sentinel开启feign链路追踪
1、添加Feign 支持
1、添加Feign 支持参考文档
整个调用链只发现了这个请求,并没有发现通过feign
远程调用的这些链路。而我们更要做的是对被调用方的熔断、保护、降级。
参考文档: Feign 支持

2、Sentinel开启feign链路追踪
秒杀时间又过了,可以在Windows
系统里面设置一下系统时间为秒杀范围内的时间即可以解决这个问题。

打开 http://item.gulimall.com/1.html 页面,可以看到已经显示秒杀价
和立即抢购
了

给调用方的配置文件里添加feign.sentinel.enabled=true
配置,使Sentinel开启feign链路追踪
这里的调用方是gulimall-product
模块,该模块调用其他模块,因此在gulimall-product
模块的src/main/resources/application.properties
配置文件里添加如下配置,指定Sentinel开启feign链路追踪
#Sentinel开启feign链路追踪
feign.sentinel.enabled=true

3、测试
再走一遍有秒杀信息的商品的下单流程,然后在Sentinel
的控制台里点击gulimall-product
,可以看到已经有远程调用的GET:http://gulimall-seckill/sku/seckill/{skuId}
这个请求了
GET:http://gulimall-seckill/sku/seckill/{skuId}

2、消费方定制远程调用失败返回结果
1、停掉生产方
由于是gulimall-product
模块调用gulimall-seckill
模块,因此我们可以停掉GulimallSeckillApplication
服务,模拟生产方宕机,不能提供服务。

2、查看异常
此时刷新 http://item.gulimall.com/1.html 页面,可以看到报了不能负载均衡到gulimall-seckill
服务,这样系统的服务自治能力就很差,如果秒杀服务宕机此时就影响到了商品服务;我们应该做的是秒杀服务宕机后不影响商品服务,商品服务继续提供服务,商品服务此时不能调用秒杀服务,应该不显示商品的秒杀信息。

3、消费方自定义远程调用失败返回结果
可以对消费方的远程调用方法做一些限制,如果远程调用失败就返回我们自定义的返回结果。
在gulimall-product
模块的com.atguigu.gulimall.product.feign
包里新建fallback
文件夹,在fallback
文件夹里新建SeckillFeignServiceFallBack
类,用于设置远程调用失败后返回的默认结果。
package com.atguigu.gulimall.product.feign.fallback;
import com.atguigu.common.exception.BizCodeException;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.product.feign.SeckillFeignService;
import org.springframework.stereotype.Component;
/**
* @author 无名氏
* @date 2022/8/28
* @Description:
*/
@Component
public class SeckillFeignServiceFallBack implements SeckillFeignService {
@Override
public R getSkuSeckillInfo(Long skuId) {
return R.error(BizCodeException.TOO_MANY_REQUEST);
}
}

然后在gulimall-product
模块的com.atguigu.gulimall.product.feign.SeckillFeignService
接口上,将@FeignClient("gulimall-seckill")
修改为@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
,指明远程调用失败后使用SeckillFeignServiceFallBack
来处理
@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)

在gulimall-product
模块的com.atguigu.gulimall.product.feign.fallback.SeckillFeignServiceFallBack
类里添加日志信息,用于查看是否调用了该方法。
package com.atguigu.gulimall.product.feign.fallback;
import com.atguigu.common.exception.BizCodeException;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.product.feign.SeckillFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author 无名氏
* @date 2022/8/28
* @Description:
*/
@Slf4j
@Component
public class SeckillFeignServiceFallBack implements SeckillFeignService {
@Override
public R getSkuSeckillInfo(Long skuId) {
log.info("熔断方法被调用...getSkuSeckillInfo");
return R.error(BizCodeException.TOO_MANY_REQUEST);
}
}

4、测试
在gulimall-product
模块的src/main/resources/application.yml
配置文件里,将com.atguigu.gulimall
包下的error
级别修改为info
级别
logging:
level:
com.atguigu.gulimall: info

重启GulimallProductApplication
服务

刷新 http://item.gulimall.com/1.html 页面,这次就不报错了,并且没有显示秒杀信息

此时GulimallProductApplication
服务的控制台已经显示熔断方法被调用...getSkuSeckillInfo
,表明刚刚写的SeckillFeignServiceFallBack
远程调用失败处理类已经生效了
2022-08-28 15:33:44.029 INFO 14668 --- [ool-2-thread-15] c.a.g.p.f.f.SeckillFeignServiceFallBack : 熔断方法被调用...getSkuSeckillInfo

5、设置远程调用熔断等待时间
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.controller.SeckillController
类里,修改getSkuSeckillInfo
方法,让其睡眠300
毫秒,模拟生产方逻辑复杂,执行时间长。
/**
* 查询指定sku的一个秒杀信息
* @param skuId
* @return
*/
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(300);
SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}

启动GulimallSeckillApplication
服务,没设置降级前,刷新页面,然后疯狂按f5
刷新,可用看到怎么都不会调用降级方法。
设置RT
(响应时间)为1ms
,时间窗口为10s
后,当在10s
内的请求响应时间超过1ms
的次数大于最小请求数目
(默认为5
)后,在这个10s
内的后面的请求都会被熔断。当超过这个10s
的时间段后又会再次恢复访问,又重新进行计数。

熔断降级策略
6、Sentinel 提供以下几种熔断策略:
- 慢调用比例 (
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 - 异常比例 (
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。 - 异常数 (
ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
熔断降级规则说明
熔断降级规则(DegradeRule)包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |

7、总结
使用Sentinel来保护feign远程调用:熔断; 1)、调用方的熔断保护: feign.sentinel.enabled=true 2)、调用方手动指定远程服务的降级策略。远程服务被降级处理。触发我们的熔断回调方法 3)、超大流量的时候,必须牺牲一 些远程服务。在服务的提供方(远程服务)指定降级策略; 提供方是在运行。但是不运行自己的业务逻辑,返回的是默认的降级数据(限流的数据),即SeckillSentinelConfig
类配置的返回的降级后的数据(发送方是熔断,远程服务是降级)
@Configuration
public class SeckillSentinelConfig {
public SeckillSentinelConfig(){
WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler() {
@Override
public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {
R error = R.error(BizCodeException.TOO_MANY_REQUEST);
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.getWriter().write(JSON.toJSONString(error));
}
});
}
}

为每个服务(gulimall-auth-server
、gulimall-cart
、gulimall-coupon
、gulimall-member
、gulimall-order
、gulimall-search
、gulimall-seckill
、gulimall-third-party
、gulimall-ware
)都配置Sentinel开启feign链路追踪
#sentinel开启feign链路追踪
feign.sentinel.enabled=true

为每个服务(gulimall-auth-server
、gulimall-cart
、gulimall-coupon
、gulimall-member
、gulimall-order
、gulimall-product
、gulimall-search
、gulimall-third-party
、gulimall-ware
)都加上SeckillSentinelConfig
配置类

自定义受保护资源
7.2.4、1、方法内受保护
使用try(Entry entry = SphU.entry("seckillSkus"))
尝试调用SphU.entry("seckillSkus")
获取seckillSkus
,如果获取不到就捕获异常。(用try-with-resources代替try-catch-finally,即根据资源,而不是根据try里的代码)
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
//try(需要释放的资源){} ==> 用try-with-resources代替try-catch-finally
try(Entry entry = SphU.entry("seckillSkus")) {
long time = System.currentTimeMillis();
Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
for (String key : keys) {
String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] split = replace.split("_");
Long start = Long.parseLong(split[0]);
Long end = Long.parseLong(split[1]);
if (time >= start && time <= end) {
//这里的 -1相当于length-1,即最后一个元素。取出的结果为[0,length-1] 包含开始和最后的元素,即所有元素
List<String> range = redisTemplate.opsForList().range(key, 0, -1);
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
if (range != null) {
List<String> list = operations.multiGet(range);
if (!CollectionUtils.isEmpty(list)) {
List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(item.toString(), SeckillSkuRedisTo.class);
//秒杀开始后可以查看到随机码
//seckillSkuRedisTo.setRandomCode(null);
return seckillSkuRedisTo;
}).collect(Collectors.toList());
return collect;
}
}
//只要找到了在当前时间范围内的秒杀,不管range是否为null都退出循环(当然如果range不为null,直接就return了)
break;
}
}
}catch (BlockException e){
log.error("资源被限流:{}",e.getMessage());
}
return null;
}

在Sentinel 控制台
的gulimall-seckill
模块里的 /currentSeckillSkus
里已经显示seckillSkus
了,可以对seckillSkus
进行流控,指定单机阈值等信息。

在没对seckillSkus
流控前,不断刷新 http://seckill.gulimall.com/currentSeckillSkus 都可以访问,对seckillSkus
流控后,再次频繁访问 http://seckill.gulimall.com/currentSeckillSkus 页面,就开始报错了。

2、对方法进行保护
1、对方法进行保护
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类的getCurrentSeckillSkus
方法上添加如下注解,即可使用getCurrentSeckillSkusResource
资源对该方法进行保护。
@SentinelResource("getCurrentSeckillSkusResource")

刷新 http://seckill.gulimall.com/currentSeckillSkus 页面,在Sentinel 控制台
的gulimall-seckill
模块已经显示getCurrentSeckillSkusResource
和对应方法里的seckillSkus
了。

给getCurrentSeckillSkusResource
和seckillSkus
都添加流控,可以看到可以正常生效,但是getCurrentSeckillSkusResource
的流控返回的不够友好,我们可以自定义流控后返回的页面。

2、自定义流控响应
在gulimall-seckill
模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl
类的getCurrentSeckillSkus
方法上修改@SentinelResource
注解,添加,blockHandler = "blockHandler"
,指定流控后的处理。在该类添加同名、同返回类型的方法,并多添加一个BlockException e
参数,该参数用于获取出错的信息,然后自定义流控后的处理。
/**
* 5、自定义受保护的资源
* 1)、代码
* try(Entry entry = SphU. entry( "seckillSkus")){
* //业务逻辑
* }catch(Execption e){}
* 2)、基于注解。
* @SentineLResource(vaLue = "getCurrentSeckillSkusResource", blockHandler = "blockHandler")
* 无论是1, 2方式一定要配置被限流以后的默认返回.
* url请求可以设置统一返回WebCallbackManager
*/
public List<SeckillSkuRedisTo> getCurrentSeckillSkus(BlockException e){
log.error("getCurrentSeckillSkus被限流了");
return null;
}
@SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")

网关限流
3、1、添加依赖
整合网关需要引入spring-cloud-starter-alibaba-sentinel
和spring-cloud-alibaba-sentinel-gateway
,由于common
已经引入spring-cloud-starter-alibaba-sentinel
了,因此只需引入spring-cloud-alibaba-sentinel-gateway
就行了
在gulimall-gateway
网关模块的pom.xml
文件里添加spring-cloud-alibaba-sentinel-gateway
依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

2、基于QPS限流
关闭sentinel-dashboard-1.6.3
版本的Sentinel仪表板
,启动sentinel-dashboard-1.7.1.jar
版本的sentinel仪表板
,首先刷新 http://seckill.gulimall.com/currentSeckillSkus 页面,让懒加载的Sentinel
加载到该请求,然后复制在网关模块里配置的该请求对应的模块的id
,然后打开Sentinel仪表板
点击新增网关流控规则
,在API名称
输入在网关模块里配置的gulimall-seckill
模块的id
,即gulimall_seckill_route
,然后在QPS 阈值
里输入1
,然后点击新增
接口新增流控规则,频繁刷新 http://seckill.gulimall.com/currentSeckillSkus 页面,可以看到已经返回自定义的结果了。

3、基于请求头限流
还可以争对各种属性进行限流,比如Client IP
、Remote Host
、Header
、URL参数
、Cookie
。

4、分组限流
可以在API 管理
里新增自定义API
,然后对API 分组
进行流控。
(如果没成功,需要修改pom
文件里sentinel
的版本号)

5、自定义限流返回数据
在gulimall-gateway
模块的src/main/resources/application.properties
配置文件里添加如下配置,重启sentinel仪表板
和所有微服务
,定制限流后返回的数据并未生效
#定制限流后返回的数据(亲测不生效)
spring.cloud.sentinel.scg.fallback.content-type=application/json
spring.cloud.sentinel.scg.fallback.response-status=400
spring.cloud.sentinel.scg.fallback.response-body=请求过多,请稍后重试

在gulimall-gateway
模块的com.atguigu.gulimall.gateway.config
包里添加SentinelGatewayConfig
类,用于自定义限流后返回的数据
package com.atguigu.gulimall.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.exception.BizCodeException;
import com.atguigu.common.utils.R;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author 无名氏
* @date 2022/8/28
* @Description:
*/
@Configuration
public class SentinelGatewayConfig {
public SentinelGatewayConfig(){
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
//网关限流后,就会调用该回调
//Mono(单个对象) Flux(集合) 响应式编程
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
final R error = R.error(BizCodeException.TOO_MANY_REQUEST);
final String errorJson = JSON.toJSONString(error);
final Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errorJson), String.class);
return body;
}
});
}
}

不断请求 http://seckill.gulimall.com/currentSeckillSkus ,可以看到已经返回自定义的数据了

7.3、Sleuth+Zipkin 服务链路追踪
7.3.1、概述
1、为什么用
微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与, 参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。 链路追踪组件有 Google 的 Dapper,Twitter 的 Zipkin,以及阿里的 Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件。
2、基本术语
Span(跨度):基本工作单元,发送一个远程调度任务 就会产生一个 Span,Span 是一个 64 位 ID 唯一标识的,Trace 是用另一个 64 位 ID 唯一标识的,Span 还有其他数据信息,比如摘要、时间戳事件、Span 的 ID、以及进度 ID。
Trace(跟踪):一系列 Span 组成的一个树状结构。请求一个微服务系统的 API 接口, 这个 API 接口,需要调用多个微服务,调用每个微服务都会产生一个新的 Span,所有由这个请求产生的 Span 组成了这个 Trace。
Annotation(标注):用来及时记录一个事件的,一些核心注解用来定义一个请求的开 始和结束 。这些注解包括以下:
cs - Client Sent -客户端发送一个请求,这个注解描述了这个 Span 的开始
sr - Server Received -服务端获得请求并准备开始处理它,如果将其 sr 减去 cs 时间戳便可得到网络传输的时间。
ss - Server Sent (服务端发送响应)–该注解表明请求处理的完成(当请求返回客户端),如果 ss 的时间戳减去 sr 时间戳,就可以得到服务器请求的时间。
cr - Client Received(客户端接收响应)此时 Span 的结束,如果 cr 的时间戳减去cs 时间戳便可以得到整个请求所消耗的时间。
(假设A->B->C,此时就会有3个Span。而Trace就只有一个,用于追踪整个链路。Annotation(标注)就相当于给Span打一个标签)
如果服务调用顺序如下

那么用以上概念完整的表示出来如下:

Span 之间的父子关系如下:

7.3.2、整合 Sleuth
1、添加依赖
在gulimall-common
模块的pom.xml
文件里的project -> dependencyManagement -> dependencies
里添加如下依赖,用于对spring-cloud
进行版本约束。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
在gulimall-common
模块的pom.xml
文件里的project -> dependencies
里添加spring-cloud-starter-sleuth
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2、添加配置
在gulimall-product
模块的src/main/resources/application.properties
配置文件里添加如下配置,指定logging.level.org.springframework.cloud.openfeign
包和logging.level.org.springframework.cloud.sleuth
包下的日志级别为debug
logging.level.org.springframework.cloud.openfeign: debug
logging.level.org.springframework.cloud.sleuth: debug

在gulimall-seckill
模块的src/main/resources/application.properties
配置文件里添加同样的配置

重启各个模块,刷新 https://item.gulimall.com/1.html 页面,查看GulimallProductApplication
服务的控制台,打印了如下日志
DEBUG [gulimall-product,4333ca18c611f553,cc74b65e5467a683,false]
gulimall-product:服务名
4333ca18c611f553:是 TranceId一条链路中,只有一个TranceId
cc74b65e5467a683:是 spanId,链路中的基本工作单元 id
false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察

7.3.3、整合 zipkin 可视化观察
1、概述
通过 Sleuth 产生的调用链监控信息,可以得知微服务之间的调用链路,但监控信息只输出到控制台不方便查看。我们需要一个图形化的工具-zipkin。Zipkin 是 Twitter 开源的分布式跟踪系统,主要用来收集系统的时序数据,从而追踪系统的调用问题。zipkin 官网地址如下: https://zipkin.io/

2、docker 安装 zipkin 服务器
使用如下命令,安装 zipkin 服务器
docker run -d -p 9411:9411 openzipkin/zipkin

3、添加依赖
在gulimall-common
模块的pom.xml
文件里添加zipkin
的依赖,并删除sleuth
的依赖(zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

4、添加配置
文档里提示我们需要进行如下配置
spring:
application:
name: user-service
zipkin:
# zipkin 服务器的地址
base-url: http://192.168.56.10:9411/
#关闭服务发现,否则Spring Cloud 会把 zipkin 的 url 当做服务名称
discoveryClientEnabled: false
sender:
#设置使用 http 的方式传输数据
type: web
sleuth:
sampler:
#设置抽样采集率为100%,默认为0.1,即10%
probability: 1
因此,我们可以给所有微服务的src/main/resources/application.properties
文件里都加上如下配置
# zipkin 服务器的地址
spring.zipkin.base-url=http://192.168.56.10:9411/
#关闭服务发现,否则Spring Cloud 会把 zipkin 的 url 当做服务名称
spring.zipkin.discovery-client-enabled=false
##设置使用 http 的方式传输数据 (可选值:rabbit、kafka、web)
spring.zipkin.sender.type=web
#设置抽样采集率为100%,默认为0.1,即10%
spring.sleuth.sampler.probability=1

走一遍下单的流程,访问: http://192.168.56.10:9411/ 页面。可以看到这些链路都显示出来了

还能显示这些链路的流向等信息

5、Zipkin 数据持久化
Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据持久化,自然就是得将数据存储至数据库。好在 Zipkin 支持将数据存储至:
内存(默认)
MySQL
Elasticsearch
Cassandra
Zipkin 数据持久化相关的官方文档地址如下: https://github.com/openzipkin/zipkin#storage-component
Zipkin 支持的这几种存储方式中,内存显然是不适用于生产的,这一点开始也说了。而使用MySQL 的话,当数据量大时,查询较为缓慢,也不建议使用。Twitter 官方使用的是 Cassandra 作为 Zipkin 的存储数据库,但国内大规模用 Cassandra 的公司较少,而且 Cassandra 相关文档也不多。
综上,故采用 Elasticsearch 是个比较好的选择,关于使用 Elasticsearch 作为 Zipkin 的存储数据库的官方文档如下:
elasticsearch-storage、zipkin-storage/elasticsearch
通过 docker 的方式将Zipkin 数据持久化到elasticsearch
的命令如下
docker run
--env STORAGE_TYPE=elasticsearch
--env ES_HOSTS=192.168.56.10:9200
openzipkin/zipkin-dependencies

使用 es 时Zipkin Dependencies
支持的环境变量

高级篇总结
高并发有三宝:缓存
、异步
、队排好
