前言


  1.  学习视频:Java项目《谷粒商城》架构师级Java项目实战,对标阿里P6-P7,全网最强
  2.  学习文档: 
    1. 谷粒商城 - 个人笔记(基础篇一)
    2. 谷粒商城 - 个人笔记(基础篇二)
    3. 谷粒商城 - 个人笔记(基础篇三)
    4. 谷粒商城 - 个人笔记(高级篇一)
    5. 谷粒商城 - 个人笔记(高级篇二)
    6. 谷粒商城 - 个人笔记(高级篇三)
    7. 谷粒商城 - 个人笔记(高级篇四)
    8. 谷粒商城 - 个人笔记(高级篇五)
    9. 谷粒商城 - 个人笔记(集群部署篇一)
    10. 谷粒商城 - 个人笔记(集群部署篇二)
    11. 谷粒商城 - 个人笔记(集群部署篇三)
  3.  接口文档:谷粒商城接口文档

  4. 本内容仅用于个人学习笔记,如有侵扰,联系删

十二、商城业务-支付


1、支付宝支付

1.1、进入“蚂蚁金服开放平台”

支付宝开放 平台地址:

1.2、下载支付宝官方 demo,进行配置和测试

电脑网站支付文档:https://docs.open.alipay.com/270/106291/
下载 demo 

 ​​​​​​

 1.3、配置使用沙箱进行测试

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

沙箱应用基本信息

沙箱账号

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

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

2 、加密和数字签名
  • 加密是指:
    • 我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解密的技术。
    • 公钥和私钥都可以用来加密,也都可以用来解密。
    • 但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。
    • 加密的目的是:
      • 为了确保数据传输过程中的不可读性,就是不想让别人看到。
    • 加密方式:
      • 对称加密
      • 非对称加密
  • 签名:
    • 给我们将要发送的数据,做上一个唯一签名(类似于指纹)
    • 用来互相验证接收方和发送方的身份;
    • 在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以用来达到数据的明文传输。
  • 验签:
    • 支付宝为了验证请求的数据是否商户本人发的
    • 商户为了验证响应的数据是否支付宝发的

1.5、支付宝加密原理

  • 支付宝加密采用RSA非对称加密,分别在商户端和支付宝端有两对公钥和私钥
  • 在发送订单数据时,直接使用明文,但会使用商户私钥加一个对应的签名,支付宝端会使用商户公钥对签名进行验签,只有数据明文和签名对应的时候才能说明传输正确
  • 支付成功后,支付宝发送支付成功数据之外,还会使用支付宝私钥加一个对应的签名,商户端收到支付成功数据之后也会使用支付宝公钥延签,成功后才能确认

1.6、支付宝支付流程 

2、内网穿透

 

 2.1、简介

内网穿透功能可以允许我们使用外网的网址来访问主机;

正常的外网需要访问我们项目的流程是:

  1. 买服务器并且有公网固定 IP
  2. 买域名映射到服务器的 IP
  3. 域名需要进行备案和审核

2.2、使用场景

  1. 开发测试(微信、支付宝)
  2. 智慧互联
  3. 远程控制
  4. 私有云

2.3、内网穿透的几个常用软件

  1. natapp:https://natapp.cn/   优惠码:022B93FD9 折)[仅限第一次使用]
  2. 续断www.zhexi.tech          优惠码:SBQMEA95 折)[仅限第一次使用]
  3. 花生壳:https://www.oray.com/

2.4、内网穿透常用软件安装

续断:https://www.zhexi.tech/

第一步:登录

第二步:安装客户端

第三步:安装(一定使用管理员身份安装,否则安装失败)

安装好之后,会网站会感应到我们的主机

第四步:新建隧道

隧道建立好,会给我们生成一个域名

3、订单服务-整合支付

3.1、搭建支付宝沙箱环境

1、导入依赖

        <!--阿里支付模块-->
        <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.9.28.ALL</version>
        </dependency>

2、抽取支付工具类并进行配置

成功调用该接口后,返回的数据就是支付页面的html,因此后续会使用@ResponseBody

添加“com.atguigu.gulimall.order.config.AlipayTemplate”类,代码如下:

@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {

    //在支付宝创建的应用的id
    private   String app_id = "2016102600763190";

    // 商户私钥,您的PKCS8格式RSA2私钥
    private String merchant_private_key = "MjXN6Hnj8k2GAriRFt0BS9gjihbl9Rt38VMNbBi3Vt3Cy6TOwANLLJ/DfnYjRqwCG81fkyKlDqdsamdfCiTysCa0gQKBgQDYQ45LSRxAOTyM5NliBmtev0lbpDa7FqXL0UFgBel5VgA1Ysp0+6ex2n73NBHbaVPEXgNMnTdzU3WF9uHF4Gj0mfUzbVMbj/YkkHDOZHBggAjEHCB87IKowq/uAH/++Qes2GipHHCTJlG6yejdxhOsMZXdCRnidNx5yv9+2JI37QKBgQCw0xn7ZeRBIOXxW7xFJw1WecUV7yaL9OWqKRHat3lFtf1Qo/87cLl+KeObvQjjXuUe07UkrS05h6ijWyCFlBo2V7Cdb3qjq4atUwScKfTJONnrF+fwTX0L5QgyQeDX5a4yYp4pLmt6HKh34sI5S/RSWxDm7kpj+/MjCZgp6Xc51g==";

    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    private String alipay_public_key = "MIIBIjA74UKxt2F8VMIRKrRAAAuIMuawIsl4Ye+G12LK8P1ZLYy7ZJpgZ+Wv5nOs3DdoEazgCERj/ON8lM1KBHZOAV+TkrIcyi7cD1gfv4a1usikrUqm8/qhFvoiUfyHJFv1ymT7C4BI6aHzQ2zcUlSQPGoPl4C11tgnSkm3DlH2JZKgaIMcCOnNH+qctjNh9yIV9zat2qUiXbxmrCTtxAmiI3I+eVsUNwvwIDAQAB";

    // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
    private  String notify_url="http://**.natappfree.cc/payed/notify";

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    //同步通知,支付成功,一般跳转到成功页
    private  String return_url="http://order.gulimall.com/memberOrder.html";

    // 签名方式
    private  String sign_type = "RSA2";

    // 字符编码格式
    private  String charset = "utf-8";

    // 支付宝网关; https://openapi.alipaydev.com/gateway.do
    private  String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";

    public  String pay(PayVo vo) throws AlipayApiException {

        //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
        //1、根据支付宝的配置生成一个支付客户端
        AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
                app_id, merchant_private_key, "json",
                charset, alipay_public_key, sign_type);

        //2、创建一个支付请求 //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(return_url);
        alipayRequest.setNotifyUrl(notify_url);

        //商户订单号,商户网站订单系统中唯一订单号,必填
        String out_trade_no = vo.getOut_trade_no();
        //付款金额,必填
        String total_amount = vo.getTotal_amount();
        //订单名称,必填
        String subject = vo.getSubject();
        //商品描述,可空
        String body = vo.getBody();

        alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
                + "\"total_amount\":\""+ total_amount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        String result = alipayClient.pageExecute(alipayRequest).getBody();

        //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
        System.out.println("支付宝的响应:"+result);

        return result;

    }

3、添加“com.atguigu.gulimall.order.vo.PayVo”类,代码如下:

@Data
public class PayVo {
    private String out_trade_no; // 商户订单号 必填
    private String subject; // 订单名称 必填
    private String total_amount;  // 付款金额 必填
    private String body; // 商品描述 可空
}

4、添加配置

支付宝相关的设置
alipay.app_id=自己的APPID

5、修改gulimall-order模块的pay.html(支付页)的支付宝按钮

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

3.2、订单支付与同步通知

添加“com.atguigu.gulimall.order.web.PayWebController”类,代码如下:

@Controller
public class PayWebController {
    
    @Autowired
    private AlipayTemplate alipayTemplate;

    @Autowired
    private OrderService orderService;

    /**
     * 支付订单
     */
    @ResponseBody
    @GetMapping("/payOrder")
    public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {

//        PayVo payVo = new PayVo();
//        payVo.setBody();//订单备注
//        payVo.setOut_trade_no();//订单号
//        payVo.setSubject();//订单主题
//        payVo.setTotal_amount();//订单金额
        PayVo payVo = orderService.getOrderPay(orderSn);
        // 返回的是一个页面。将此页面直接交给浏览器就行
        String pay = alipayTemplate.pay(payVo);
        System.out.println(pay);
        return "hello";
    }
}

修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:

    /**
     * 根据订单号获取当前订单支付信息
     *
     * @param orderSn
     * @return
     */
    PayVo getOrderPay(String orderSn);

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:

      @Override
    public PayVo getOrderPay(String orderSn) {
        PayVo payVo = new PayVo();
        OrderEntity order = this.getOrderByOrderSn(orderSn);
        // 支付金额设置为两位小数,否则会报错
        BigDecimal bigDecimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
        payVo.setTotal_amount(bigDecimal.toString());
        // 商户订单号
        payVo.setOut_trade_no(order.getOrderSn());
        List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
        OrderItemEntity entity = order_sn.get(0);
        // 订单名称
        payVo.setSubject(entity.getSkuName());
        // 商品描述
        payVo.setBody(entity.getSkuAttrsVals());
        return payVo;
    }

http://order.gulimall.com/payOrder?orderSn=202012051517520571335121191551672321

运行结果

支付宝的响应:<form name="punchout_form" method="post" action="https://openapi.alipaydev.com/gateway.do?charset=utf-8&method=alipay.trade.page.pay&sign=YdraUOF%2Bu9lnoN9WVg22AQhniZXf28ffZf5V5vb7ajRtZ5I76lCZNCiH8%2BKJ0lCLLfb6PIvXXAQQFbiO9P89xou%2B11I%2FUm51ysptIsR7rzIFOiGQfSH2TpCjKIIZifPFAgZI8V7AKShdL6ejq0kcW%2FqMG0Jj14H0l1KqyfcGi6aPAc8JPJ3gXc8irUAzDkE5qNq7kzoZOjKIy%2FEv63L4lvBa8aDCRuV4dABti%2BhglYKaOj0IhDSh5BumWnrBll%2F%2FDuG1UDiXjILL5ddKGSE%2FIXPv3ZbNTneqD6OdGYuKXMDT0yEX4MiuZncrqThlJ2tMFmE5%2BLHX%2B6%2FROpoCZPL7iQ%3D%3D&version=1.0&app_id=2021000116660265&sign_type=RSA2&timestamp=2020-12-05+15%3A17%3A57&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
<input type="hidden" name="biz_content" value="{&quot;out_trade_no&quot;:&quot;202012051517520571335121191551672321&quot;,&quot;total_amount&quot;:&quot;5800.00&quot;,&quot;subject&quot;:&quot;华为 HUAWEI Mate 30 5G 麒麟990 4000万超感光徕卡影像双超级快充白色 6GB&quot;,&quot;body&quot;:&quot;颜色:白色;内存:6GB&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>

我们可以看出返回的结果是html 。所以我们直接修改这个接口,让他返回是html页面

    @ResponseBody
    @GetMapping(value = "payOrder", produces = "text/html")
    public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {

//        PayVo payVo = new PayVo();
//        payVo.setBody();//订单备注
//        payVo.setOut_trade_no();//订单号
//        payVo.setSubject();//订单主题
//        payVo.setTotal_amount();//订单金额
        PayVo payVo = orderService.getOrderPay(orderSn);
        // 返回的是一个页面。将此页面直接交给浏览器就行
        String pay = alipayTemplate.pay(payVo);
        System.out.println(pay);
        return pay;
    }

 测试

  • 1、将支付页让浏览器显示
  • 2、支付成功以后,我们要跳到用户的订单列表页

修改“com.atguigu.gulimall.order.config.AlipayTemplate”类,代码如下:

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 同步通知,支付成功,一般跳转到成功页
    private  String return_url = "http://member.gulimall.com/memberOrder";

 gulimall-member

1、添加thymeleaf模板引擎

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

2、添加订单页的html(orderList.html)

3、往虚拟机的添加订单页的静态资源(在/mydata/nginx/html/static/目录下,创建member文件夹)

4、修改静态资源访问路径

5、做登录拦截添加SpringSession依赖 

        <!--整合SpringSession完成session共享问题-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

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

 6、添加配置

spring:
  #关闭thymeleaf的缓存
  thymeleaf:
    cache: false
  #配置redis
  redis:
    host: 192.168.119.127
    port: 6379
  #配置SpringSession存储类型
  session:
    store-type: redis

7、主启动类添加SpringSession自动开启

@EnableRedisHttpSession
@EnableFeignClients(basePackages="com.atguigu.gulimall.member.feign")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallMemberApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallMemberApplication.class, args);
    }

}

8、添加“com.atguigu.gulimall.member.config.GulimallSessionConfig”类,代码如下:

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

添加登录拦截器“com.atguigu.gulimall.member.interceptor.LoginUserInterceptor”

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean status = matcher.match("/member/**", requestURI);
        if (status){
            return true;
        }
        

        MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute != null){
            loginUser.set(attribute);
            return true;
        }else {
            //没登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }

    }
}

把登录拦截器配置到spring里

添加“com.atguigu.gulimall.member.config.MemberWebConfig”类,代码如下:

@Configuration
public class MemberWebConfig implements WebMvcConfigurer {

    @Autowired
    LoginUserInterceptor loginUserInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}

在gulimall-gateway配置路由:

        - id: gulimall_member_route
          uri: lb://gulimall-member
          predicates:
            - Host=member.gulimall.com

添加域名(C:\Windows\System32\drivers\etc\hosts)

#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com
192.168.119.127 cart.gulimall.com
192.168.119.127 order.gulimall.com
192.168.119.127 member.gulimall.com

修改首页我的订单地访问路径gulimall-product (index.html)

          <li>
            <a href="http://member.gulimall.com/memberOrder.html">我的订单</a>
          </li>

找到沙箱环境里面有沙箱账号

3.3、订单列表页渲染完成

修改“com.atguigu.gulimall.member.web.MemberWebController”类,代码如下“:

@Controller
public class MemberWebController {
    
    @Autowired
    private OrderFeignService orderFeignService;

    @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";
    }
}

添加“com.atguigu.gulimall.member.feign.OrderFeignService”类,代码如下:

@FeignClient("gulimall-order")
public interface OrderFeignService {

    @PostMapping("/order/order/listWithItem")
    public R listWithItem(@RequestBody Map<String, Object> params);
}

因为订单服务做了用户登录的拦截,所以远程调用订单服务需要用户信息,我们给它共享cookies

添加“com.atguigu.gulimall.member.config.GuliFeignConfig”类,代码如下:

@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                System.out.println("RequestInterceptor线程..." + Thread.currentThread().getId());
                //1、RequestContextHolder拿到刚进来的请求
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes != null) {
                    HttpServletRequest request = attributes.getRequest();//老请求
                    if (request != null) {
                        //同步请求头数据。Cookie
                        String cookie = request.getHeader("Cookie");
                        //给新请求同步了老请求的cookie
                        requestTemplate.header("Cookie", cookie);
                        System.out.println("feign远程之前先执行RequestInterceptor.apply()");
                    }
                }
            }
        };
    }
}

远程服务:gulimall-order

修改“com.atguigu.gulimall.order.controller.OrderController”类,代码如下:

    /**
     * 分页查询当前登录用户的所有订单
     * @param params
     * @return
     */
    @PostMapping("/listWithItem")
    public R listWithItem(@RequestBody Map<String, Object> params){
        PageUtils page = orderService.queryPageWithItem(params);

        return R.ok().put("page", page);
    }

修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:

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

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:

    @Override
    public PageUtils queryPageWithItem(Map<String, Object> params) {
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>().eq("member_id", memberResponseVO.getId()).orderByDesc("id")
        );
        List<OrderEntity> order_sn = page.getRecords().stream().map(order -> {
            List<OrderItemEntity> entities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));
            order.setItemEntities(entities);
            return order;
        }).collect(Collectors.toList());
        page.setRecords(order_sn);
        return new PageUtils(page);
    }

修改OrderEntity

修改orderList.html

        <table class="table" th:each="order:${orders.page.list}">
          <tr>
            <td colspan="7" style="background:#F7F7F7" >
              <span style="color:#AAAAAA">2017-12-09 20:50:10</span>
              <span><ruby style="color:#AAAAAA">订单号:</ruby>[[${order.orderSn}]] 70207298274</span>
              <span>谷粒商城<i class="table_i"></i></span>
              <i class="table_i5 isShow"></i>
            </td>
          </tr>
          <tr class="tr" th:each="item,itemStat:${order.itemEntities}">
            <td colspan="3" style="border-right: 1px solid #ccc">
              <img style="height: 60px; width: 60px;" th:src="${item.skuPic}" alt="" class="img">
              <div>
                <p style="width: 242px; height: auto;overflow: auto">
                  [[${item.skuName}]]
                </p>
                <div><i class="table_i4"></i>找搭配</div>
              </div>
              <div style="margin-left:15px;">x[[${item.skuQuantity}]]</div>
              <div style="clear:both"></div>
            </td>
            <td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">[[${order.receiverName}]]<i><i class="table_i1"></i></i></td>
            <td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}" style="padding-left:10px;color:#AAAAB1;">
              <p style="margin-bottom:5px;">总额 ¥[[${order.payAmount}]]</p>
              <hr style="width:90%;">
              <p>在线支付</p>
            </td>
            <td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">
              <ul>
                <li style="color:#71B247;" th:if="${order.status==0}">待付款</li>
                <li style="color:#71B247;" th:if="${order.status==1}">已付款</li>
                <li style="color:#71B247;" th:if="${order.status==2}">已发货</li>
                <li style="color:#71B247;" th:if="${order.status==3}">已完成</li>
                <li style="color:#71B247;" th:if="${order.status==4}">已取消</li>
                <li style="color:#71B247;" th:if="${order.status==5}">售后中</li>
                <li style="color:#71B247;" th:if="${order.status==6}">售后完成</li>

                <li style="margin:4px 0;" class="hide"><i class="table_i2"></i>跟踪<i class="table_i3"></i>
                    <div class="hi">
                      <div class="p-tit">
                        普通快递   运单号:390085324974
                      </div>
                      <div class="hideList">
                        <ul>
                          <li>
                            [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                            的快件已签收,感谢您使用韵达快递)签收
                          </li>
                          <li>
                            [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                            的快件已签收,感谢您使用韵达快递)签收
                          </li>
                          <li>
                            [北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描
                          </li>
                          <li>
                            [北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636
                          </li>
                        </ul>
                      </div>
                    </div>
                </li>
                <li class="tdLi">订单详情</li>
              </ul>
            </td>
            <td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">
              <button>确认收货</button>
              <p style="margin:4px 0; ">取消订单</p>
              <p>催单</p>
            </td>
          </tr>
        </table>

3.4、异步通知内网穿透环境搭建

  • 订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态
  • 由于同步跳转可能由于网络问题失败,所以使用异步通知
  • 支付宝使用的是最大努力通知方案,保障数据一致性,隔一段时间会通知商户支付成功,直到返回success

1)、内网穿透设置异步通知地址

内网穿透联调流程

下面我们根据内网穿透联调的流程进行具体操作: 

  • 将外网映射到本地的order.gulimall.com:80

  • 由于回调的请求头不是order.gulimall.com,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置将/payed/notify异步通知转发至订单服务

 1、设置异步通知的地址

// 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private  String notify_url = "http://8xlc1ea491.*****.tech/payed/notify";

2、修改内网穿透:将外网映射到本地的order.gulimall.com:80

3、nginx配置访问/payed/notify异步通知转发至订单服务

配置好之后,重启nginx 

http://8xlc1ea491.52http.tech/payed/notify?name=hello 访问还是404,查看日志

上面日志显示默认以本地的方式访问所以直接访问静态资源/static/..,我们访问这个域名下的/payed路径,我们要添加这个域名,并把host改成order.gulimall.com服务。不然默认以本地的方式访问 

再次重启niginx 

 修改登录拦截器给他放行 

修改“com.atguigu.gulimall.order.interceptor.LoginUserInterceptor”类,代码如下:

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        // 该路径只是远程调用,你不需要登录拦截
        String requestURI = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean status = matcher.match("/order/order/status/**", requestURI);
        boolean payed = matcher.match("/payed/**", requestURI);
        if (status || payed) {
            return true;
        }

        MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute != null) {
            loginUser.set(attribute);
            return true;
        } else {
            // 没登录就去登录
            request.getSession().setAttribute("msg", "请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}

2)、验证签名

添加“com.atguigu.gulimall.order.listener.OrderPayedListener”类,代码如下:

@Slf4j
@RestController
public class OrderPayedListener {

    @Autowired
    private OrderService orderService;

    @Autowired
    private AlipayTemplate alipayTemplate;

    /**
     * 支付宝成功异步通知
     *
     * @param request
     * @return
     */
    @PostMapping("/payed/notify")
    public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException {
        log.info("收到支付宝异步通知******************");
        // 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
        // 获取支付宝POST过来反馈信息
        // 需要验签
        Map<String, String> params = new HashMap<>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for (String name : requestParams.keySet()) {
            String[] values = requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            // 乱码解决,这段代码在出现乱码时使用
            // valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }

        boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
                alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名

        if (signVerified){
            log.info("支付宝异步通知验签成功");
            // 修改订单状态
            orderService.handlePayResult(vo);
            return "success";
        }else {
            log.info("支付宝异步通知验签失败");
            return "error";
        }
    }

}

添加“com.atguigu.gulimall.order.vo.PayAsyncVo”类,代码如下:

@ToString
@Data
public class PayAsyncVo {

    private String gmt_create;
    private String charset;
    private String gmt_payment;
    private Date notify_time;
    private String subject;
    private String sign;
    private String buyer_id;//支付者的id
    private String body;//订单的信息
    private String invoice_amount;//支付金额
    private String version;
    private String notify_id;//通知id
    private String fund_bill_list;
    private String notify_type;//通知类型; trade_status_sync
    private String out_trade_no;//订单号
    private String total_amount;//支付的总额
    private String trade_status;//交易状态  TRADE_SUCCESS
    private String trade_no;//流水号
    private String auth_app_id;//
    private String receipt_amount;//商家收到的款
    private String point_amount;//
    private String app_id;//应用id
    private String buyer_pay_amount;//最终支付的金额
    private String sign_type;//签名类型
    private String seller_id;//商家的id

}

修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:

    /**
     * 处理支付成功返回结果
     *
     * @param vo
     */
    String handlePayResult(PayAsyncVo vo);

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:

    @Override
    public String handlePayResult(PayAsyncVo vo) {
        // 1、保存交易流水
        PaymentInfoEntity infoEntity = new PaymentInfoEntity();
        infoEntity.setAlipayTradeNo(vo.getTrade_no());
        infoEntity.setAlipayTradeNo(vo.getOut_trade_no());
        infoEntity.setPaymentStatus(vo.getTrade_status());
        infoEntity.setCallbackTime(vo.getNotify_time());
        paymentInfoService.save(infoEntity);

        // 2、修改订单状态信息
        if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")){
            //支付成功状态
            String outTradeNo = vo.getOut_trade_no();
            this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
        }
        return "success";
    }

修改“com.atguigu.gulimall.order.dao.OrderDao”类,代码如下:

@Mapper
public interface OrderDao extends BaseMapper<OrderEntity> {

    /**
     * 修改订单状态
     *
     * @param outTradeNo
     * @param code
     */
    void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);
}

OrderDao.xml

    <update id="updateOrderStatus">
        update oms_order set status = #{code} where order_sn = #{outTradeNo}
    </update>
#springMVC的日期格式化
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss

3.5、收单

  1. 订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库 存解锁了。
    1. 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
  2. 由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到订单解锁,手动调用收单。
  3. 网络阻塞问题,订单支付成功的异步通知一直不到达。
    1. 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝 此订单的状态。
  4. 其他各种问题
    1. 每天晚上闲时下载支付宝对账单,一一进行对账。

添加超时时间

十三、商城业务-秒杀服务


1、后台添加秒杀商品

1、配置网关

    - id: coupon_route
          uri: lb://gulimall-coupon
          predicates:
            - Path=/api/coupon/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

2、每日秒杀关联商品功能实现 

点击关联商品的时候,会弹出一个页面,并且F12可以看到会调用一个url请求: 

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

根据此url去完善该接口

修改“com.atguigu.gulimall.coupon.service.impl.SeckillSkuRelationServiceImpl”代码如下:

@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
        // 场次id不是null
        String promotionSessionId = (String) params.get("promotionSessionId");
        if (!StringUtils.isEmpty(promotionSessionId)) {
            queryWrapper.eq("promotion_session_id", promotionSessionId);
        }
        IPage<SeckillSkuRelationEntity> page = this.page(new Query<SeckillSkuRelationEntity>().getPage(params), queryWrapper);

        return new PageUtils(page);
    }

}

2、搭建秒杀服务环境

1、导入pom.xml依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-seckill</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-seckill</name>
    <description>秒杀</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>

    <dependencies>
        <!--以后使用redis.client作为所有分布式锁,分布式对象等功能框架-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>

        <dependency>
            <groupId>com.auguigu.gulimall</groupId>
            <artifactId>gulimall-commom</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

</project>

2、添加配置

spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.119.127

3、主启动类添加注解

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallSeckillApplication.class, args);
    }

}

3、定时任务

3.1cron 表达式

语法:秒 分 时 日 月 周 年(Spring 不支持) 

http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

特殊字符:
  • ,:枚举:
    • (cron="7,9,23 * * * * ?"):任意时刻的 7,923 秒启动这个任务;
  • -:范围:
    • (cron="7-20 * * * * ?"):任意时刻的 7-20 秒之间,每秒启动一次
  • *:任意:
    • 指定位置的任意时刻都可以
  • /:步长:
    • (cron="7/5 * * * * ?"):第 7 秒启动,每 5 秒一次;
    • (cron="*/5 * * * * ?"):任意秒启动,每 5 秒一次;
  • ?:(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使?
    • (cron="* * * 1 * ?"):每月的 1 号,启动这个任务;
  • L:(出现在日和周的位置):last:最后一个
    • (cron="* * * ? * 3L"):每月的最后一个周二
  • W:Work Day:工作日
    • (cron="* * * W * ?"):每个月的工作日触发
    • (cron="* * * LW * ?"):每个月的最后一个工作日触发
  • #:第几个
    • (cron="* * * ? * 5#2"):每个月的第 2 个周 4 

3.2、SpringBoot 整合定时任务 

springboot整合定时任务流程:

1、@EnableScheduling 开启定时任务

2、@Scheduled 开启一个定时任务

3、自动配置类 TaskSchedulingAutoConfiguration

异步任务 

1、@EnableAsync 开启异步任务功能

2、@Async 给希望异步执行的方法上标注

3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在TaskExecutionProperties

 添加“com.atguigu.gulimall.seckill.scheduled.HelloSchedule”类,代码如下:

@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {

    /**
     * 1、Spring中6位组成,不允许7位d的年
     * 2、周的位置,1-7代表周一到周日
     * 3、定时任务不应该阻塞。默认是阻塞的
     *      1)、可以让业务运行以异步的方式,自己提交到线程池
     *      2)、支持定时任务线程池;设置TaskSchedulingProperties;
     *              spring.task.scheduling.pool.size=5
     *      3)、让定时任务异步执行
     *          异步任务
     *
     *      解决:使用异步任务来完成定时任务不阻塞的功能
     */
    @Async
    @Scheduled(cron = "*/5 * * * * ?")
    public void hello() throws InterruptedException {
        log.info("hello......");
        Thread.sleep(3000);
    }
}

 配置定时任务参数

spring.task.execution.pool.core-size=20
spring.task.execution.pool.max-size=50

4、秒杀商品上架

4.1、秒杀商品上架思路 

  • 项目独立部署,独立秒杀模块gulimall-seckill
  • 使用定时任务每天三点上架最新秒杀商品,削减高峰期压力

4.2、秒杀商品上架流程

4.3、存储模型设计

1、查询秒杀活动场次和sku信息的存储模型

添加“com.atguigu.gulimall.seckill.vo.SeckillSessionWithSkus”类,代码如下:

package com.atguigu.gulimall.seckill.vo;

import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
public class SeckillSessionWithSkus {
    private Long id;
    /**
     * 场次名称
     */
    private String name;
    /**
     * 每日开始时间
     */
    private Date startTime;
    /**
     * 每日结束时间
     */
    private Date endTime;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 创建时间
     */
    private Date createTime;

    private List<SeckillSkuVo> relationEntities;

}

2、查询秒杀活动商品关联的存储模型

添加“com.atguigu.gulimall.seckill.vo.SeckillSkuVo”类,代码如下:

package com.atguigu.gulimall.seckill.vo;

import lombok.Data;

import java.math.BigDecimal;

@Data
public class SeckillSkuVo {
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;

}

3、查询商品信息的存储模型 

 添加“com.atguigu.gulimall.seckill.vo.SkuInfoVo”类,代码如下:

package com.atguigu.gulimall.seckill.vo;

import lombok.Data;

import java.math.BigDecimal;

@Data
public class SkuInfoVo {

    private Long skuId;
    /**
     * spuId
     */
    private Long spuId;
    /**
     * sku名称
     */
    private String skuName;
    /**
     * sku介绍描述
     */
    private String skuDesc;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     * 默认图片
     */
    private String skuDefaultImg;
    /**
     * 标题
     */
    private String skuTitle;
    /**
     * 副标题
     */
    private String skuSubtitle;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 销量
     */
    private Long saleCount;
}

4、缓存获得秒杀活动场次和sku信息的存储模型

添加"com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo"类,代码如下:

package com.atguigu.gulimall.seckill.to;

import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;

import java.math.BigDecimal;

@Data
public class SeckillSkuRedisTo {
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    //以上都为SeckillSkuRelationEntity的属性

    //skuInfo
    private SkuInfoVo skuInfo;

    //当前商品秒杀的开始时间
    private Long startTime;

    //当前商品秒杀的结束时间
    private Long endTime;

    //当前商品秒杀的随机码
    private String randomCode;
}

4.4、定时上架

配置定时任务

添加“com.atguigu.gulimall.seckill.config.ScheduledConfig”类,代码如下:

@EnableAsync // 开启对异步的支持,防止定时任务之间相互阻塞
@EnableScheduling // 开启对定时任务的支持
@Configuration
public class ScheduledConfig {
}

每天凌晨三点远程调用coupon服务上架最近三天的秒杀商品

添加“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,代码如下

@Slf4j
@Component
public class SeckillSkuScheduled {

    @Autowired
    private SeckillService seckillService;

    /**
     * TODO 幂等性处理
     * 上架最近三天的秒杀商品
     */
    @Scheduled(cron = "0 0 3 * * ?")
    public void uploadSeckillSkuLatest3Days() {
        // 重复上架无需处理
        log.info("上架秒杀的信息......");
        seckillService.uploadSeckillSkuLatest3Days();
    }
}

添加“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下

public interface SeckillService {
    void uploadSeckillSkuLatest3Days();
}

添加“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下

@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;
    
    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:"; //秒杀活动信息

    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:"; // 秒杀商品信息

    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码

    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 1、扫描最近三天需要参与秒杀的活动的场次和sku信息
        R session = couponFeignService.getLasts3DaySession();
        if (session.getCode() == 0){
            // 上架商品
            List<SeckillSessionWithSkus> data = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
            });
            // 2、缓存到redis

            // 2.1、缓存活动信息
            saveSessionInfos(data);

            // 2.2、缓存获得关联商品信息
            saveSessionSkuInfos(data);
        }
    }
   
}
4.4.1、获取最近三天的秒杀信息

获取最近三天的秒杀场次信息,再通过秒杀场次id查询对应的商品信息

添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:

@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    @GetMapping("/coupon/seckillsession/lasts3DaySession")
    R getLasts3DaySession();
}

gulimall-coupon

修改“com.atguigu.gulimall.coupon.controller.SeckillSessionController”类,代码如下:

    /**
     * 获取最近3天的秒杀商品
     */
    @GetMapping("/lasts3DaySession")
    public R getLasts3DaySession(){
        List<SeckillSessionEntity> session = seckillSessionService.getLasts3DaySession();
        return R.ok().setData(session);
    }

添加“com.atguigu.gulimall.coupon.service.SeckillSessionService”类,代码如下:

    /**
     * 获取最近3天的秒杀商品
     *
     * @return
     */
    List<SeckillSessionEntity> getLasts3DaySession();

添加“com.atguigu.gulimall.coupon.service.impl.SeckillSessionServiceImpl”类,代码如下:

    @Override
    public List<SeckillSessionEntity> getLasts3DaySession() {
        // 计算最近三天
        List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
        if (!CollectionUtils.isEmpty(list)) {
            return list.stream().map(session -> {
                Long id = session.getId();
                List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                session.setRelationEntities(relationEntities);
                return session;
            }).collect(Collectors.toList());
        }
        return null;
    }
    
    /**
     * 起始时间
     *
     * @return
     */
    private String startTime() {
        LocalDate now = LocalDate.now();
        LocalTime time = LocalTime.MIN;
        LocalDateTime start = LocalDateTime.of(now, time);
        String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }

    /**
     * 结束时间
     *
     * @return
     */
    private String endTime() {
        LocalDate now = LocalDate.now();
        LocalDate localDate = now.plusDays(2);
        LocalTime time = LocalTime.MIN;
        LocalDateTime end = LocalDateTime.of(localDate, time);
        String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }
4.4.2、在redis中缓存秒杀活动信息
    /**
     * 缓存秒杀活动信息
     *
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            if (!hasKey) {
                List<String> collect = session.getRelationEntities()
                        .stream()
                        .map(item -> item.getPromotionSessionId().toString() + "_" + item.getSkuId().toString())
                        .collect(Collectors.toList());
                System.out.println("saveSessionInfos------------------------" + collect);
                // 缓存活动信息(list操作)
                stringRedisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }
4.4.3、在redis中缓存获得关联秒杀活动的商品信息
    /**
     * 缓存获得关联秒杀的商品信息
     *
     * @param sessions
     */
    private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions) {
        // 准备hash操作
        BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        sessions.stream().forEach(session -> {
            session.getRelationEntities().stream().forEach(seckillSkuVo -> {
                if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())) {
                    // 缓存商品
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();

                    // 1、sku的基本信息
                    R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (0 == r.getCode()) {
                        SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        redisTo.setSkuInfo(skuInfo);
                    }

                    // 2、sku的秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo, redisTo);

                    // 3、设置当前商品的秒杀时间信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());

                    // 4、随机码
                    String token = UUID.randomUUID().toString().replace("_", "");
                    redisTo.setRandomCode(token);
                    
                    // 如果当前这个场次的商品的库存信息已经上架就不需要上架
                    // 5、使用库存作为分布式信号量 ==》限流
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 5.1、商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                    String jsonString = JSON.toJSONString(redisTo);
                    log.info("saveSessionSkuInfos------------------------" + jsonString);
                    ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);
                }
            });
        });
    }

添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:

@FeignClient("gulimall-product")
public interface ProductFeignService {

    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);
}

4.5、幂等性保证

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

  • 由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法
    • 分布式锁:锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态

修改“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,代码如下:

@Slf4j
@Component
public class SeckillSkuScheduled {

    @Autowired
    private SeckillService seckillService;

    @Autowired
    private RedissonClient redissonClient;

    private final String upload_lock = "seckill:upload:lock";

    /**
     * 上架最近3天的秒杀商品
     * 幂等性处理
     */
    @Scheduled(cron = "*/3 0 0 * * ?")
    public void uploadSeckillSkuLatest3Days() {
        // 重复上架无需处理
        log.info("上架秒杀的信息......");
        // 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态
        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            seckillService.uploadSeckillSkuLatest3Days();
        } finally {
            lock.unlock();
        }

    }
}

5、获取当前秒杀商品

5.1、获取到当前可以参加秒杀的商品信息

添加“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下

@Controller
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    /**
     * 获取到当前可以参加秒杀的商品信息
     *
     * @return
     */
    @ResponseBody
    @GetMapping(value = "/getCurrentSeckillSkus")
    public R getCurrentSeckillSkus() {
        List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
        return R.ok().setData(vos);
    }
}

添加“com.atguigu.gulimall.seckill.service.SeckillService”类:代码如下:

  List<SeckillSkuRedisTo> getCurrentSeckillSkus();

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

       @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        // 获取秒杀活动的所有key
        Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        long currentTime = System.currentTimeMillis();
        for (String key : keys) {
            String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
            String[] split = replace.split("_");
            long startTime = Long.parseLong(split[0]);
            long endTime = Long.parseLong(split[1]);
            // 当前秒杀活动处于有效期内
            if (currentTime > startTime && currentTime < endTime) {
                // 获取这个秒杀场次的所有商品信息
                List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                assert range != null;
                List<String> strings = hashOps.multiGet(range);
                if (!CollectionUtils.isEmpty(strings)) {
                    return strings.stream().map(item -> JSON.parseObject(item, SeckillSkuRedisTo.class))
                            .collect(Collectors.toList());
                }
                break;
            }
        }
        return null;
    }

5.2、首页获取并拼装数据

1、配置网关

        - id: gulimall_seckill_route
          uri: lb://gulimall-seckill
          predicates:
            - Host=seckill.gulimall.com

2、配置域名

#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com
192.168.119.127 cart.gulimall.com
192.168.119.127 order.gulimall.com
192.168.119.127 member.gulimall.com
192.168.119.127 seckill.gulimall.com

3、修改gulimall-product模块的index.html页面,代码如下: 

        <div class="swiper-container swiper_section_second_list_left">
            <div class="swiper-wrapper">
                <div class="swiper-slide">
                    <!-- 动态拼装秒杀商品信息 -->
                    <ul id="seckillSkuContent"></ul>
                </div>
<script type="text/javascript">
    /**
     * 检索商品
     */
    function search() {
        var keyword = $("#searchText").val()
        window.location.href = "http://search.gulimall.com/list.html?keyword=" + keyword;
    }

    /**
     * 获取秒杀当前活动商品信息
     */
    $.get("http://seckill.gulimall.com/getCurrentSeckillSkus", function (res) {
        if (res.data.length > 0) {
            res.data.forEach(function (item) {
                $("<li onclick='toDetail(" + item.skuId + ")'></li>")
                    .append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />"))
                    .append($("<p>" + item.skuInfo.skuTitle + "</p>"))
                    .append($("<span>" + item.seckillPrice + "</span>"))
                    .append($("<s>" + item.skuInfo.price + "</s>"))
                    .appendTo("#seckillSkuContent");
            })
        }
    })

    /**
     * 商品详情
     * @param skuId
     */
    function toDetail(skuId) {
        location.href = "http://item.gulimall.com/" + skuId + ".html";
    }
</script>

 4、首页效果展示

6、商品详情页获取当前商品的秒杀信息

修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

    @Override
    public SkuItemVo item(Long skuId) {
        SkuItemVo skuItemVo = new SkuItemVo();
        // 使用异步编排
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            // 1、sku基本信息获取    pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);

        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            // 2、sku的图片信息      pms_sku_images
            List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);

        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            // 3、获取spu的销售属性组合
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);

        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
            // 4、获取spu的介绍 pms_spu_info_desc
            SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesc(desc);
        }, executor);

        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            // 5、获取spu的规格参数信息
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);

        CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
            // 6、查询当前sku是否参与秒杀优惠
            R r = seckillFeignService.getSkuSecKillInfo(skuId);
            if (0 == r.getCode()) {
                SeckillInfoVo skillInfo = r.getData(new TypeReference<SeckillInfoVo>() {
                });
                skuItemVo.setSeckillInfo(skillInfo);
            }
        }, executor);
        // 等待所有任务执行完成
        try {
            CompletableFuture.allOf(saleAttrFuture, imageFuture, descFuture, baseAttrFuture, seckillFuture).get();
        } catch (InterruptedException e) {
            log.error("1等待所有任务执行完成异常{}", e);
        } catch (ExecutionException e) {
            log.error("2等待所有任务执行完成异常{}", e);
        }
        return skuItemVo;
    }

添加“com.atguigu.gulimall.product.vo.SeckillInfoVo”类,代码如下:

@Data
public class SeckillInfoVo {

    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;

    /**
     * 当前商品秒杀的开始时间
     */
    private Long startTime;

    /**
     * 当前商品秒杀的结束时间
     */
    private Long endTime;

    /**
     * 当前商品秒杀的随机码
     */
    private String randomCode;
}

修改“com.atguigu.gulimall.product.vo.SkuItemVo”类,代码如下:

远程调用gulimall-seckill

添加“com.atguigu.gulimall.product.feign.SeckillFeignService”类,代码如下:

@FeignClient("gulimall-seckill")
public interface SeckillFeignService {

    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}

修改“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

    /**
     * 获取秒杀商品的详情信息
     */
    @ResponseBody
    @GetMapping("/sku/seckill/{skuId}")
    public R getSkuSecKillInfo(@PathVariable("skuId") Long skuId){
        SeckillSkuRedisTo to = seckillService.getSkuSecKillInfo(skuId);
        return R.ok().setData(to);
    }

修改“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下:

SeckillSkuRedisTo getSkuSecKillInfo(Long skuId);

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

    @Override
    public SeckillSkuRedisTo getSkuSecKillInfo(Long skuId) {
        // 1、获取所有需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (null != keys){
            // 1_10(正则表达式)
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                // 匹配场次商品id
                if (Pattern.matches(regx, key)){
                    String json = hashOps.get(key);
                    SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);

                    // 随机码
                    long current = new Date().getTime();
                    // 不在秒杀时间范围内的随机码置空,不返回
                    if (current < skuRedisTo.getStartTime() || current > skuRedisTo.getEndTime()){
                        skuRedisTo.setRandomCode(null);
                    }
                    return skuRedisTo;
                }

            }
        }
        return null;
    }

修改gulimall-product模块的item.html页面,代码如下:

                <div class="box-summary clear">
                    <ul>
                        <li>京东价</li>
                        <li>
                            <span>¥</span>
                            <span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>
                        </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="${item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
										秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
									</span>

                        </li>
                        <li>
                            <a href="/static/item/">
                                预约说明
                            </a>
                        </li>
                    </ul>
                </div>

 详情页效果展示:

7、登录检查

1、pom引入SpringSession依赖和redis

        <!--整合SpringSession完成session共享问题-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

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

2、在配置文件中添加SpringSession的保存方式

#SpringSession保存方式
spring.session.store-type=redis

3、SpringSession的配置

添加“com.atguigu.gulimall.seckill.config.GulimallSessionConfig”类,代码如下:

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

4、主启动类开启RedisHttpSession这个功能

@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallSeckillApplication.class, args);
    }

}

添加用户登录拦截器

添加“com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor”类,代码如下:

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean match = matcher.match("/kill", requestURI);
        // 如果是秒杀,需要判断是否登录,其他路径直接放行不需要判断
        if (match) {
            MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
            if (attribute != null){
                loginUser.set(attribute);
                return true;
            }else {
                //没登录就去登录
                request.getSession().setAttribute("msg","请先进行登录");
                response.sendRedirect("http://auth.gulimall.com/login.html");
                return false;
            }
        }
        return true;
    }
}
  • 把拦截器配置到spring中,否则拦截器不生效。
  • 添加addInterceptors表示当前项目的所有请求都要讲过这个拦截请求

 添加“com.atguigu.gulimall.seckill.config.SeckillWebConfig”类,代码如下:

@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {

    @Autowired
    LoginUserInterceptor loginUserInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}

修改item.html

							<div class="box-btns-two" th:if="${item.seckillInfo != null && (item.seckillInfo.startTime <= #dates.createNow().getTime() && #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 || (item.seckillInfo.startTime > #dates.createNow().getTime() || #dates.createNow().getTime() > item.seckillInfo.endTime)}">
								<a href="#" id="addToCart" th:attr="skuId=${item.info.skuId}">
									加入购物车
								</a>
							</div>
  • 前端要考虑秒杀系统设计的限流思想
  • 在进行立即抢购之前,前端先进行判断是否登录
    /**
     * 立即抢购
     */
    $("#seckillA").click(function () {
        var isLogin = [[${session.loginUser != null}]];//true
        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;
    });

8、秒杀系统设计

8.1、秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)+ 独立部署。
限流方式:
  1. 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
  2. nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
  3. 网关限流,限流的过滤器
  4. 代码中使用分布式信号量
  5. rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。

8.2、秒杀架构

秒杀架构思路

  • 项目独立部署,独立秒杀模块gulimall-seckill
  • 使用定时任务每天三点上架最新秒杀商品,削减高峰期压力
  • 秒杀链接加密,为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口
  • 库存预热,先从数据库中扣除一部分库存以redisson信号量的形式存储在redis中
  • 队列削峰,秒杀成功后立即返回,然后以发送消息的形式创建订单

 秒杀架构图 

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

 8.4、秒杀流程

  •  秒杀流程图一

  • 秒杀流程图二

 我们使用秒杀流程图二来实现功能  

8.5、代码实现

8.5.1、秒杀接口
  • 点击立即抢购时,会发送请求

  • 秒杀请求会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单

添加“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

    @GetMapping("/kill")
    public R seckill(@RequestParam("killId") String killId,
                     @RequestParam("key") String key,
                     @RequestParam("num") Integer num){
        // 1、判断是否登录(登录拦截器已经自动处理)

        String orderSn = seckillService.kill(killId, key, num);
        return R.ok().setData(orderSn);
    }

修改“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下:

    /**
     * 秒杀
     *
     * @param killId 秒杀商品id
     * @param key 随机码
     * @param num 数量
     * @return
     */
    String kill(String killId, String key, Integer num);

使用队列削峰 做流量削峰

引入rabbitMQ依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

在配置文件中添加rabbitMQ的配置 

#RabbitMQ的地址
spring.rabbitmq.host=192.168.119.127
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/

配置RabbitMQ(消息确认机制暂未配置)

添加“com.atguigu.gulimall.seckill.config.MyRabbitConfig”类,代码如下:

@Configuration
public class MyRabbitConfig {
    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

}

主启动类不用启动//@EnableRabbit 不用监听RabbitMQ, 因为我们只用来发送消息,不接收消息

重要

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

    // TODO 上架秒杀商品的时候,每一个数据都有一个过期时间。
    // TODO 秒杀后续的流程,简化了 收货地址等信息。
    @Override
    public String kill(String killId, String key, Integer num) {
        long s1 = System.currentTimeMillis();
        // 0、从拦截器获取用户信息
        MemberResponseVO repsVo = LoginUserInterceptor.loginUser.get();
        // 1、获取当前商品的详细信息
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if (!StringUtils.isEmpty(json)){
            SeckillSkuRedisTo redis = JSON.parseObject(json, SeckillSkuRedisTo.class);
            // 2、校验合法性
            Long startTime = redis.getStartTime();
            Long endTime = redis.getEndTime();
            long current = new Date().getTime();
            long ttl = endTime - startTime; // 场次存活时间
            // 2.1、校验时间的合法性
            if (current >= startTime && current <= endTime){
                // 2.2、校验随机码和商品id
                String randomCode = redis.getRandomCode();
                String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                if (randomCode.equals(key) && skuId.equals(killId)){
                    // 2.3、验证购物的数量是否合理
                    if (num <= redis.getSeckillLimit()){
                        // 2.4、验证这个人是否购买过。幂等性处理。如果只要秒杀成功,就去占位  userId_sessionId_skillId
                        // SETNX
                        String redisKey = repsVo.getId() + "_" + skuId;
                        // 2.4.1、自动过期--通过在redis中使用 用户id_skuId 来占位看是否买过
                        Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (ifAbsent){
                            // 2.5、占位成功,说明该用户未秒杀过该商品,则继续尝试获取库存信号量
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            boolean b = semaphore.tryAcquire(num);
                            if (b){
                                // 秒杀成功
                                // 2.6、快速下单发送MQ消息 10ms
                                String orderSn = IdWorker.getTimeId();
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(orderSn);
                                orderTo.setMemberId(repsVo.getId());
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(redis.getSeckillPrice());
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
                                long s2 = System.currentTimeMillis();
                                log.info("耗时..." + (s2-s1));
                                return orderSn;
                            }
                            return null;

                        }else {
                            // 2.4.2、说明已经买过
                            return null;
                        }

                    }

                }else {
                    return null;
                }
            }else {

                return null;
            }

        }
        return null;
    }

新建“com.atguigu.common.to.mq.SeckillOrderTo”类,代码如下:

@Data
public class SeckillOrderTo {

    /**
     * 订单号
     */
    private String orderSn;

    /**
     * 活动场次id
     */
    private Long promotionSessionId;

    /**
     * 商品id
     */
    private Long skuId;

    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;

    /**
     * 购买数量
     */
    private Integer num;

    /**
     * 会员id
     */
    private Long memberId;

}
8.5.2、创建订单

gulimall-order

1、创建秒杀队列,并绑定队列到订单交换机

修改“com.atguigu.gulimall.order.config.MyMQConfig”类,代码如下:

    /**
     * 订单秒杀队列
     */
    @Bean
    public Queue orderSeckillOrderQueue() {
        return new Queue("order.seckill.order.queue", true, false, false);
    }

    /**
     * 绑定订单秒杀队列
     */
    @Bean
    public Binding orderSeckillOrderQueueBinding() {
        return new Binding("order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);
    }

2、监听消息队列

添加“com.atguigu.gulimall.order.listener.OrderSeckillListener”类,代码如下:

@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {

    @Autowired
    private OrderService orderService;

    @RabbitHandler
    public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
        try {
            log.info("准备创建秒杀单的详细信息。。。");
            orderService.createSeckillOrder(seckillOrder);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 修改失败 拒绝消息 使消息重新入队
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }
}

3、创建秒杀订单 

添加“com.atguigu.gulimall.order.service.OrderService”类,代码如下:

    /**
     * 创建秒杀订单
     * 
     * @param seckillOrder
     */
    void createSeckillOrder(SeckillOrderTo seckillOrder);

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下: 

    @Override
    public void createSeckillOrder(SeckillOrderTo seckillOrder) {
        // TODO 1、保存订单信息
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(seckillOrder.getOrderSn());
        orderEntity.setMemberId(seckillOrder.getMemberId());
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));
        orderEntity.setPayAmount(multiply);
        this.save(orderEntity);
        // TODO 2、保存订单项信息
        OrderItemEntity entity = new OrderItemEntity();
        entity.setOrderSn(seckillOrder.getOrderSn());
        entity.setRealAmount(multiply);
        //TODO 3、获取当前sku的详细信息进行设置
        entity.setSkuQuantity(seckillOrder.getNum());

        orderItemService.save(entity);
    }

8.5.3、秒杀页面完成

把gulimall-cart服务的成功页面放到gulimall-seckill服务里

修改里面的静态资源路径,我们借用购物车的资源,替换如下:

引入thymeleaf依赖

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

在配置里关闭thymeleaf缓存

#关闭缓存
spring.thymeleaf.cache=false

修改“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

    @GetMapping("/kill")
    public String seckill(@RequestParam("killId") String killId,
                     @RequestParam("key") String key,
                     @RequestParam("num") Integer num,
                     Model model){
        // 1、判断是否登录(登录拦截器已经自动处理)

        String orderSn = seckillService.kill(killId, key, num);
        model.addAttribute("orderSn", orderSn);
        return "success";
    }
<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>正在准备订单数据,10s以后自动跳转支付 <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>

十四、Sentinel


1、基本概念 

1.1、熔断降级限流

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

1.2Sentinel 简介

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

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

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

只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来 。大部分情况下,可以使用方法签名,URL ,甚至服务名称作为资源名来标示资源。
  • 规则
围绕资源的实时状态设定的规则,可以包括 流量控制规则 熔断降级规则 以及 系统保护规 。所有规则可以动态实时调整。

1.3Hystrix Sentinel 比较

2、整合SpringBoot

1、整合sentinel流程:

      1)、导入依赖 spring-cloud-starter-alibaba-sentinel

      2)、下载Sentinel的控制台

      3)、配置sentinel控制台地址信息

      4)、在控制台调整参数【默认所有的流控设置保存在项目内存中,重启失效】

2、每一个微服务导入信息审计模块spring-boot-starter-actuator

并配置management.endpoints.web.exposure.include=* (暴露Sentinel的信息)

3、自定义Sentinel的流控返回数据

gulimall-common模块的pom.xml文件添加sentinel熔断、限流依赖 

        <!--sentinel熔断、限流-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

gulimall-seckill模块的pom.xml文件添加统计审计信息依赖 

        <!--统计审计信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

 找到项目里sentinel对应的版本

去官网下载该版本的控制台

https://github.com/alibaba/Sentinel/tags

java -jar sentinel-dashboard-1.8.6.jar --server.port=8333

打开http://localhost:8333

用户名:sentinel

密码:sentinel

在每个服务配置里配置Sentinel

#Sentinel
#Sentinel控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8333
#Sentinel传输端口:默认 8719,假如被占用了会自动从 8719 开始依次 +1 扫描,直至找到未被占用的端口
spring.cloud.sentinel.transport.port=8719
#暴露的 endpoint 路径为 /actuator/sentinel
#Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、
#当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。
management.endpoints.web.exposure.include=*

3、自定义流控响应

 添加“com.atguigu.gulimall.seckill.config.SeckillSentinelConfig”类,代码如下:

@Configuration
public class SeckillSentinelConfig implements BlockExceptionHandler {
    /**
     * 2.2.0以后的版本实现的是BlockExceptionHandler;以前的版本实现的是WebCallbackManager
     * @param httpServletRequest
     * @param httpServletResponse
     * @param e
     * @throws Exception
     */
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        R error = R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(), BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(error));
    }

    /**
     * 因为版本冲突导致无法引入 WebCallbackManager
     */
//    public SeckillSentinelConfig() {
//        WebCallbackManager.setUrlBlockHandler((request, response, ex) -> {
//            R error = R.error(BizCodeEnum.TOO_MANY_REQUESTS_EXCEPTION.getCode(), BizCodeEnum.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
//            response.setCharacterEncoding("UTF-8");
//            response.setContentType("application/json");
//            response.getWriter().write(JSON.toJSONString(error));
//        });
//    }
}

http://seckill.gulimall.com/getCurrentSeckillSkus

4、Sentinel全服务引入

  • 每个微服务引入actuator依赖
       <!--统计审计信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
  • 每个配置文件配置以下参数
#Sentinel
#Sentinel控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8333
#Sentinel传输端口:默认 8719,假如被占用了会自动从 8719 开始依次 +1 扫描,直至找到未被占用的端口
spring.cloud.sentinel.transport.port=8719
#暴露的 endpoint 路径为 /actuator/sentinel
#Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、
#当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。
management.endpoints.web.exposure.include=*

5、feign熔断降级

https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5

什么是熔断降级

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。 

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。 

注意:本文档针对 Sentinel 1.8.0 及以上版本。1.8.0 版本对熔断降级特性进行了全新的改进升级,请使用最新版本以更好地利用熔断降级的能力。 

熔断降级设计理念 

在限制的手段上, Sentinel Hystrix 采取了完全不一样的方法。
Hystrix 通过线程池隔离的方式,来对依赖(在 Sentinel 的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。
Sentinel 对这个问题采取了两种手段 :
  • 通过并发线程数进行限制
和资源池隔离的方法不同, Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其
它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个
资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步
堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积
的线程完成任务后才开始继续接收请求。
  • 通过响应时间对资源进行降级
除了对并发线程数进行控制以外, Sentinel 还可以通过响应时间来快速降级不稳定的资源。
当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的
时间窗口之后才重新恢复。

熔断策略 

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 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

整合测试:

https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/se ntinel-example/sentinel-feign-example/readme-zh.md

默认情况下,sentinel是不会对feign进行监控的,需要开启配置

在gulimall-product类配置文件添加配置

#sentinel是不会对feign进行监控的,需要开启配置
feign.sentinel.enabled=true

5.1、fegin的熔断

使用Sentinel来保护feign远程调用:熔断。

  1. 调用方的熔断保护开启  feign.sentinel.enabled=true
  2. 调用方手动指定远程服务的降级策略。远程服务被降级处理,触发我们的熔断回调方法
  3. 超大浏览的时候,必须牺牲一些远程服务。在服务的提供方(远程服务)指定降级策略; 提供方是在运行。但是不运行自己的业务逻辑。返回的是默认的降级数据(限流的数据)
  • gulimall-product服务远程调用gulimall-seckill服务。调用前直接把gulimall-seckill服务宕机,以前不使用sentinel的熔断的时候直接调用会报错。
  • 现在使用sentinel的熔断保护机制,远程调用失败,我们会在调用方实现远程调用类,自定义返回信息给页面。
  • 后台配置熔断策略

  • 定义FeignClient及其降级配置

修改“com.atguigu.gulimall.product.feign.SeckillFeignService”类,代码如下:

@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {

    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}
  • 自定义--远程调用失败具体的fallback 实现

添加“com.atguigu.gulimall.product.feign.fallback.SeckillFeignServiceFallBack”类,代码如下:

@Slf4j
@Component
public class SeckillFeignServiceFallBack implements SeckillFeignService {
    @Override
    public R getSkuSecKillInfo(Long skuId) {
        log.info("熔断方法调用.....getSkuSecKillInfo");
        return R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(),BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
    }
}

6、自定义受保护资源

https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-examples/sentinel-example/sentinel-core-example/readme-zh.md 注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。示例: 

Entry entry = null;
try {
  entry = SphU.entry(key, EntryType.IN, key);

  // Write your biz code here.
  // <<BIZ CODE>>
} catch (Throwable t) {
  if (!BlockException.isBlockException(t)) {
    Tracer.trace(t);
  }
} finally {
  if (entry != null) {
    entry.exit();
  }
}

开源整合模块,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource 注解会自动统计业务异常,无需手动调用。

熔断降级规则说明 

熔断降级规则(DegradeRule)包含下面几个重要的属性: 

 熔断器事件监听

Sentinel 支持注册自定义的事件监听器监听熔断器状态变换事件(state change event)。示例: 

EventObserverRegistry.getInstance().addStateChangeObserver("logging",
    (prevState, newState, rule, snapshotValue) -> {
        if (newState == State.OPEN) {
            // 变换至 OPEN state 时会携带触发时的值
            System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
                TimeUtil.currentTimeMillis(), snapshotValue));
        } else {
            System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
                TimeUtil.currentTimeMillis()));
        }
    });

 自定义受保护的资源方法:

1)、代码:

       try(Entry entry = SphU.entry("seckillSkus")){

             // 业务逻辑

       }catch(Exception e){

       }

2)、基于注解

       @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")

       无论1.2一定要配置被限流以后的默认返回

       url请求可以设置统一返回 SeckillSentinelConfig

6.1、基于代码的限流

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下: 

    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        try (Entry entry = SphU.entry("seckillSkus")) {
            // 获取秒杀活动的所有key
            Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
            long currentTime = System.currentTimeMillis();
            for (String key : keys) {
                String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
                String[] split = replace.split("_");
                long startTime = Long.parseLong(split[0]);
                long endTime = Long.parseLong(split[1]);
                // 当前秒杀活动处于有效期内
                if (currentTime > startTime && currentTime < endTime) {
                    // 获取这个秒杀场次的所有商品信息
                    List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                    assert range != null;
                    List<String> strings = hashOps.multiGet(range);
                    if (!CollectionUtils.isEmpty(strings)) {
                        return strings.stream().map(item -> JSON.parseObject(item, SeckillSkuRedisTo.class))
                                .collect(Collectors.toList());
                    }
                    break;
                }
            }
        } catch (BlockException e) {
            log.error("资源被限流{}", e.getMessage());
        }
        return null;
    }

 

6.2、基于注解的自定义限流 

    public List<SeckillSkuRedisTo> blockHandler(BlockException e){
        log.error("getCurrentSeckillSkusResource被限流了。。。。");
        return null;
    }

    // blockHandler 函数会在原方法被限流/降级/系统保护的时候调用,而fallback函数会针对所有类型的异常
    //基于注解的限流
    @SentinelResource(value = "getCurrentSeckillSkusResource", blockHandler = "blockHandler")
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        try (Entry entry = SphU.entry("seckillSkus")) {
            // 获取秒杀活动的所有key
            Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
            long currentTime = System.currentTimeMillis();
            for (String key : keys) {
                String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
                String[] split = replace.split("_");
                long startTime = Long.parseLong(split[0]);
                long endTime = Long.parseLong(split[1]);
                // 当前秒杀活动处于有效期内
                if (currentTime > startTime && currentTime < endTime) {
                    // 获取这个秒杀场次的所有商品信息
                    List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                    assert range != null;
                    List<String> strings = hashOps.multiGet(range);
                    if (!CollectionUtils.isEmpty(strings)) {
                        return strings.stream().map(item -> JSON.parseObject(item, SeckillSkuRedisTo.class))
                                .collect(Collectors.toList());
                    }
                    break;
                }
            }
        } catch (BlockException e) {
            log.error("资源被限流{}", e.getMessage());
        }
        return null;
    }

 

7、网关流控 

如果能在网关层就进行流控,可以避免请求流入业务,减小服务压力

Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。 

Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:

  • GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
  • ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。

其中网关限流规则 GatewayFlowRule 的字段解释如下:

  • resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
  • resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。
  • grade:限流指标维度,同限流规则的 grade 字段。
  • count:限流阈值
  • intervalSec:统计时间窗口,单位是秒,默认是 1 秒。
  • controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
  • burst:应对突发请求时额外允许的请求数目。
  • maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
  • paramItem:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:
    • parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
    • fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。
    • pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
    • matchStrategy:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)

用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过 GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。

Spring Cloud Gateway

从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:

  • route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
  • 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组

使用时需引入以下模块(以 Maven 为例)

gulimall-gateway引入依赖

<!-- 引入sentinel网关限流 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>

注意引入的依赖要和gulimall-common的pom里的阿里巴巴版本一致 

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

参数解释:

间隔就是:当每秒的请求超过QPS值,启动间隔。2分钟内所有调用这个接口直接返回。不去调用业务代码

自定义返回数据信息 

您可以在 GatewayCallbackManager 注册回调进行定制:

  • setBlockHandler:注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为 BlockRequestHandler。默认实现为 DefaultBlockRequestHandler,当被限流时会返回类似于下面的错误信息:Blocked by Sentinel: FlowException

注意

  • Sentinel 网关流控默认的粒度是 route 维度以及自定义 API 分组维度,默认不支持 URL 粒度。若通过 Spring Cloud Alibaba 接入,请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。
  • 若使用 Spring Cloud Alibaba Sentinel 数据源模块,需要注意网关流控规则数据源类型是 gw-flow,若将网关流控规则数据源指定为 flow 则不生效。

 新增“com.atguigu.gulimall.gateway.config.SentinelGateWayConfig”类,代码如下: 

@Configuration
public class SentinelGateWayConfig {

    // 响应式编程
    public SentinelGateWayConfig() {
        GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
            // 网关限流了请求,就会调用此回调
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                R error = R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(), BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
                String s = JSON.toJSONString(error);
                Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(s), String.class);
                return body;
            }
        });
    }

}

十五、Sleuth+Zipkin 服务链路追踪


1、基本概念和整合

1.1、为什么用

微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位 。主要体现在,一个请求可能需要调用很多个服务 ,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位
链路追踪组件有 Google Dapper Twitter Zipkin ,以及阿里的 Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件。

1.2、基本术语

  • Span(跨度):基本工作单元,发送一个远程调度任务 就会产生一个 SpanSpan 是一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 时间戳便可以得到整个请求所消耗的时间。

官方文档:https://github.com/spring-cloud/spring-cloud-sleuth/tree/2.2.x 

如果服务调用顺序如下

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

Span 之间的父子关系如下: 

2、整合 Sleuth

 1、gulimall-common导入依赖

        <!--链路追踪 sleuth-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
2 、每个微服务配置:打开 debug 日志
logging:
  level:
    org.springframework.cloud.openfeign: debug
    org.springframework.cloud.sleuth: debug

 3、发起一次远程调用,观察控制台

 查看商品详情:http://item.gulimall.com/10.html

DEBUG [gulimall-product,541450f08573fff5,541450f08573fff5,false]
gulimall-product:服务名
541450f08573fff5:是 TranceId,一条链路中,只有一个 TranceId
541450f08573fff5:是 spanId,链路中的基本工作单元 id
false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察

3、整合 zipkin 可视化观察

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

 

 1、docker 安装 zipkin 服务器

docker run --name zipkin-server -d --restart=always -p 9411:9411 openzipkin/zipkin
2、导入
        <!--链路追踪 zipkin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用

3、添加 zipkin 相关配置

spring:
  application:
    name: gulimall-product  
  zipkin:
    base-url: http://192.168.56.10:9411/ # zipkin 服务器的地址
    # 关闭服务发现,否则 Spring Cloud 会把 zipkin 的 url 当做服务名称
    discoveryClientEnabled: false
    sender:
      type: web # 设置使用 http 的方式传输数据
  sleuth:
    sampler:
      probability: 1 # 设置抽样采集率为 100%,默认为 0.1,即 10%

 发送远程请求,测试 zipkin

 服务调用链追踪信息统计 

4、Zipkin 数据持久化

Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据持久化,自然就是得将数据存储至数据库。好在 Zipkin 支持将数据存储至:
  • 内存(默认)
  • MySQL
  • Elasticsearch
  • Cassandra
Zipkin 数据持久化相关的官方文档地址如下:

Zipkin 支持的这几种存储方式中,内存显然是不适用于生产的,这一点开始也说了。而使用
MySQL 的话,当数据量大时,查询较为缓慢,也不建议使用。 Twitter 官方使用的是 Cassandra
作为 Zipkin 的存储数据库,但国内大规模用 Cassandra 的公司较少,而且 Cassandra 相关文
档也不多。
综上,故采用 Elasticsearch 是个比较好的选择,关于使用 Elasticsearch 作为 Zipkin 的存储数
据库的官方文档如下:
elasticsearch-storage

通过 docker 的方式
docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200
openzipkin/zipkin-dependencies

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

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐