跳至主要內容

6.3、支付商品

apzs...大约 125 分钟

6.3、支付商品

6.3.1、支付

1、相关概念

1、文档

支付宝电脑网站解锁

文档链接:https://opendocs.alipay.com/open/270/105898?ref=api

GIF 2022-8-20 15-34-17
GIF 2022-8-20 15-34-17

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

GIF 2022-8-20 15-36-49
GIF 2022-8-20 15-36-49

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

GIF 2022-8-20 15-42-24
GIF 2022-8-20 15-42-24
2、对称秘钥

加密-对称加密

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

image-20220820160519647
image-20220820160519647

DES3DESTripleDES)、AESRC2RC4RC5Blowfish等,加密解密使用同一把钥匙

3、非对称秘钥

加密-非对称加密

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

image-20220820160600084
image-20220820160600084

RSAElgamal等,加密解密使用不同钥匙

2、支付宝支付

1、电脑网站支付产品介绍

电脑网站支付产品介绍: https://opendocs.alipay.com/open/270

image-20220820162411417
image-20220820162411417
2、SDK & Demo

下载demo: https://opendocs.alipay.com/open/270/106291

image-20220820162414669
image-20220820162414669
3、配置使用沙箱进行测试

1、使用 RSA 工具生成签名 2、下载沙箱版钱包 3、运行官方 demo 进行测试

4、公钥 & 私钥

什么是公钥、私钥、加密、签名和验签?

1、公钥私钥 公钥和私钥是一个相对概念 它们的公私性是相对于生成者来说的。 一对密钥生成后,保存在生成者手里的就是私钥, 生成者发布出去大家用的就是公钥

5、加密 & 数字签名

加密和数字签名概念 加密:

  • 我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解密的技术。
  • 公钥和私钥都可以用来加密,也都可以用来解密。
  • 但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。
  • 加密的目的是: 为了确保数据传输过程中的不可读性,就是不想让别人看到。

签名:

  • 给我们将要发送的数据,做上一个唯一签名(类似于指纹)
  • 用来互相验证接收方和发送方的身份;
  • 在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以用来达到数据的明文传输。

验签

  • 支付宝为了验证请求的数据是否商户本人发的
  • 商户为了验证响应的数据是否支付宝发的

签名是先计算哈希值然后用私钥加密,私钥对外不公开所以不能自己生成签名(下面这个图应该有问题,商户给支付宝发消息应该使用支付宝的公钥进行加密,支付宝收到消息后使用支付宝自己的私钥进行解密;支付宝业务处理完成后,使用商户的公钥对消息进行加密,商户收到消息后使用商户自己的私钥进行解密)

image-20220820163916589
image-20220820163916589

3、使用支付宝提供的demo

1、开启沙箱

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

GIF 2022-8-20 16-57-39
GIF 2022-8-20 16-57-39

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

image-20220820170302186
image-20220820170302186
2、调试demo

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

image-20220820170911893
image-20220820170911893

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

image-20220820170940656
image-20220820170940656

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

image-20220820171012120
image-20220820171012120

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

image-20220820171050458
image-20220820171050458

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

image-20220820171045983
image-20220820171045983

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

image-20220820171327453
image-20220820171327453

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

image-20220820171323382
image-20220820171323382
3、测试

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

GIF 2022-8-20 19-03-14
GIF 2022-8-20 19-03-14
4、再次测试

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

image-20220820190610688
image-20220820190610688

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

GIF 2022-8-20 19-08-39
GIF 2022-8-20 19-08-39

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>
image-20220820194410081
image-20220820194410081
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; // 商品描述 可空
}
image-20220820194630307
image-20220820194630307

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

image-20220820194859881
image-20220820194859881

gulimall-order模块的com.atguigu.gulimall.order.config.AlipayTemplate类里,修改app_idmerchant_private_keyalipay_public_keygatewayUrl这些字段的值

image-20220820205459867
image-20220820205459867

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;
}
image-20220820204732495
image-20220820204732495

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

点击查看完整代码

image-20220820205600161
image-20220820205600161
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]
image-20220820201108997
image-20220820201108997

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

spring:
  application:
    name: gulimall-product
image-20220820201455316
image-20220820201455316

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

image-20220820202047005
image-20220820202047005

重启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
image-20220820202557738
image-20220820202557738
4、不显示真是应付金额

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

image-20220820203125417
image-20220820203125417

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>
image-20220820203242820
image-20220820203242820

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

image-20220820203427654
image-20220820203427654

5、添加支付逻辑

1、修改页面

在 http://order.gulimall.com/submitOrder 页面里,打开控制台,定位到支付宝图标,复制支付宝

image-20220820203529433
image-20220820203529433

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>
image-20220820203622191
image-20220820203622191
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;
    }
}
image-20220820210626254
image-20220820210626254

gulimall-order模块的com.atguigu.gulimall.order.service.OrderService接口里太内疚getOrderPay抽象方法

PayVo getOrderPay(String orderSn);
image-20220820210704284
image-20220820210704284

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;
}
image-20220820213319648
image-20220820213319648
3、测试

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

GIF 2022-8-20 21-35-22
GIF 2022-8-20 21-35-22

支付宝返回了一个<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&timestamp=2022-08-20+21%3A35%3A11&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
<input type="hidden" name="biz_content" value="{&quot;out_trade_no&quot;:&quot;202208202135096131560983780531920898&quot;,&quot;total_amount&quot;:&quot;47401.00&quot;,&quot;subject&quot;:&quot;华为 HUAWEI Mate30Pro 罗兰紫 8GB+128GB 麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄 4G全网通手机&quot;,&quot;body&quot;:&quot;颜色:罗兰紫;版本:8GB+128GB&quot;,&quot;product_code&quot;:&quot;FAST_INSTANT_TRADE_PAY&quot;}">
<input type="submit" value="立即支付" style="display:none" >
</form>
<script>document.forms[0].submit();</script>
image-20220820213731759
image-20220820213731759

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)

image-20220820214600161
image-20220820214600161

修改gulimall-order模块的com.atguigu.gulimall.order.config.AlipayTemplate类里的returnUrl字段

private String returnUrl="http://member.gulimall.com/memberOrder.html";
image-20220821085200342
image-20220821085200342
4、添加页面

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

image-20220821085439712
image-20220821085439712

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

GIF 2022-8-21 8-59-42
GIF 2022-8-21 8-59-42

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

点击查看完整orderList页面

href="
href="/static/member/

src="
src="/static/member/
image-20220821090509845
image-20220821090509845
5、添加配置

gulimall-member模块的pom.xml文件里添加thymeleaf依赖

<!--模板引擎:thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
image-20220821085558121
image-20220821085558121

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

spring.thymeleaf.cache=false
image-20220821085651323
image-20220821085651323

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

image-20220821090621314
image-20220821090621314

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

点击查看LoginUserInterceptor类完整代码

image-20220821091053614
image-20220821091053614

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("/**");
    }
}
image-20220821092255087
image-20220821092255087

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

点击查看GulimallSessionConfig类完整代码

image-20220821094007003
image-20220821094007003

gulimall-gateway模块的src/main/resources/application.yml配置文件里添加如下配置

spring:
  cloud:
    gateway:
      routes:
        - id: gulimall_member_route
          uri: lb://gulimall-member
          predicates:
            - Host=member.gulimall.com
image-20220821091646118
image-20220821091646118

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
image-20220821091850027
image-20220821091850027
6、修改页面和配置

在 http://gulimall.com/ 页面里,打开控制台,定位到我的订单图标,复制我的订单

image-20220821092354913
image-20220821092354913

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>
image-20220821092529782
image-20220821092529782

gulimall-member模块的pom.xml文件里引入redisSpringSession

<!--引入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>
image-20220821093615823
image-20220821093615823

gulimall-member模块的src/main/resources/application.properties文件里添加redis配置

spring.redis.host=192.168.56.10
spring.session.store-type=redis
image-20220821093326466
image-20220821093326466

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

@EnableRedisHttpSession
image-20220821093712051
image-20220821093712051
7、测试

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

GIF 2022-8-21 9-44-18
GIF 2022-8-21 9-44-18

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)
image-20220821094400481
image-20220821094400481

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;
    }
}
image-20220821094832279
image-20220821094832279

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

GIF 2022-8-21 9-47-15
GIF 2022-8-21 9-47-15

访问 http://order.gulimall.com/toTrade 页面,打开控制台,点击Network,查看请求可以发现,以下请求访问失败了

http://gulimall.com/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"}
GIF 2022-8-21 9-54-54
GIF 2022-8-21 9-54-54

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

image-20220821095545062
image-20220821095545062

查看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
image-20220821095653346
image-20220821095653346
8、修改代码

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

image-20220821095713343
image-20220821095713343

修改gulimall-member模块的com.atguigu.gulimall.member.interceptor.LoginUserInterceptor类的preHandle方法

boolean match = new AntPathMatcher().match("/member/**", uri);
image-20220821095912000
image-20220821095912000

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

GIF 2022-8-21 10-36-38
GIF 2022-8-21 10-36-38

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);
}
image-20220821104459980
image-20220821104459980

gulimall-order模块的com.atguigu.gulimall.order.service.OrderService接口里添加queryPageWithItem抽象方法

PageUtils queryPageWithItem(Map<String, Object> params);
image-20220821104531133
image-20220821104531133

gulimall-order模块的com.atguigu.gulimall.order.entity.OrderEntity类里添加itemEntities字段

private List<OrderItemEntity> itemEntities;
image-20220821104700619
image-20220821104700619

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);
}
image-20220821105623206
image-20220821105623206

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

image-20230105162906919
image-20230105162906919

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);
}
image-20220821111534607
image-20220821111534607

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";
}
image-20220821111942297
image-20220821111942297
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
image-20220821112441583
image-20220821112441583

查看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]
image-20220821112600476
image-20220821112600476

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

image-20220821113255095
image-20220821113255095

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

image-20220821113512193
image-20220821113512193

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

点击查看GuliFeignConfig文件

image-20220821113652369
image-20220821113652369
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)
image-20220821113907324
image-20220821113907324

查看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]
image-20220821113954107
image-20220821113954107

查看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]
image-20220821114139421
image-20220821114139421

修改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";
}
image-20220821114438979
image-20220821114438979

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";
}
image-20220821114829708
image-20220821114829708
4、不知道item_entities字段

重启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]
image-20220821114548173
image-20220821114548173

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

@TableField(exist = false)
private List<OrderItemEntity> itemEntities;
image-20220821114644661
image-20220821114644661
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")

image-20220821161320706
image-20220821161320706

重新启动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"}]}}
image-20220821115628224
image-20220821115628224

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

image-20220821115815504
image-20220821115815504

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);
}
image-20220821171411273
image-20220821171411273

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

image-20220821172050128
image-20220821172050128

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

image-20220821171917121
6、修改我的订单页

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

image-20220821170704862
image-20220821170704862

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

image-20220821170808432
image-20220821170808432

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

image-20220821195316547
image-20220821195316547

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

image-20220821195223053
image-20220821195223053

2、内网穿透

1、原理

付款成功后,支付宝会多次向配置的异步通知接口发请求,直到该接口返回success才停止

查看文档: https://opendocs.alipay.com/open/270/105902

image-20220822212951827
image-20220822212951827

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

image-20220822103936610
image-20220822103936610

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

image-20220822103955313
image-20220822103955313

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

image-20220822104037547
image-20220822104037547
2、添加异步通知地址

gulimall-order模块的com.atguigu.gulimall.order.config.AlipayTemplate类里,修改notifyUrl的值为内网穿透服务商提供的域名+/payed/notify

private String notifyUrl="http://1661133527191.free.aeert.com/payed/notify";
image-20220822100221123
image-20220822100221123

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";
    }
}
image-20220822095214928
image-20220822095214928

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

http://order.gulimall.com/payed/notify
http://auth.gulimall.com/login.html
image-20220822103457122
image-20220822103457122

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;
    }
    ....
}

点击查看LoginUserInterceptor类完整代码

image-20220822103124588
image-20220822103124588

再次访问 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
image-20220822103212900
image-20220822103212900

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

order.gulimall.com:80
image-20220822101828649
image-20220822101828649
3、请求头Host不匹配

直接访问内网穿透服务商提供的域名,可以正确访问nginx里设置的访问 http://order.gulimall.com:80 网址的默认页面(文件为/mydata/nginx/html/index.html

http://1661133527191.free.aeert.com/
image-20220822102146497
image-20220822102146497

访问 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
image-20220822102718135
image-20220822102718135

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

image-20230106095424374
image-20230106095424374

访问失败的原因如下图所示,本质上就是niginx转发请求头Hostorder.gulimall.com的请求时,能够正常转发到192.168.56.10,而请求头Host1661133527191.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/下的Hostorder.gulimall.com并配置代理1661133527191.free.aeert.com域名下的请求。

image-20220822104128371
image-20220822104128371
4、修改请求头的Host

/mydata/nginx/conf/conf.d/gulimall.conf文件里手动修改/payed/下的Hostorder.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
image-20220822094228272
image-20220822094228272

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

location /payed/ {
    proxy_set_header Host order.gulimall.com;
    proxy_pass http://gulimall;
}
image-20220822104539560
image-20220822104539560

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

image-20220822104730726
image-20220822104730726
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"
image-20220822110122903
image-20220822110122903

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

server_name  gulimall.com *.gulimall.com 1661133527191.free.aeert.com;
image-20220822110624899
image-20220822110624899

再次重启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
image-20220822110805349
image-20220822110805349

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

image-20220822110942054
image-20220822110942054

查看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"
image-20220822111211397
image-20220822111211397

点击查看完整nginxgulimall.conf配置

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";
}
image-20220822113816891
image-20220822113816891

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

image-20220822113903714
image-20220822113903714

格式后的json如下图所示

image-20220822114321604
image-20220822114321604
2、处理支付结果

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

image-20220822114640034
image-20220822114640034

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

image-20220822115552353
image-20220822115552353

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";
}
image-20220822151957993
image-20220822151957993

gulimall-order模块的com.atguigu.gulimall.order.service.OrderService接口里添加handlePayResult方法

boolean handlePayResult(PayAsyncVo vo);
image-20220822152030613
image-20220822152030613

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

image-20220822152138297
image-20220822152138297

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;
}
image-20220822154012779
image-20220822154012779

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

image-20220822152922368
image-20220822152922368

gulimall_oms数据库的oms_payment_info表里,给order_snalipay_trade_no添加唯一索引

image-20220822153135012
image-20220822153135012

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

image-20220822153536184
image-20220822153536184
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);
image-20220822154529589
image-20220822154529589

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>
image-20220822154721487
image-20220822154721487

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());
}
image-20220822164100234
image-20220822164100234
4、数据无法成功封装

重新支付商品后,在GulimallOrderApplication服务的控制台报了下图所示的错误

image-20220822163832684
image-20220822163832684

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

image-20220822170954882
image-20220822170954882

返回的信息的值为数组,如果使用蛇形命名法可以正确处理,但是使用驼峰命名法不能正确处理

{
	"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类型,然后修改对应的controllerservice接口等,都使用PayAsyncVo2类来封装信息

image-20220822192126693
image-20220822192126693

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

image-20220822191928493
image-20220822191928493

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

image-20220822193007069
image-20220822193007069
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());
}
image-20220822183600466
image-20220822183600466

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

image-20220822172111771
image-20220822172111771

修改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;
}
image-20220822172149798
image-20220822172149798
6、notify_time字段无法封装

测试后,报了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]]
image-20220822172032664
image-20220822172032664

gulimall-order模块的src/main/resources/application.properties配置文件里添加如下配置,全局指定使用的日期格式。

spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
image-20220822172836401
image-20220822172836401

或在gulimall-order模块的com.atguigu.gulimall.order.vo.PayAsyncVo类的notify_time字段上添加如下注解,指定该字段使用的日期格式。

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date notify_time;
image-20220822184613577
image-20220822184613577

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

image-20220822173038730
image-20220822173038730

4、支付时设置收单

收单(超过允许的时间后不允许用户支付)场景:

  1. 订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。

    • 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
  2. 由于时延等问题,订单解锁完成,正在解锁库存的时候,异步通知才到

    • 订单解锁,手动调用收单
  3. 网络阻塞问题,订单支付成功的异步通知一直不到达

    • 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态
  4. 其他各种问题

    • 每天晚上闲时下载支付宝对账单,一 一进行对账

文档地址: https://opendocs.alipay.com/open/028r8t?scene=22

image-20220822214039556
image-20220822214039556

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

点击查看完整代码

image-20220822215150136
image-20220822215150136

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

image-20220823104711855
image-20220823104711855

超过时间再支付,此时就会显示抱歉,您的交易因超时已失败。

抱歉,您的交易因超时已失败。
您订单的最晚付款时间为: 2022-08-23 10:47:16,目前已过期,交易关闭。

image-20220823104755770'

5、手动调用收单

gulimall-order模块的com.atguigu.gulimall.order.listener.OrderCloseListener类的listener方法里,关闭订单后,手动调用收单接口,防止用户在订单关闭后才支付。(其实我认为应该先手动调用收单接口关闭订单,因为如果关闭订单后,用户此时支付了,而我们手动调用收单接口的请求还没有发给支付宝,此时就会出现订单关闭了,但是用户成功支付的情况,而先先手动调用收单接口关闭订单就不会出现这种情况了)

image-20220822214900198
image-20220822214900198

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

image-20220822215254889
image-20220822215254889

相关代码如下图所示

image-20220822215517613
image-20220822215517613

七、秒杀

7.1、秒杀

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

image-20220824213156518
image-20220824213156518
image-20220824214655339
image-20220824214655339

7.1.1、新增秒杀场次

1、添加秒杀场次

1、秒杀业务:秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化) + 独立部署。

限流方式:

  1. 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
  2. nginx限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
  3. 网关限流,限流的过滤器
  4. 代码中使用分布式信号量
  5. 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://localhost:88/api/coupon/seckillsession/list?t=1661218666831&page=1&limit=10&key=
image-20220823093853412
image-20220823093853412

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}
image-20220823094011610
image-20220823094011610

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

image-20220823094153725
image-20220823094153725
2、添加秒杀场次

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

image-20220823094350190
image-20220823094350190

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

image-20220823094450252
image-20220823094450252

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

image-20220823094551807
image-20220823094551807

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

image-20220823094831694
image-20220823094831694
3、关联商品

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

image-20220823095118825
image-20220823095118825

gulimall_sms数据库的sms_seckill_sku_relation表里,给12promotionSessionId都新增一下数据

image-20220823100824094
image-20220823100824094

优惠营销 -> 每日秒杀页面里,点击操作的关联商品,这时就能显示这个秒杀id的所有关联的信息了,请求的接口如下

http://localhost:88/api/coupon/seckillskurelation/list?t=1661220555869&page=1&limit=10&key=&promotionSessionId=1

image-20220823100930503
image-20220823100930503

然后点击参数名的输入框,输入1,按回车,此时又发了如下请求,并查询到了相关的关联商品信息

http://localhost:88/api/coupon/seckillskurelation/list?t=1661220676407&page=1&limit=10&key=1&promotionSessionId=1
image-20220823101254572
image-20220823101254572

查看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 = ?) 
image-20220823101248884
image-20220823101248884

优惠营销 -> 每日秒杀页面里,点击第2个秒杀场次的操作的关联商品,此时会发如下请求

http://localhost:88/api/coupon/seckillskurelation/list?t=1661220577315&page=1&limit=10&key=&promotionSessionId=2

image-20220823101036682
image-20220823101036682
4、添加关联商品

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

image-20220823103632186
image-20220823103632186

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

image-20220823103650832
image-20220823103650832

7.1.2、秒杀模块

1、新建秒杀模块

1、新建模块

选中IDEAProjectgulimall,右键依次点击New->Module->Spring Initializr->Next

New Module对话框里Group里输入com.atguigu.gulimallArtifact里输入gulimall-seckillJava Version选择8Description里输入秒杀Package里输入com.atguigu.gulimall.seckill,然后点击Next

com.atguigu.gulimall
gulimall-seckill
秒杀
com.atguigu.gulimall.seckill
image-20220823105607877
image-20220823105607877

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

image-20220823105826059
image-20220823105826059

最后点击Finish

image-20220823105828355
image-20220823105828355
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就好了(最好先替换文件,再加入到项目)

image-20220823110055075
image-20220823110055075

gulimall-seckill模块的pom.xml文件里,添加gulimall-common依赖

点击查看完整pom文件

<dependency>
   <groupId>com.atguigu.gulimall</groupId>
   <artifactId>gulimall-common</artifactId>
   <version>0.0.1-SNAPSHOT</version>
</dependency>
image-20220823110311352
image-20220823110311352
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");
   }

}
image-20220823111158722
image-20220823111158722
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
image-20220823110520190
image-20220823110520190

gulimall-seckill模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication启动类里添加@EnableDiscoveryClient注解,修改@SpringBootApplication,让其在启动的时候排除DataSourceAutoConfiguration

然后启动GulimallSeckillApplication服务,然后限制内存-Xmx100m

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
image-20220823111103762
image-20220823111103762

2、cron表达式

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

image-20220823111922564
image-20220823111922564
CronTrigger Tutorial

CronTrigger Tutorialopen in new windowIntroductionopen in new windowFormatopen in new windowSpecial charactersopen in new windowExamplesopen in new windowNotesopen in new window

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 NameMandatoryAllowed ValuesAllowed Special Characters
SecondsYES0-59, - * /
MinutesYES0-59, - * /
HoursYES0-23, - * /
Day of monthYES1-31, - * ? / L W
MonthYES1-12 or JAN-DEC, - * /
Day of weekYES1-7 or SUN-SAT, - * ? / L #
YearNOempty, 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 as mon.

Examples

Here are some full examples:

ExpressionMeaning
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 * * ? 2005Fire 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 WEDFire at 2:10pm and at 2:44pm every Wednesday in the month of March.
0 15 10 ? * MON-FRIFire 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 ? * 6LFire at 10:15am on the last Friday of every month
0 15 10 ? * 6LFire at 10:15am on the last Friday of every month
0 15 10 ? * 6L 2002-2005Fire at 10:15am on every last friday of every month during the years 2002, 2003, 2004 and 2005
0 15 10 ? * 6#3Fire 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...");
    }
}
image-20220823114012352
image-20220823114012352

运行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...
image-20220823113937150
image-20220823113937150
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...
image-20220823155437830
image-20220823155437830

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);
}
image-20220823162502400
image-20220823162502400
2、指定定时任务线程池大小(不生效)

方法二:指定定时任务线程池大小(不生效)

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

image-20220823160902099
image-20220823160902099

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

image-20220823160904223
image-20220823160904223

gulimall-seckill模块的src/main/resources/application.properties配置文件里添加如下配置,使用此配置不会生效

spring.task.scheduling.pool.size=5
image-20220823161015789
image-20220823161015789

重启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...
image-20220823162224491
image-20220823162224491
3、使用Spring自带的异步任务

方法三:Spring的异步任务

gulimall-seckill模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication启动类上添加@EnableAsync注解,开启异步任务

@EnableAsync
image-20220823161617048
image-20220823161617048

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

@Async
image-20220823161624006
image-20220823161624006

重启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...
image-20220823161438635
image-20220823161438635

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();
}
image-20220823163150679
image-20220823163150679
2、ThreadPoolTaskExecutor继承关系

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

image-20230106161740731
image-20230106161740731

点击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
image-20220823163522592
image-20220823163522592

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

image-20220823163532548
image-20220823163532548

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

image-20220823163536360
image-20220823163536360

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

image-20220823163541582
image-20220823163541582

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

image-20220823163544520
image-20220823163544520
3、修改配置

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

image-20220823164048721
image-20220823164048721

gulimall-seckill模块的src/main/resources/application.properties配置文件里,修改核心线程数最大线程数

spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50
image-20220823164333075
image-20220823164333075

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

image-20220823164821069
image-20220823164821069

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

image-20220823164823629
image-20220823164823629

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 {
}
image-20220823165012347
image-20220823165012347

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();


    }
}
image-20220823165846917
image-20220823165846917

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();
}
image-20220823165941772
image-20220823165941772

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.扫描需要参与秒杀的活动

    }
}
image-20220823170120090
image-20220823170120090
2、远程获取秒杀活动场次

gulimall-seckill模块的com.atguigu.gulimall.seckill.GulimallSeckillApplication类上添加@EnableFeignClients注解,用于开启Feign的远程调用功能

@EnableFeignClients
image-20220823170152658
image-20220823170152658

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 {

}
image-20220823185509399
image-20220823185509399

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);
}
image-20220823170817519
image-20220823170817519

gulimall-coupon模块的com.atguigu.gulimall.coupon.service.SeckillSessionService接口里添加getLatest3DaySession抽象方法

List<SeckillSessionEntity> getLatest3DaySession();
image-20220823170900023
image-20220823170900023

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'
image-20220823171438483
image-20220823171438483
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("=============================");
   }

}
image-20220823172547340
image-20220823172547340

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

2022-08-23
2022-08-25
=============================
00:00
23:59:59.999999999
=============================
2022-08-23T00:00
2022-08-25T23:59:59.999999999
=============================
image-20220823172550405
image-20220823172550405
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"));
}
image-20220823184155523
image-20220823184155523

gulimall-coupon模块的com.atguigu.gulimall.coupon.entity.SeckillSessionEntity类里添加如下字段。

@TableField(exist = false)
private List<SeckillSkuRelationEntity> relationSkus;
image-20220823184504005
image-20220823184504005

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());
}
image-20220823185300381
image-20220823185300381

gulimall-seckill模块的com.atguigu.gulimall.seckill.feign.CouponFeignService接口里,添加getLatest3DaySession方法

@GetMapping("/coupon/seckillsession/latest3DaySession")
public R getLatest3DaySession();
image-20220823185608694
image-20220823185608694

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;
    }
}
image-20220823191307090
image-20220823191307090

gulimall-common模块的com.atguigu.common.utils.R类里添加如下代码

点击查看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);
}
image-20220823193539875
image-20220823193539875
5、添加saveSessionInfos

gulimall-seckill模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl类里添加saveSessionInfos方法,修改uploadSeckillSkuLatest3Days方法

点击查看完整SeckillServiceImpl类代码

@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);
    });
}
image-20220823203841311
image-20220823203841311

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

点击查看完整SeckillSkuRedisTo类代码

image-20220824102402560
image-20220824102402560

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

image-20220823210922935
image-20220823210922935

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);
}
image-20220823211310200
image-20220823211310200

修改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)方法

image-20220824100038799
image-20220824100038799

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);
}
image-20220824103400442
image-20220824103400442

7.1.3、分布式信号量

1、引入redisson

1、添加依赖和配置

由于秒杀的请求量大,不可能查数据库,因此可以使用分布式信号量机制。

gulimall-seckill模块的pom.xml文件里添加如下依赖,引入redisson

<!-- 引入redisson,做分布式锁和分布式对象 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>
image-20220824103639370
image-20220824103639370

复制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);
    }

}
image-20220824103755664
image-20220824103755664
2、使用redisson

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());
image-20220824105240671
image-20220824105240671
3、准备测试

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

image-20220824105137257
image-20220824105137257

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();
}
image-20220824110210600
image-20220824110210600

修改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);
}
image-20220824110516106
image-20220824110516106
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.
image-20220824110044069
image-20220824110044069

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

image-20220824111122825
image-20220824111122825

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

image-20220824111233658
image-20220824111233658

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

image-20220824112717946
image-20220824112717946

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

image-20220824111236341
image-20220824111236341

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

image-20220824111238837
image-20220824111238837
5、修改代码重新测试

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

image-20220824113333439
image-20220824113333439

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

image-20220824113140143
image-20220824113140143

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

image-20220824113434386
image-20220824113434386

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

image-20220824113456618
image-20220824113456618

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

image-20220824113932773
image-20220824113932773

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

image-20220824113953199
image-20220824113953199

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

image-20220824113934918
image-20220824113934918

2、定时任务-分布式下的问题

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

image-20220824114752543
image-20220824114752543
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();
    }
}
image-20220824115401241
image-20220824115401241

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);
        }
    });
}
image-20220824153409609
image-20220824153409609

(但是我感觉这有问题,有可能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());
            }
        }
        );
    });
}
image-20220824155345831
image-20220824155345831
2、测试

删除redis里以seckill开头的数据,重启GulimallSeckillApplication服务,让上架秒杀的商品信息的定时任务执行多次

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

image-20220824155611328
image-20220824155611328

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

image-20220824155603814
image-20220824155603814

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

image-20220824155606181
image-20220824155606181
3、修改代码后再次测试

gulimall-seckill模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl类里,修改saveSessionInfos方法

String skuKey = seckillSkuRelationVo.getSkuId().toString();

替换为

String skuKey = session.getStartTime().getTime() +"_"+ seckillSkuRelationVo.getSkuId().toString();

用于区分不同活动的促销信息。

image-20230109100345358
image-20230109100345358

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

image-20220824161802227
image-20220824161802227

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

image-20220824161804685
image-20220824161804685

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

image-20220824161806803
image-20220824161806803
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();

效果都一样,不过老师讲的方法显得更清晰一些

image-20220824162159051
image-20220824162159051

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());

点击查看SeckillServiceImpl类完整代码

image-20220824162740947
image-20220824162740947

删除redis里以seckill开头的数据,重启GulimallSeckillApplication服务,再让上架秒杀的商品信息的定时任务执行多次

可以看到seckill:sessions:里多次执行上架秒杀的商品信息的定时任务后并没有多次添加了,而且也更容易区分场次信息了

image-20220824163040808
image-20220824163040808

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

image-20220824163042716
image-20220824163042716

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

image-20220824163045199
image-20220824163045199

都设置好了,不过好像都没设置过期时间

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);
    }
}
image-20220824163641496
image-20220824163641496

gulimall-seckill模块的com.atguigu.gulimall.seckill.service.SeckillService接口里添加getCurrentSeckillSkus抽象方法

List<SeckillSkuRedisTo> getCurrentSeckillSkus();
image-20220824163717457
image-20220824163717457
2、查看文档

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

image-20220824165909399
image-20220824165909399

redis文档: LRANGE | Redisopen in new window

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"
image-20220824170028012
image-20220824170028012
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()
image-20220824172757088
image-20220824172757088

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

image-20220824185200928
image-20220824185200928

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

image-20220824185234350
image-20220824185234350

4、测试

1、使用Collections.singleton(range)

修改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;
}
image-20220824183720157
image-20220824183720157
2、添加秒杀场次

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

image-20220824184307893
image-20220824184307893

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

image-20220824184401833
image-20220824184401833

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

image-20220824184426043
image-20220824184426043
3、ArrayList不能强转成String

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

image-20220824184642047
image-20220824184642047

打开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) 
image-20220824184721715
image-20220824184721715
4、方法一

可以将Collections.singleton(range)修改为Arrays.asList(range.toArray())

image-20220824184931379
image-20220824184931379

重启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}}]}
image-20220824184846694
image-20220824184846694
5、方法二(推荐)

修改BoundHashOperations的泛型,把BoundHashOperations<String, Object, Object> operations改为BoundHashOperations<String, String, Object> operations。把operations.multiGet(Arrays.asList(range.toArray()));改为operations.multiGet(range);

点击查看SeckillServiceImpl类完整代码

@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;
}
image-20220824185523332
image-20220824185523332

删除reidsseckill开头的key,重新启动GulimallSeckillApplication服务,等待刚刚创建的秒杀活动保存到redis

可以看到这样也可以保存成功 http://localhost:25000/currentSeckillSkus

image-20220824190053736
image-20220824190053736
6、原因

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

image-20220824190222468
image-20220824190222468

这是因为我们最开始使用的BoundHashOperations的泛型是BoundHashOperations<String, Object, Object> operations,此时的HKObject

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);
   
   ......
}
image-20220824190224762
image-20220824190224762

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

image-20220824190226691
image-20220824190226691

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

image-20220824190305321
image-20220824190305321

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

image-20220824190516456
image-20220824190516456

7.1.4、显示秒杀商品

1、添加配置

gulimall-gateway模块的src/main/resources/application.yml配置文件里添加如下配置,将seckill.gulimall.com域名的请求全部负载均衡到gulimall-seckill模块

点击查看application.yml配置文件完整配置

spring:
  cloud:
    gateway:
      routes:
        - id: gulimall_seckill_route
          uri: lb://gulimall-seckill
          predicates:
            - Host=seckill.gulimall.com
image-20220824190755730
image-20220824190755730

打开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
image-20220824190957079
image-20220824190957079

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

image-20220824191117496
image-20220824191117496

2、显示秒杀商品

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

image-20220824191521071
image-20220824191521071

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

image-20220824191802504
image-20220824191802504

修改后的代码如下

image-20220824191922056
image-20220824191922056

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");
    })
  }
})
image-20220824193416056
image-20220824193416056

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

image-20220824193317275
image-20220824193317275

3、查询当前sku是否参与秒杀优惠

1、获取sku秒杀信息

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

image-20220824193616733
image-20220824193616733

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);
}
image-20220824194402058
image-20220824194402058

gulimall-seckill模块的com.atguigu.gulimall.seckill.service.SeckillService接口里添加getSkuSeckillInfo抽象方法

SeckillSkuRedisTo getSkuSeckillInfo(Long skuId);
image-20220824194404672
image-20220824194404672

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;
}
image-20220824195711178
image-20220824195711178
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);
}
image-20220824201857677
image-20220824201857677

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

点击查看SeckillInfoVo类完整代码

image-20220824200357390
image-20220824200357390

gulimall-product模块的com.atguigu.gulimall.product.vo.SkuItemVo类里添加seckillInfo字段(不是刚刚新添加的seckillInfo类,别添加错了)

点击查看SkuItemVo类完整代码

/**
 * 当前商品的秒杀优惠信息
 */
SeckillInfoVo seckillInfo;
image-20220824200619492
image-20220824200619492

gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl类里,修改item方法,在CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();上面添加如下异步执行的请求,并将其添加到CompletableFuture.allOf的参数里面

点击查看SkuInfoServiceImpl类完整代码

@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();
image-20220824201018390
image-20220824201018390

7.1.5、商品页面添加秒杀提醒

1、商品页添加秒杀信息

1、商品页添加秒杀开始时间

在 http://gulimall.com/5.html 页面里,打开控制台,定位到预约享资格位置,复制预约享资格

image-20220824201253539
image-20220824201253539

gulimall-product模块的src/main/resources/templates/item.html配置文件里搜索预约享资格,将预约享资格修改为[[${item.seckillInfo.startTime}]]

<li style="color: red" th:if="${item.seckillInfo!=null}">
   <!--预约享资格-->
   [[${item.seckillInfo.startTime}]]
</li>
image-20220824201631421
image-20220824201631421

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

image-20220824202224602
image-20220824202224602

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>
image-20220824204454541
image-20220824204454541

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

image-20220824204506239
image-20220824204506239
2、商品页添加秒杀价

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

image-20220824204647496
image-20220824204647496

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>
image-20220824205419223
image-20220824205419223

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

image-20220824205448268
image-20220824205448268

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");
    })
  }
})
image-20220824211447192
image-20220824211447192

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

GIF 2022-8-24 21-12-30
GIF 2022-8-24 21-12-30

2、处理秒杀逻辑

1、高并发系统关注的问题

秒杀( 高并发) 系统应关注以下问题

image-20220824213156518
image-20220824213156518
image-20220824214655339
image-20220824214655339
2、修改秒杀场次时间

在 http://item.gulimall.com/1.html 页面里,打开控制台,定位到加入购物车位置,复制加入购物车

image-20220824215059692
image-20220824215059692

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>
image-20220828184609122
image-20220828184609122

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

image-20220828150556457
image-20220828150556457

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

image-20220828150953013
image-20220828150953013

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

image-20220828151021501
image-20220828151021501

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

image-20220828151023910
image-20220828151023910
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]
image-20220828151400315
image-20220828151400315

gulimall-seckill模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl类里,修改saveSessionInfos方法

if (hasKey == null || !hasKey && CollectionUtils.isEmpty(session.getRelationSkus())) {
image-20220828151556751
image-20220828151556751

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

<a href="#" id="secKillA" skuid="1" sessionid="4" code="0b41ce397be0476c9cfd604924546a56">立即抢购</a>
image-20220828185401770
image-20220828185401770

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;
})
image-20220828190301580
image-20220828190301580

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

GIF 2022-8-28 19-16-39
GIF 2022-8-28 19-16-39
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;
})
image-20220828191809849
image-20220828191809849

在 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的错误,这是正常的,因为这个接口还没写。

GIF 2022-8-28 19-14-15
GIF 2022-8-28 19-14-15

3、引入SpringSession

1、引入SpringSession

gulimall-seckill模块的pom.xml文件里引入SpringSession

<!--引入SpringSession-->
<dependency>
   <groupId>org.springframework.session</groupId>
   <artifactId>spring-session-data-redis</artifactId>
</dependency>
image-20220828192835324
image-20220828192835324
2、添加配置

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

spring.session.store-type=redis
image-20220828193007418
image-20220828193007418

复制gulimall-product模块的com.atguigu.gulimall.product.config.GulimallSessionConfig类,粘贴到gulimall-seckill模块的com.atguigu.gulimall.seckill.config包下。

点击查看GulimallSessionConfig类完整代码

image-20220828193203195
image-20220828193203195

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

@EnableRedisHttpSession
image-20220828193248078
image-20220828193248078

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

点击查看LoginUserInterceptor类完整代码

image-20220828193600420
image-20220828193600420

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;
}
image-20220828193912831
image-20220828193912831

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("/**");
    }
}
image-20220828194329439
image-20220828194329439

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

GIF 2022-8-28 19-47-15
GIF 2022-8-28 19-47-15
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();
}
image-20220828211356212
image-20220828211356212

gulimall-seckill模块的com.atguigu.gulimall.seckill.service.SeckillService接口里添加kill抽象方法

String kill(String killId, String key, Integer num);
image-20220828202027388
image-20220828202027388

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;
}
image-20220828211147911
image-20220828211147911
4、SETNX只能一个抢到

org.springframework.data.redis.core.ValueOperations接口里有个Boolean setIfAbsent(K key, V value);方法,只能有一个人能够抢到该key,并将值设置进去。抢到了返回trueredis里返回1),没抢到返回falseredis里返回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);
image-20220828204711411
image-20220828204711411

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 SETopen in new window. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists".

Return

Integer replyopen in new window, specifically:

  • 1 if the key was set
  • 0 if the key was not set

Examples

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
image-20220828204930892
image-20220828204930892

4、秒杀中使用RabbitMQ

1、秒杀架构图

秒杀业务的RabbitMQ架构图如下图红色方框圈住的部分所示。

秒杀业务
秒杀业务
2、引入RabbitMQ

gulimall-seckill模块的pom.xml文件里引入amqp场景,使用RabbitMQ

<!--引入amqp场景,使用RabbitMQ-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
image-20220828211534829
image-20220828211534829

gulimall-seckill模块的src/main/resources/application.properties文件里添加如下依赖

spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.virtual-host=/
image-20220828212135844
image-20220828212135844

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

点击查看MyRabbitConfig类完整代码

image-20220828212503641
image-20220828212503641

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

image-20220828212608587
image-20220828212608587

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

image-20220828213121570
image-20220828213121570
3、接收消息

复制gulimall-seckill模块的com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo类的部分字段(skuInfoVo字段不复制,删除一些字段),添加orderSnmemberId字段

点击查看SecKillOrderTo类完整代码

image-20220828215201406
image-20220828215201406

gulimall-seckill模块的com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl类的kill方法里的String orderSn = IdWorker.getTimeId();下面添加如下代码,向RabbitMq发送消息。

点击查看kill方法完整代码

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);
image-20220828215724392
image-20220828215724392

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);
}
image-20220828220548675
image-20220828220548675
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();
        }
    }
}
image-20220828221412545
image-20220828221412545

修改gulimall-order模块的com.atguigu.gulimall.order.listener.OrderSeckillListener接口的createSeckillOrder抽象方法

void createSeckillOrder(SecKillOrderTo secKillOrderTo);
image-20220828221443166
image-20220828221443166

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);
}
image-20220828223052656
image-20220828223052656
5、测试

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

GIF 2022-8-28 22-51-54
GIF 2022-8-28 22-51-54

5、完善功能

1、修改页面

gulimall-cart模块的src/main/resources/templates/success.html文件复制到gulimall-seckill模块的src/main/resources/templates文件夹里

image-20220829190725053
image-20220829190725053
2、修改代码

gulimall-seckill模块的com.atguigu.gulimall.seckill.controller.SeckillController类上的@RestController注解修改为@Controller,然后在getCurrentSeckillSkusgetSkuSeckillInfo方法上添加@ResponseBody注解

image-20220829190944312
image-20220829190944312

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

点击查看SeckillController类完整代码

image-20220829191413653
image-20220829191413653
3、使用thymeleaf

gulimall-seckill模块的pom.xml文件里,引入thymeleaf模板引擎

<!--模板引擎:thymeleaf-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
image-20220829191534012
image-20220829191534012

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

spring.thymeleaf.cache=false
image-20220829191646309
image-20220829191646309
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";
}
image-20220829191905363
image-20220829191905363

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/
image-20220829192734285
image-20220829192734285

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

image-20220829192627147
image-20220829192627147

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>
image-20220828194851830
image-20220828194851830
5、测试

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

image-20220828195003525
image-20220828195003525

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

image-20220828194328641
image-20220828194328641

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

image-20220828194444601
image-20220828194444601

测试以下立即抢购的完整流程,可以看到逻辑都没啥问题

其实上线秒杀后,应该把秒杀上架的库存提前在库存服务锁定住,等秒杀结束后如果redis里还有库存,再解锁redis里剩余数量的库存

上架秒杀商品的时候,每一个数据都有过期时间。 秒杀后续的流程,简化了收货地址等信息。

GIF 2022-8-28 19-53-13
GIF 2022-8-28 19-53-13

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/介绍

image-20220828204432369
image-20220828204432369

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

image-20220828204441371
image-20220828204441371

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

Sentinel Logo
Sentinel Logo
Sentinel 是什么?

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinelopen in new window 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

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

Sentinel 的主要特性:

Sentinel-features-overview
Sentinel-features-overview

Sentinel 的开源生态:

Sentinel-opensource-eco
Sentinel-opensource-eco

Sentinel 分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
Sentinel 基本概念

资源

资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。

只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下, 可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

规则

围绕资源的实时状态设定的规则,可以包括流量控制规则熔断降级规则以及系统保护规 。所有规则可以动态实时调整。

Hystrix 与 Sentinel 比较
image-20220828205909056
image-20220828205909056

3、使用Sentinel文档

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

使用Sentinel离线文档: Sentinel使用

image-20220828221835525
image-20220828221835525

4、整合文档

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

整合SpringBoot离线文档:整合SpringBoot

image-20220828222732748
image-20220828222732748

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>
image-20220828223407840
image-20220828223407840
2、下载仪表盘

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

image-20220828223650514
image-20220828223650514

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

image-20220828224058293
image-20220828224058293

使用如下命令,启动sentinel仪表盘

java -jar sentinel-dashboard-1.6.3.jar
image-20220828224415756
image-20220828224415756
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
image-20220830190847834
image-20220830190847834
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);
}
image-20220830191851586
image-20220830191851586

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

image-20220830192311228
image-20220830192311228
5、测试

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

image-20220828224602517
image-20220828224602517

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

image-20220828224740812
image-20220828224740812

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

GIF 2022-8-30 19-32-27
GIF 2022-8-30 19-32-27
6、流量控制

点击查看流量控制规则open in new window

Field说明默认值
resource资源名,资源名是限流规则的作用对象
count限流阈值
grade限流阈值类型,QPS 模式(1)或并发线程数模式(0)QPS 模式
limitApp流控针对的调用来源default,代表不区分调用来源
strategy调用关系限流策略:直接、链路、关联根据资源本身(直接)
controlBehavior流控效果(直接拒绝/WarmUp/匀速+排队等待),不支持按调用关系限流直接拒绝
clusterMode是否集群限流

2、添加健康管理

1、数据的实时监控、审计、健康及指标信息

查看文档: Endpoint 支持open in new window

GIF 2022-8-31 20-41-38
GIF 2022-8-31 20-41-38

在使用 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 的心跳频率等等信息。

image-20220830194520683
image-20220830194520683
2、启用SpringBoot健康管理

gulimall-seckill模块的pom.xml文件里添加actuator健康管理依赖

(如果在gulimall-order设置会循环依赖,后面会说)

<!--健康管理-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
image-20220830194553967
image-20220830194553967

jmx方式暴露健康信息(即在JConsolejvisualvm等扩展上可以看到健康信息,默认只使用这种方式暴露健康信息。还可以配置以web方式暴露健康信息,这样不仅可以在扩展上看到这些信息,也可以通过浏览器看到这些健康信息)

参见:Java Management Extensionsopen in new window

JMX(英语:Java Management Extensions,即Java管理扩展)是Javaopen in new window平台上为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统open in new window平台、系统体系结构open in new window网络传输协议open in new window,灵活的开发无缝集成的系统、网络和服务管理应用。

#健康管理暴露所有端点
management.endpoints.jmx.exposure.include='*'
image-20220830195321824
image-20220830195321824
3、修改限流后返回的数据

gulimall-common模块的com.atguigu.common.exception.BizCodeException枚举类里添加如下枚举,用于返回限流数据

/**
 * 同一个接口QPS每秒发送的请求数过多
 */
TOO_MANY_REQUEST(10003,"请求流量过大异常"),
image-20220830200955651
image-20220830200955651

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));
            }
        });
    }

}
image-20220830203705864
image-20220830203705864

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

GIF 2022-8-30 20-41-54
GIF 2022-8-30 20-41-54

3、其他模块添加健康管理

1、订单模块添加健康管理

gulimall-order模块的pom.xml文件里添加actuator健康管理依赖

<!--健康管理-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
image-20220830205407446
image-20220830205407446

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='*'
image-20220830205414524
image-20220830205414524
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)
└─────┘
image-20220830205546089
image-20220830205546089

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

image-20220830210226251
image-20220830210226251

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

image-20220830211036893
image-20220830211036893

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

image-20220830211221193
image-20220830211221193

gulimall-order模块的com.atguigu.gulimall.order.config.MyRabbitConfig类里添加rabbitTemplate(ConnectionFactory connectionFactory)方法,我们自己向容器中存放RabbitTemplate,自己设置我们定制的MessageConverter

删除RabbitTemplate rabbitTemplate;字段上的@Autowired注解,和initRabbitTemplate方法上的@PostConstruct注解

点击查看MyRabbitConfig类完整代码

@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
    rabbitTemplate = new RabbitTemplate(connectionFactory);
    rabbitTemplate.setMessageConverter(messageConverter());
    initRabbitTemplate();
    return rabbitTemplate;
}
image-20220830211601054
image-20220830211601054
3、其他模块添加配置

给其他模块(gulimall-auth-servergulimall-cartgulimall-coupongulimall-gatewaygulimall-membergulimall-productgulimall-searchgulimall-third-partygulimall-ware)添加配置

image-20220830213210941
image-20220830213210941

在其他模块的pom.xml文件里添加actuator健康管理依赖

<!--健康管理-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
image-20220830212542596
image-20220830212542596

在其他模块的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='*'
image-20230110095623813
image-20230110095623813

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

image-20220830214931962
image-20220830214931962

重启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
image-20220831172712413
image-20220831172712413

这是由不匹配的 Spring Boot 依赖项引起的open in new window

这是由于版本不匹配导致的,将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>
image-20220831173123968
image-20220831173123968

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

image-20220831173247604
image-20220831173247604

将该测试类修改为junit4即可

点击查看GulimallThirdPartyApplicationTests类完整代码

image-20220831173330424
image-20220831173330424

4、流控规则

1、集群阈值模式--单机均摊

单机均摊:该模式下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);

设置的阈值为10,则每台机器的阈值都为10

全局模式:配置的阈值等同于整个集群的总阈值。

每台机器分得的QPS/线程数不一样,但这些机器分得的总数为配置的阈值。

参考链接:https://zhuanlan.zhihu.com/p/364009386

image-20220831174304830
image-20220831174304830
2、流控模式--直连

流量控制open in new window :只对该服务进行流控

image-20220831201155540
image-20220831201155540
3、流控模式--关联

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

image-20220831195328836
image-20220831195328836
4、流控模式--链路

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

     a
   /   \
  b1     b2
 /        \
c          c
image-20220831200325381
image-20220831200325381
5、流控效果--快速失败

达到阈值后,直接失败。假设设置阈值为500,有700个请求,则多出的200个请求直接丢弃。

image-20220831201522214
image-20220831201522214
6、流控效果--Warm Up

设置阈值是500,预热时间是10s,则在10s内缓慢增加流量,直到10s后才达到峰值500

image-20220831201259563
image-20220831201259563
7、流控效果--排队等待

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

image-20220831201759316
image-20220831201759316
8、参考文档

在线文档: https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6

离线文档: 流量控制概述

image-20220831182009572
image-20220831182009572

7.2.3、Sentinel开启feign链路追踪

1、添加Feign 支持

1、添加Feign 支持参考文档

整个调用链只发现了这个请求,并没有发现通过feign远程调用的这些链路。而我们更要做的是对被调用方的熔断、保护、降级。

参考文档: Feign 支持open in new window

GIF 2022-8-31 20-42-59
GIF 2022-8-31 20-42-59
2、Sentinel开启feign链路追踪

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

image-20220828205444897
image-20220828205444897

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

image-20220828205535540
image-20220828205535540

给调用方的配置文件里添加feign.sentinel.enabled=true配置,使Sentinel开启feign链路追踪

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

#Sentinel开启feign链路追踪
feign.sentinel.enabled=true
image-20220829143201917
image-20220829143201917
3、测试

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

GET:http://gulimall-seckill/sku/seckill/{skuId}
image-20220828151418451
image-20220828151418451

2、消费方定制远程调用失败返回结果

1、停掉生产方

由于是gulimall-product模块调用gulimall-seckill模块,因此我们可以停掉GulimallSeckillApplication服务,模拟生产方宕机,不能提供服务。

image-20220828151118808
image-20220828151118808
2、查看异常

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

image-20220828205956889
image-20220828205956889
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);
    }
}
image-20220828151929580
image-20220828151929580

然后在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)
image-20220828152430564
image-20220828152430564

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);
    }
}
image-20220828152949670
image-20220828152949670
4、测试

gulimall-product模块的src/main/resources/application.yml配置文件里,将com.atguigu.gulimall包下的error级别修改为info级别

logging:
  level:
    com.atguigu.gulimall: info
image-20220828152754109
image-20220828152754109

重启GulimallProductApplication服务

image-20220828153102628
image-20220828153102628

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

image-20220828153155146
image-20220828153155146

此时GulimallProductApplication服务的控制台已经显示熔断方法被调用...getSkuSeckillInfo,表明刚刚写的SeckillFeignServiceFallBack远程调用失败处理类已经生效了

2022-08-28 15:33:44.029  INFO 14668 --- [ool-2-thread-15] c.a.g.p.f.f.SeckillFeignServiceFallBack  : 熔断方法被调用...getSkuSeckillInfo
image-20220828153403769
image-20220828153403769
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);
}
image-20220828154723077
image-20220828154723077

启动GulimallSeckillApplication服务,没设置降级前,刷新页面,然后疯狂按f5刷新,可用看到怎么都不会调用降级方法。

设置RT(响应时间)为1ms,时间窗口为10s后,当在10s内的请求响应时间超过1ms的次数大于最小请求数目(默认为5)后,在这个10s内的后面的请求都会被熔断。当超过这个10s的时间段后又会再次恢复访问,又重新进行计数。

GIF 2022-8-28 16-17-12
GIF 2022-8-28 16-17-12
6、熔断降级策略open in new window

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 引入)
image-20220828162939048
image-20220828162939048
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));
            }
        });
    }

}
image-20220828163157492
image-20220828163157492

为每个服务(gulimall-auth-servergulimall-cartgulimall-coupongulimall-membergulimall-ordergulimall-searchgulimall-seckillgulimall-third-partygulimall-ware)都配置Sentinel开启feign链路追踪

#sentinel开启feign链路追踪
feign.sentinel.enabled=true
image-20220828164648978
image-20220828164648978

为每个服务(gulimall-auth-servergulimall-cartgulimall-coupongulimall-membergulimall-ordergulimall-productgulimall-searchgulimall-third-partygulimall-ware)都加上SeckillSentinelConfig配置类

image-20220828164701564
image-20220828164701564

7.2.4、自定义受保护资源open in new window

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;
}
image-20220828150816611
image-20220828150816611

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

image-20220828150520984
image-20220828150520984

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

GIF 2022-8-28 15-24-52
GIF 2022-8-28 15-24-52

2、对方法进行保护

1、对方法进行保护

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

@SentinelResource("getCurrentSeckillSkusResource")
image-20220828151904605
image-20220828151904605

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

image-20220828152932141
image-20220828152932141

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

GIF 2022-8-28 15-38-12
GIF 2022-8-28 15-38-12
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")
image-20220828154638997
image-20220828154638997

3、网关限流open in new window

1、添加依赖

整合网关需要引入spring-cloud-starter-alibaba-sentinelspring-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>
image-20220828160121708
image-20220828160121708
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 页面,可以看到已经返回自定义的结果了。

GIF 2022-8-28 19-20-58
GIF 2022-8-28 19-20-58
3、基于请求头限流

还可以争对各种属性进行限流,比如Client IPRemote HostHeaderURL参数Cookie

GIF 2022-8-28 19-29-08
GIF 2022-8-28 19-29-08
4、分组限流

可以在API 管理新增自定义API,然后对API 分组进行流控。

(如果没成功,需要修改pom文件里sentinel的版本号)

GIF 2022-8-28 19-36-50
GIF 2022-8-28 19-36-50
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=请求过多,请稍后重试
image-20220828195129017
image-20220828195129017

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;
            }
        });
    }

}
image-20220828213513731
image-20220828213513731

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

GIF 2022-8-28 21-38-15
GIF 2022-8-28 21-38-15

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打一个标签)

如果服务调用顺序如下

image-20220829091448319
image-20220829091448319

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

image-20220829091500261
image-20220829091500261

Span 之间的父子关系如下:

image-20220829091526049
image-20220829091526049

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>
image-20220829092228462
image-20220829092228462

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
image-20220829093736984
image-20220829093736984

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

image-20220829093740953
image-20220829093740953

重启各个模块,刷新 https://item.gulimall.com/1.html 页面,查看GulimallProductApplication服务的控制台,打印了如下日志

DEBUG [gulimall-product,4333ca18c611f553,cc74b65e5467a683,false]

gulimall-product:服务名
4333ca18c611f553:是 TranceId一条链路中,只有一个TranceId 
cc74b65e5467a683:是 spanId,链路中的基本工作单元 id
false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察
GIF 2022-8-29 9-39-47
GIF 2022-8-29 9-39-47

7.3.3、整合 zipkin 可视化观察

1、概述

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

image-20220829105134089
image-20220829105134089

2、docker 安装 zipkin 服务器

使用如下命令,安装 zipkin 服务器

docker run -d -p 9411:9411 openzipkin/zipkin
image-20220829095100126
image-20220829095100126

3、添加依赖

gulimall-common模块的pom.xml文件里添加zipkin的依赖,并删除sleuth的依赖(zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
image-20220829095416340
image-20220829095416340

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
image-20220829100727550
image-20220829100727550

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

GIF 2022-8-29 10-47-31
GIF 2022-8-29 10-47-31

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

GIF 2022-8-29 10-58-29
GIF 2022-8-29 10-58-29

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-storageopen in new windowzipkin-storage/elasticsearchopen in new window

通过 docker 的方式将Zipkin 数据持久化到elasticsearch的命令如下

docker run 
--env STORAGE_TYPE=elasticsearch 
--env ES_HOSTS=192.168.56.10:9200 
openzipkin/zipkin-dependencies
image-20220829114726137
image-20220829114726137

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

image-20220829114745615
image-20220829114745615

高级篇总结

高并发有三宝:缓存异步队排好

image-20220829115133200
image-20220829115133200
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.0.0-alpha.8