谷粒商城-12-p300-p338

300、商城业务-支付-支付宝沙箱&代码

1、进入“蚂蚁金服开放平台” https://open.alipay.com/platform/home.htm

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

文档地址

https://open.alipay.com/platform/home.htm 支付宝&蚂蚁金服开发者平台

image-20210619191513689

https://docs.open.alipay.com/catalog 开发者文档

https://docs.open.alipay.com/270/106291/ 全部文档=>电脑网谷粒商城-12-p300-p338_第1张图片站支付文档;下载 demo

配置使用沙箱进行测试

1、使用 RSA 工具生成签名

2、下载沙箱版钱包

3、运行官方 demo 进行测试

301、商城业务-支付-RSA、加密加签、密钥等

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

1、公钥私钥

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

加密是指:

  • 我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解密的技术。

  • 公钥和私钥都可以用来加密,也都可以用来解密。

  • 但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。

  • 加密的目的是:

  • 为了确保数据传输过程中的不可读性,就是不想让别人看到。

签名:

  • 给我们将要发送的数据,做上一个唯一签名(类似于指纹)

  • 用来互相验证接收方和发送方的身份;

  • 在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以用来达到数据的明文传输。

验签

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

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

2、支付宝支付流程

谷粒商城-12-p300-p338_第2张图片

加密-对称加密

谷粒商城-12-p300-p338_第3张图片

加密-非对称加密

谷粒商城-12-p300-p338_第4张图片

支付宝的加解密过程

谷粒商城-12-p300-p338_第5张图片

302、商城业务-支付-内网穿透

因为我们要开发支付功能,支付宝会有回调地址,所以在开发过程中也需要我们的地址能够被支付宝回调成功,所以需要内网穿透。

1 、简介

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

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

1、买服务器并且有公网固定 IP

2、买域名映射到服务器的 IP

3、域名需要进行备案和审核

2 、使用场景

1、开发测试(微信、支付宝)

2、智慧互联

3、远程控制

4、私有云

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

1、natapp:https://natapp.cn/ 优惠码:022B93FD(9 折)[仅限第一次使用]

2、续断:www.zhexi.tech 优惠码:SBQMEA(95 折)[仅限第一次使用]

3、花生壳:https://www.oray.com/

谷粒商城-12-p300-p338_第6张图片

303、商城业务-订单服务-整合支付前需要注意的问题

(1) 支付宝加密原理

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

304、商城业务-订单服务-整合支付

1、引入依赖

    
        
        <dependency>
            <groupId>com.alipay.sdkgroupId>
            <artifactId>alipay-sdk-javaartifactId>
            <version>4.9.28.ALLversion>
        dependency>

2、抽取支付工具类并进行配置(可以查找老师的代码)

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

我们可以在异步通知回调地址(notify_url)接口修改我们的订单的状态(支付成功修改订单状态)还有一个return_url返回到我们支付成功后要跳转的页面

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

    }

(4) 订单支付与同步通知

image-20210619201326757

3、点击支付跳转到支付接口



/**
     * 1、将支付页让浏览器展示。
     * 2、支付成功以后,我们要跳到用户的订单列表页
     * @param orderSn
     * @return
     * @throws AlipayApiException
     */
@ResponseBody
@GetMapping(value = "/aliPayOrder",produces = "text/html")
public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
    System.out.println("接收到订单信息orderSn:"+orderSn);
    //获取当前订单并设置支付订单相关信息
    PayVo payVo = orderService.getOrderPay(orderSn);
    String pay = alipayTemplate.pay(payVo);
    return pay;//这个是返回的支付宝的支付页面,produces = "text/html"是一个HTML的字符串,相应页面后自动在浏览器渲染支付页面。
}

@Override
public PayVo getOrderPay(String orderSn) {
    OrderEntity orderEntity = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
    PayVo payVo = new PayVo();
    //交易号
    payVo.setOut_trade_no(orderSn);
    //支付金额设置为两位小数,否则会报错
    BigDecimal payAmount = orderEntity.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
    payVo.setTotal_amount(payAmount.toString());

    List<OrderItemEntity> orderItemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
    OrderItemEntity orderItemEntity = orderItemEntities.get(0);
    //订单名称
    payVo.setSubject(orderItemEntity.getSkuName());
    //商品描述
    payVo.setBody(orderItemEntity.getSkuAttrsVals());
    return payVo;
}

设置成功回调地址为订单详情页

305、商城业务-订单服务-支付成功同步回调

设置成功回调地址为订单详情页

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

	  /**
     * 获取当前用户的所有订单
     * @return
     */
    @RequestMapping("/memberOrder.html")
    public String memberOrder(@RequestParam(value = "pageNum",required = false,defaultValue = "0") Integer pageNum,Model model){
        Map<String, Object> params = new HashMap<>();
        params.put("page", pageNum.toString());
        //分页查询当前用户的所有订单及对应订单项
        PageUtils page = orderService.getMemberOrderPage(params);
        model.addAttribute("pageUtil", page);
        //返回至订单详情页
        return "list";
    }

306、商城业务-订单服务-订单列表页渲染完成

支付成功后跳转到我们指定的list 的页面

@Controller
public class MemberWebController {

    @Autowired
    OrderFeignService orderFeignService;

    /**
     * 订单分页查询
     * @param pageNum
     * @param model
     * @return
     */
    @GetMapping("/memberOrder.html")
    public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
                                  Model model, HttpServletRequest request){
        //获取到支付宝给我们传来的所有请求数据;
//        request。验证签名,如果正确可以去修改。

        //查出当前登录的用户的所有订单列表数据
        Map<String,Object> page =new HashMap<>();
        page.put("page",pageNum.toString());
        //
        R r = orderFeignService.listWithItem(page);
        System.out.println(JSON.toJSONString(r));
        model.addAttribute("orders",r);
        return "orderList";
    }
}
package com.atguigu.gulimall.member.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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Map;

/**
 * @创建人: 放生
 * @创建时间: 2022/5/7
 * @描述:
 */
@FeignClient("gulimall-order")
public interface OrderFeignService {

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

307、商城业务-订单服务-异步通知内网穿透环境搭建

我们整个支付宝会有两个回调地址,一个是return_url,一个是notify_url,我们之前是在return_url地址中处理支付成功后跳转的地址,我们可以在notify_url处理支付成功后,修改订单的状态逻辑。虽然也可以在return_url地址中一同处理订单的业务逻辑,但是这个地址支付宝只回调一次,而notify_url只要我们没有响应支付宝“success”,会在24h内回调8次(最大努力通知)

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

1、配置我们的 notify_url

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

2、利用内网穿透工具,给上一步的url 生成一个内网穿透的地址

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

  • 由于回调的请求头不是order.gulimall.com,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置

3、修改nginx

谷粒商城-12-p300-p338_第7张图片

4、测试

用内网穿透的地址测试访问通我们的notify_url

308、商城业务-订单服务-支付完成

1、验证签名

@PostMapping("/payed/notify")
public String handlerAlipay(HttpServletRequest request, PayAsyncVo payAsyncVo) throws AlipayApiException {
    System.out.println("收到支付宝异步通知******************");
    // 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
    // 获取支付宝POST过来反馈信息
    //TODO 需要验签(以下的验签代码支付宝案列中有讲解)
    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){
        System.out.println("支付宝异步通知验签成功");
        //修改订单状态
        orderService.handlerPayResult(payAsyncVo);
        return "success";
    }else {
        System.out.println("支付宝异步通知验签失败");
        return "error";
    }
}

2、修改订单状态与保存交易流水

@Override
public void handlerPayResult(PayAsyncVo payAsyncVo) {
    //保存交易流水
    PaymentInfoEntity infoEntity = new PaymentInfoEntity();
    String orderSn = payAsyncVo.getOut_trade_no();
    infoEntity.setOrderSn(orderSn);
    infoEntity.setAlipayTradeNo(payAsyncVo.getTrade_no());
    infoEntity.setSubject(payAsyncVo.getSubject());
    String trade_status = payAsyncVo.getTrade_status();
    infoEntity.setPaymentStatus(trade_status);
    infoEntity.setCreateTime(new Date());
    infoEntity.setCallbackTime(payAsyncVo.getNotify_time());
    paymentInfoService.save(infoEntity);

    //判断交易状态是否成功
    if (trade_status.equals("TRADE_SUCCESS") || trade_status.equals("TRADE_FINISHED")) {
        baseMapper.updateOrderStatus(orderSn, OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY);
    }

3、 异步通知的参数

@PostMapping("/payed/notify")
public String handlerAlipay(HttpServletRequest request) {
    System.out.println("收到支付宝异步通知******************");
    Map<String, String[]> parameterMap = request.getParameterMap();
    for (String key : parameterMap.keySet()) {
        String value = request.getParameter(key);
        System.out.println("key:"+key+"===========>value:"+value);
    }
    return "success";
}
收到支付宝异步通知******************
key:gmt_create===========>value:2020-10-18 09:13:26
key:charset===========>value:utf-8
key:gmt_payment===========>value:2020-10-18 09:13:34
key:notify_time===========>value:2020-10-18 09:13:35
key:subject===========>value:华为
key:sign===========>value:aqhKWzgzTLE84Scy5d8i3f+t9f7t7IE5tK/s5iHf3SdFQXPnTt6MEVtbr15ZXmITEo015nCbSXaUFJvLiAhWpvkNEd6ysraa+2dMgotuHPIHnIUFwvdk+U4Ez+2A4DBTJgmwtc5Ay8mYLpHLNR9ASuEmkxxK2F3Ov6MO0d+1DOjw9c/CCRRBWR8NHSJePAy/UxMzULLtpMELQ1KUVHLgZC5yym5TYSuRmltYpLHOuoJhJw8vGkh2+4FngvjtS7SBhEhR1GvJCYm1iXRFTNgP9Fmflw+EjxrDafCIA+r69ZqoJJ2Sk1hb4cBsXgNrFXR2Uj4+rQ1Ec74bIjT98f1KpA==
key:buyer_id===========>value:2088622954825223
key:body===========>value:上市年份:2020;内存:64G
key:invoice_amount===========>value:6300.00
key:version===========>value:1.0
key:notify_id===========>value:2020101800222091334025220507700182
key:fund_bill_list===========>value:[{"amount":"6300.00","fundChannel":"ALIPAYACCOUNT"}]
key:notify_type===========>value:trade_status_sync
key:out_trade_no===========>value:12345523123
key:total_amount===========>value:6300.00
key:trade_status===========>value:TRADE_SUCCESS
key:trade_no===========>value:2020101822001425220501264292
key:auth_app_id===========>value:2016102600763190
key:receipt_amount===========>value:6300.00
key:point_amount===========>value:0.00
key:app_id===========>value:2016102600763190
key:buyer_pay_amount===========>value:6300.00
key:sign_type===========>value:RSA2
key:seller_id===========>value:2088102181115314

各参数详细意义见[支付宝开放平台异步通知](

309、商城业务-订单服务-收单

1、收单

由于可能出现订单已经过期后,库存已经解锁,但支付成功后再修改订单状态的情况,需要设置支付有效时间,只有在有效期内才能进行支付(就是比如:客户打开支付页面20分钟没有支付,20分钟后,后台会把未支付的订单解锁,库存解锁,这个时候客户在支付,此时后台已经把这个订单,库存释放了,这个时候支付就有问题,所以要给支付宝一个时间,比如设置10分钟,如果十分钟未支付就自动收单,就不让其支付了,需要重新创建新的单。)

alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
        + "\"total_amount\":\""+ total_amount +"\","
        + "\"subject\":\""+ subject +"\","
        + "\"body\":\""+ body +"\","
        //设置过期时间为1m
        +"\"timeout_express\":\"1m\","
        + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

超时后订单显示:“抱歉您的交易因超时已失败”’

310、商城业务-秒杀服务-后台添加秒杀商品

1 、秒杀业务

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

限流方式:

  1. 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计

  2. nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法

  3. 网关限流,限流的过滤器

  4. 代码中使用分布式信号量

  5. rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。

2 、秒杀流程

见秒杀流程图

谷粒商城-12-p300-p338_第8张图片

谷粒商城-12-p300-p338_第9张图片

秒杀架构图

谷粒商城-12-p300-p338_第10张图片

秒杀系统关注的问题

  • 1、服务单一职责+独立部署
    • 秒杀服务即使自己扛不住压力,挂掉。不要影响别人
  • 2、秒杀链接加密
    • 防止恶意攻击,模拟秒杀请求,1000次/s攻击
    • 防止链接暴露,自己工作人员,提前秒杀商品,我们的案列中加入了一个校验码
  • 3、库存预热+快速扣减
    • 秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求
  • 4、动静分离
    • nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
      使用cDN网络,分担本集群压力
  • 5、恶意请求拦截
    • 识别非法攻击请求并进行拦截,网关层
  • 6、流量错峰
    • 使用各种手段,将流量分担到更大宽度的时间点。比如验证码(在输入验证码的适合可以错峰,还可以把别人恶意脚本攻击过滤掉),加入购物车
  • 7、限流&熔断&降级
    • 前端限流+后端限流
    • 限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
  • 8、队列削峰
    • 1万个商品,每个1000件秒杀。双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可。

后vue的后台管理系统里上架秒杀,打开F12看url然后去编写逻辑

  • 秒杀名称
  • 开始时间
  • 结束时间
  • 启用状态

点击关联商品可以添加秒杀里的商品。可以看sms数据库里的seckill_sky

2. 秒杀架构设计

(1) 秒杀架构

nginx–>gateway–>redis分布式信号了–> 秒杀服务

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

秒杀活动:存在在scekill:sesssions这个redis-key里,。value为 skyIds[]

秒杀活动里具体商品项:是一个map,redis-key是seckill:skusmap-key是skuId+商品随机码

(2) redis存储模型设计

秒杀场次存储的List可以当做hash keySECKILL_CHARE_PREFIX 中获得对应的商品数据

  • 随机码防止人在秒杀一开放就秒杀完,必须携带上随机码才能秒杀

  • 结束时间

  • 设置分布式信号量,这样就不会每个请求都访问数据库。seckill:stock:#(商品随机码)

  • session里存了session-sku[]的列表,而seckill:skus的key也是session-sku,不要只存储sku,不能区分场次

  • 存储后的效果

//存储的秒杀场次对应数据
//K: SESSION_CACHE_PREFIX + startTime + "_" + endTime
//V: sessionId+"-"+skuId的List
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";

//存储的秒杀商品数据
//K: 固定值SECKILL_CHARE_PREFIX
//V: hash,k为sessionId+"-"+skuId,v为对应的商品信息SeckillSkuRedisTo
private final String SECKILL_CHARE_PREFIX = "seckill:skus";

//K: SKU_STOCK_SEMAPHORE+商品随机码
//V: 秒杀的库存件数
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码

用来存储的to

@Data
public class SeckillSkuRedisTo { // 秒杀sku项
    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 skuInfoVo;

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

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

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

3 、限流

参照 Alibaba Sentinel

4、搭建秒杀服务

谷粒商城-12-p300-p338_第11张图片

勾选上 data-redis, web,openfeign ,devtools,lombok

5、pom

<dependencies>

        <dependency>
            <groupId>com.atguigu.gulimallgroupId>
            <artifactId>gulimall-commonartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettucegroupId>
                    <artifactId>lettuce-coreartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>

        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.12.0version>
        dependency>
        <dependency>
            <groupId>org.springframework.sessiongroupId>
            <artifactId>spring-session-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

6、properties

applivation.properties

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


spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=119.3.105.108
spring.thymeleaf.cache=false
spring.session.store-type=redis

#spring.task.scheduling.pool.size=5
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50

bootstrap.properties

spring.cloud.nacos.config.server-addr=119.3.105.108:8848
spring.application.name=gulimall-seckill
server.port=25000

7、主启动


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

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

}

311-312、商城业务-秒杀服务-定时任务&Cron表达式

312、商城业务-秒杀服务-SpringBoot整合定时任务与异步任务

秒杀服务定时上架秒杀商品

1、定时任务

表达式:https://cron.qqe2.com/

此处定时任务用于定时查询秒杀活动

2、方法1 注解

package com.atguigu.gulimall.seckill.scheduled;


import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;


/**
 * 定时任务
 *      1、@EnableScheduling 开启定时任务
 *      2、@Scheduled  开启一个定时任务
 *      3、自动配置类 TaskSchedulingAutoConfiguration
 *
 * 异步任务
 *      1、@EnableAsync 开启异步任务功能
 *      2、@Async 给希望异步执行的方法上标注
 *      3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在TaskExecutionProperties
 *
 */
@Slf4j
@Component
//@EnableAsync
//@EnableScheduling
public class HelloSchedule {
    /**
     * 1、Spring中6位组成,不允许第7位的年
     * 2、在周几的位置,1-7代表周一到周日; MON-SUN
     * 3、定时任务不应该阻塞。默认是阻塞的
     *      1)、可以让业务运行以异步的方式,自己提交到线程池
     *              CompletableFuture.runAsync(()->{
     *                  xxxxService.hello();
     *              },executor);
     *      2)、支持定时任务线程池;设置 TaskSchedulingProperties;
     *              spring.task.scheduling.pool.size=5
     *
     *      3)、让定时任务异步执行
     *          异步任务;
     *
     *     解决:使用异步+定时任务来完成定时任务不阻塞的功能;
     *
     *
     */
    @Async
    @Scheduled(cron = "* * * ? * 5")
    public void hello() throws InterruptedException {
        log.info("hello...");
        Thread.sleep(3000);
    }
}

3、定时上架秒杀的商品

package com.atguigu.gulimall.seckill.scheduled;
import com.atguigu.gulimall.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
 * 秒杀商品的定时上架;
 *     每天晚上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;

    @Autowired
    RedissonClient redissonClient;

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

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

    }

}

基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。

@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
public class SaticScheduleTask {
    //3.添加定时任务
    @Scheduled(cron = "0/5 * * * * ?")
    //或直接指定时间间隔,例如:5秒
    //@Scheduled(fixedRate=5000)
    private void configureTasks() {
        System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
    }
}
Cron表达式参数分别表示:

秒(0~59) 例如0/5表示每5秒
分(0~59)
时(0~23)
日(0~31)的某天,需计算
月(0~11)
周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)
@Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。
// Cron表达式范例:

每隔5秒执行一次:*/5 * * * * ?

每隔1分钟执行一次:0 */1 * * * ?

每天23点执行一次:0 0 23 * * ?

每天凌晨1点执行一次:0 0 1 * * ?

每月1号凌晨1点执行一次:0 0 1 1 * ?

每月最后一天23点执行一次:0 0 23 L * ?

每周星期天凌晨1点实行一次:0 0 1 ? * L

在26分、29分、33分执行一次:0 26,29,33 * * * ?

每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

显然,使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到实时生效的效果,可以使用接口来完成定时任务。

313、商城业务-秒杀服务-时间日期处理

查询三天内需要秒杀的商品

package com.atguigu.gulimall.coupon.service.impl;

import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.coupon.dao.SeckillSessionDao;
import com.atguigu.gulimall.coupon.entity.SeckillSessionEntity;
import com.atguigu.gulimall.coupon.entity.SeckillSkuRelationEntity;
import com.atguigu.gulimall.coupon.service.SeckillSessionService;
import com.atguigu.gulimall.coupon.service.SeckillSkuRelationService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<SeckillSessionEntity> page = this.page(
                new Query<SeckillSessionEntity>().getPage(params),
                new QueryWrapper<SeckillSessionEntity>()
        );

        return new PageUtils(page);
    }

    @Autowired
    SeckillSkuRelationService seckillSkuRelationService;

    @Override
    public List<SeckillSessionEntity> getLates3DaySession() {

        //计算最近三天
//        Date date = new Date(); //2020-12-12 13:59:16

        List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));

        if(list!=null && list.size()>0){
            List<SeckillSessionEntity> collect = list.stream().map(session -> {
                Long id = session.getId();
                List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                session.setRelationSkus(relationEntities);
                return session;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }


    private String startTime(){
        LocalDate now = LocalDate.now();
        LocalTime min = LocalTime.MIN;
        LocalDateTime start = LocalDateTime.of(now, min);
        String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }

    private String endTime(){
        LocalDate now = LocalDate.now();
        LocalDate localDate = now.plusDays(2);
        LocalDateTime of = LocalDateTime.of(localDate, LocalTime.MAX);
        String format = of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }

}

314-316、商城业务-秒杀服务-秒杀商品上架

谷粒商城-12-p300-p338_第12张图片

代码实现

package com.atguigu.guliamll.seckill.scheduled;


import com.atguigu.guliamll.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;


/**
 * @创建时间: 2022/5/8
 * @创建人: 放生
 * @描述:
 * 秒杀商品的定时上架;
 *     每天晚上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;

    @Autowired
    RedissonClient redissonClient;

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

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

    }

}

具体实现 uploadSeckillSkuLatest3Days

获取最近三天的秒杀信息

  • 获取最近三天的秒杀场次信息,再通过秒杀场次id查询对应的商品信息
  • 防止集群多次上架
package com.atguigu.guliamll.seckill.service.impl;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.to.mq.SeckillOrderTo;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberRespVo;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.guliamll.seckill.feign.CouponFeignService;
import com.atguigu.guliamll.seckill.feign.ProductFeignService;
import com.atguigu.guliamll.seckill.interceptor.LoginUserInterceptor;
import com.atguigu.guliamll.seckill.service.SeckillService;
import com.atguigu.guliamll.seckill.to.SecKillSkuRedisTo;
import com.atguigu.guliamll.seckill.vo.SeckillSesssionsWithSkus;
import com.atguigu.guliamll.seckill.vo.SkuInfoVo;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @创建人: 放生
 * @创建时间: 2022/5/8
 * @描述:
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    CouponFeignService couponFeignService;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Autowired
    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、扫描最近三天需要参与秒杀的活动
        R session = couponFeignService.getLates3DaySession();
        if (session.getCode() == 0) {
            //上架商品
            List<SeckillSesssionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSesssionsWithSkus>>() {
            });
            //缓存到redis
            //1、缓存活动信息
            saveSessionInfos(sessionData);
            //2、缓存活动的关联商品信息
            saveSessionSkuInfos(sessionData);
        }

    }

    private void saveSessionInfos(List<SeckillSesssionsWithSkus> sesssions) {
        if (sesssions != null) {
            sesssions.stream().forEach(sesssion -> {

                Long startTime = sesssion.getStartTime().getTime();
                Long endTime = sesssion.getEndTime().getTime();
                String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
                Boolean hasKey = redisTemplate.hasKey(key);
                if (!hasKey) {
                    List<String> collect = sesssion.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString()).collect(Collectors.toList());
                    //缓存活动信息
                    redisTemplate.opsForList().leftPushAll(key, collect);
                    //TODO 设置过期时间[已完成]
                    redisTemplate.expireAt(key, new Date(endTime));
                }


            });

        }
    }

    private void saveSessionSkuInfos(List<SeckillSesssionsWithSkus> sesssions) {
        if (sesssions != null) {
            sesssions.stream().forEach(sesssion -> {
                //准备hash操作
                BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                sesssion.getRelationSkus().stream().forEach(seckillSkuVo -> {
                    //4、随机码?  seckill?skuId=1&key=dadlajldj;
                    // 随机码是因为商品的id是很容易被暴露的,防止他人提前准备准备开始,或者开发人员内部知道skuid
                    String token = UUID.randomUUID().toString().replace("-", "");

                    if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())) {
                        //缓存商品
                        SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
                        //1、sku的基本数据
                        R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                        if (skuInfo.getCode() == 0) {
                            SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                            });
                            redisTo.setSkuInfo(info);
                        }

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

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

                        redisTo.setRandomCode(token);
                        String jsonString = JSON.toJSONString(redisTo);
                        //TODO 每个商品的过期时间不一样。所以,我们在获取当前商品秒杀信息的时候,做主动删除,代码在 getSkuSeckillInfo 方法里面
                        ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);
                        //如果当前这个场次的商品的库存信息已经上架就不需要上架
                        //5、使用库存作为分布式的信号量  限流;
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                        //商品可以秒杀的数量作为信号量
                        semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                        //TODO 设置过期时间。
                        semaphore.expireAt(sesssion.getEndTime());
                    }
                });
            });
        }
    }
}

Redis保存秒杀场次信息

private void saveSessionInfo(List<SeckillSessionsWithSkus> sessions){
    if(sessions != null){
        sessions.stream().forEach(session -> {
            long startTime = session.getStartTime().getTime();

            long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime; // "seckill:sessions:";
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            // 防止重复添加活动到redis中
            if(!hasKey){
                // 获取所有商品id // 格式:活动id-skuId
                List<String> skus = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId()).collect(Collectors.toList());
                // 缓存活动信息
                stringRedisTemplate.opsForList().leftPushAll(key, skus);
            }
        });
    }
}

redis保存秒杀商品信息

前面已经缓存了sku项的活动信息,但是只有活动id和skuID,接下来我们要保存完整是sku信息到redis中

private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){
    if(sessions != null){
        // 遍历session
        sessions.stream().forEach(session -> {
            BoundHashOperations<String, Object, Object> ops =
                stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); // "seckill:skus:";
            // 遍历sku
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                // 1.商品的随机码
                String randomCode = UUID.randomUUID().toString().replace("-", "");
                // 缓存中没有再添加
                if(!ops.hasKey(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId())){
                    // 2.缓存商品
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    BeanUtils.copyProperties(seckillSkuVo, redisTo);
                    // 3.sku的基本数据 sku的秒杀信息
                    R info = productFeignService.skuInfo(seckillSkuVo.getSkuId());
                    if(info.getCode() == 0){
                        SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                        redisTo.setSkuInfoVo(skuInfo);
                    }
                    // 4.设置当前商品的秒杀信息
                    redisTo.setStartTime(session.getStartTime().getTime());
                    redisTo.setEndTime(session.getEndTime().getTime());
                    // 设置随机码
                    redisTo.setRandomCode(randomCode);
                    // 活动id-skuID   秒杀sku信息
                    ops.put(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId(), JSON.toJSONString(redisTo));
                    // 5.使用库存作为分布式信号量  限流
                    RSemaphore semaphore = 
                        redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);//"seckill:stock:";
                    // 在信号量中设置秒杀数量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
                }
            });
        });
    }

317、商城业务-秒杀服务-幂等性保证

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

就是我如果上架的服务部署了多个定时任务的上架功能,会存在重复上架的问题,我们的方案是采用分布式锁来解决,也可以采用分布式的定时任务,比如xxjob

谷粒商城-12-p300-p338_第13张图片

加分布式锁 redissonClient.getLock(upload_lock)

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

    }

318、商城业务-秒杀服务-查询秒杀商品

1、获取秒杀的商品

前面已经在redis中缓存了秒杀活动的各种信息,现在写获取缓存中当前时间段在秒杀的sku,用户点击页面后发送请求

@GetMapping(value = "/getCurrentSeckillSkus")
@ResponseBody // 用户网页发请求
public R getCurrentSeckillSkus() {
    //获取到当前可以参加秒杀商品的信息
    List<SeckillSkuRedisTo> vos = secKillService.getCurrentSeckillSkus();

    return R.ok().setData(vos);
}

@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {

    // 1.确定当前时间属于那个秒杀场次
    long time = new Date().getTime();
    // 定义一段受保护的资源
    try (Entry entry = SphU.entry("seckillSkus")){
        Set<String> keys = stringRedisTemplate.keys(SESSION_CACHE_PREFIX + "*");
        for (String key : keys) {
            // seckill:sessions:1593993600000_1593995400000
            String replace = key.replace("seckill:sessions:", "");
            String[] split = replace.split("_");
            long start = Long.parseLong(split[0]);
            long end = Long.parseLong(split[1]);
            if(time >= start && time <= end){
                // 2.获取这个秒杀场次的所有商品信息
                List<String> range = stringRedisTemplate.opsForList().range(key, 0, 100);
                BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<String> list = hashOps.multiGet(range);
                if(list != null){
                    return list.stream().map(item -> {
                        SeckillSkuRedisTo redisTo = JSON.parseObject(item, SeckillSkuRedisTo.class);
                        //						redisTo.setRandomCode(null);
                        return redisTo;
                    }).collect(Collectors.toList());
                }
                break;
            }
        }
    }catch (BlockException e){
        log.warn("资源被限流:" + e.getMessage());
    }
    return null;
}

2、首页获取并拼装数据

<div class="swiper-slide">
  
  <ul id="seckillSkuContent">ul>
div>

<script type="text/javascript">
  $.get("http://seckill.gulimall.com/getCurrentSeckillSkus", function (res) {
    if (res.data.length > 0) {
      res.data.forEach(function (item) {
        $("
  • "
    ).append($("")) .append($("

    "+item.skuInfoVo.skuTitle+"

    "
    )) .append($("" + item.seckillPrice + "")) .append($("" + item.skuInfoVo.price + ""
    )) .appendTo("#seckillSkuContent"); }) } }) function toDetail(skuId) { location.href = "http://item.gulimall.com/" + skuId + ".html"; }
    script>

    319、商城业务-秒杀服务-秒杀页面渲染

    随机码是在秒杀活动开始才暴露出去。

    • 用户看到秒杀活动点击秒杀商品了,如果时间段正确,返回随机码。购买时带着

      • 注意不要redis-map中的key
      @ResponseBody
      @GetMapping(value = "/getSeckillSkuInfo/{skuId}")
      public R getSeckillSkuInfo(@PathVariable("skuId") Long skuId) {
          SeckillSkuRedisTo to = secKillService.getSeckillSkuInfo(skuId);
          return R.ok().setData(to);
      }
      
      @Override
      public SeckillSkuRedisTo getSeckillSkuInfo(Long skuId) {
          BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
          //获取所有商品的hash key
          Set<String> keys = ops.keys();
          for (String key : keys) {
              //通过正则表达式匹配 数字-当前skuid的商品
              if (Pattern.matches("\\d-" + skuId,key)) {
                  String v = ops.get(key);
                  SeckillSkuRedisTo redisTo = JSON.parseObject(v, SeckillSkuRedisTo.class);
                  //当前商品参与秒杀活动
                  if (redisTo!=null){
                      long current = System.currentTimeMillis();
                      //当前活动在有效期,暴露商品随机码返回
                      if (redisTo.getStartTime() < current && redisTo.getEndTime() > current) {
                          return redisTo;
                      }
                      //当前商品不再秒杀有效期,则隐藏秒杀所需的商品随机码
                      redisTo.setRandomCode(null);
                      return redisTo;
                  }
              }
          }
          return null;
      }
      

      在查询商品详情页的接口中查询秒杀对应信息

      @Override // SkuInfoServiceImpl
      public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
          ....;
          // 6.查询当前sku是否参与秒杀优惠
          CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
              R skuSeckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
              if (skuSeckillInfo.getCode() == 0) {
                  SeckillInfoVo seckillInfoVo = skuSeckillInfo.getData(new TypeReference<SeckillInfoVo>() {});
                  skuItemVo.setSeckillInfoVo(seckillInfoVo);
              }
          }, executor);
      

      注意所有的时间都是距离1970的差值

      更改商品详情页的显示效果

      <li style="color: red" th:if="${item.seckillSkuVo != null}">
      
          <span th:if="${#dates.createNow().getTime() < item.seckillSkuVo.startTime}">
              商品将会在[[${#dates.format(new java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀
          span>
      
          <span th:if="${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}">
              秒杀价  [[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]
          span>
      
      li>
      
      <div class="box-btns-two"
           th:if="${item.seckillSkuVo == null }">
          <a class="addToCart" href="http://cart.gulimall.com/addToCart" th:attr="skuId=${item.info.skuId}">
              加入购物车
          a>
      div>
      
      <div class="box-btns-two"
           th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}">
          <a class="seckill" href="#"
             th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}">
              立即抢购
          a>
      div>
      

    320、商城业务-秒杀服务-秒杀系统设计

    谷粒商城-12-p300-p338_第14张图片

    谷粒商城-12-p300-p338_第15张图片

    321、商城业务-秒杀服务-登录检查

    秒杀业务

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

    秒杀按钮:

    <div class="box-btns-two"
         th:if="${item.seckillInfoVo != null && (#dates.createNow().getTime() >= item.seckillInfoVo.startTime && #dates.createNow().getTime() <= item.seckillInfoVo.endTime)}">
        <a id="secKillA"
           th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfoVo.promotionSessionId},code=${item.seckillInfoVo.randomCode}">
            立即抢购
        a>
    div>
    <div class="box-btns-two"
         th:if="${item.seckillInfoVo == null || (#dates.createNow().getTime() < item.seckillInfoVo.startTime || #dates.createNow().ge`tTime() > item.seckillInfoVo.endTime)}">
        <a id="addToCartA" th:attr="skuId=${item.info.skuId}">
            加入购物车
        a>
    div>
    

    秒杀函数:

    要判断是否登入

    $("#secKillA").click(function () {
        var isLogin = [[${session.loginUser != null}]]
        if (isLogin) {
            var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");
            var num = $("#numInput").val();
            location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + $(this).attr("code") + "&num=" + num;
        } else {
            layer.msg("请先登录!")
        }
        return false;
    })
    

    添加是否登入的拦截器

    package com.atguigu.guliamll.seckill.interceptor;
    
    /**
     * @创建人: 放生
     * @创建时间: 2022/5/8
     * @描述:
     */
    import com.atguigu.common.constant.AuthServerConstant;
    import com.atguigu.common.vo.MemberRespVo;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    
    @Component
    public class LoginUserInterceptor implements HandlerInterceptor {
    
    
        public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //  /order/order/status/2948294820984028420
            String uri = request.getRequestURI();
            AntPathMatcher antPathMatcher = new AntPathMatcher();
            boolean match = antPathMatcher.match("/kill", uri);
    
            if(match){
                MemberRespVo attribute = (MemberRespVo) 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;
    
    
    
        }
    }
    
    

    322、商城业务-秒杀服务-秒杀流程

    上一章节已经写好了,我们的页面,秒杀按钮,请求路径等,接下来编写秒杀的后端接口

       @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);
            //1、判断是否登录
            return "success";
        }
    
     // TODO 上架秒杀商品的时候,每一个数据都有过期时间。
        // TODO 秒杀后续的流程,简化了收货地址等信息。
        @Override
        public String kill(String killId, String key, Integer num) {
    
            long s1 = System.currentTimeMillis();
            MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
    
            //1、获取当前秒杀商品的详细信息
            BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    
            String json = hashOps.get(killId);
            if (StringUtils.isEmpty(json)) {
                return null;
            } else {
                SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
                //校验合法性
                Long startTime = redis.getStartTime();
                Long endTime = redis.getEndTime();
                long time = new Date().getTime();
    
                long ttl = endTime - time;
    
                //1、校验时间的合法性
                if (time >= startTime && time <= endTime) {
                    //2、校验随机码和商品id
                    String randomCode = redis.getRandomCode();
                    String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                    if (randomCode.equals(key) && killId.equals(skuId)) {
                        //3、验证购物数量是否合理
                        if (num <= redis.getSeckillLimit()) {
                            //4、验证这个人是否已经购买过。幂等性; 如果只要秒杀成功,就去占位。  userId_SessionId_skuId
                            //SETNX
                            String redisKey = respVo.getId() + "_" + skuId;
                            //自动过期
                            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                            if (aBoolean) {
                                //占位成功说明从来没有买过
                                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                                //120  20ms
                                boolean b = semaphore.tryAcquire(num);
                                if (b) {
                                    //秒杀成功;
                                    //快速下单。发送MQ消息  10ms
                                    String timeId = IdWorker.getTimeId();
                                    SeckillOrderTo orderTo = new SeckillOrderTo();
                                    orderTo.setOrderSn(timeId);
                                    orderTo.setMemberId(respVo.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 timeId;
                                }
                                return null;
    
                            } else {
                                //说明已经买过了
                                return null;
                            }
    
                        }
                    } else {
                        return null;
                    }
    
                } else {
                    return null;
                }
            }
    
    
            return null;
        }
    

    323、商城业务-秒杀服务-秒杀效果完成

    谷粒商城-12-p300-p338_第16张图片

    1、在seckill服务引入mq的依赖

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-amqpartifactId>
            dependency>
    

    2、在seckil 配置类

    package com.atguigu.guliamll.seckill.config;
    
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.connection.CorrelationData;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.annotation.PostConstruct;
    
    @Configuration
    public class MyRabbitConfig {
    
    
        /**
         * 使用JSON序列化机制,进行消息转换
         */
        @Bean
        public MessageConverter messageConverter(){
    
            return new Jackson2JsonMessageConverter();
        }
    
    }
    
    

    3、在order服务添加队列

    package com.atguigu.gulimall.order.config;
    
    import com.atguigu.gulimall.order.entity.OrderEntity;
    import com.rabbitmq.client.Channel;
    import org.springframework.amqp.core.*;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @创建人: 放生
     * @创建时间: 2022/5/6
     * @描述:
     */
    @Configuration
    public class MyMQConfig {
    
     ...........
    
    
        @Bean
        public Queue orderSeckillOrderQueue(){
            //String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
            return new Queue("order.seckill.order.queue",true,false,false);
        }
    
        @Bean
        public Binding orderSeckillOrderQueueBinding(){
            /**
             * String destination, DestinationType destinationType, String exchange, String routingKey,
             * 			Map arguments
             */
            return new Binding("order.seckill.order.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.seckill.order",
                    null);
        }
    
    }
    
    

    4、在order服务添加消费者

    package com.atguigu.gulimall.order.listener;
    
    
    import com.atguigu.common.to.mq.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;
    /**
     * @创建人: 放生
     * @创建时间: 2022/5/8
     * @描述:
     */
    @Slf4j
    @RabbitListener(queues = "order.seckill.order.queue")
    @Component
    public class OrderSeckillListener {
    
        @Autowired
        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);
            }
    
        }
    }
    
    

    5、秒杀服务秒杀成功后发送数据到mq

     // TODO 上架秒杀商品的时候,每一个数据都有过期时间。
        // TODO 秒杀后续的流程,简化了收货地址等信息。
        @Override
        public String kill(String killId, String key, Integer num) {
    
            long s1 = System.currentTimeMillis();
            MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
    
            //1、获取当前秒杀商品的详细信息
            BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    
            String json = hashOps.get(killId);
            if (StringUtils.isEmpty(json)) {
                return null;
            } else {
                SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
                //校验合法性
                Long startTime = redis.getStartTime();
                Long endTime = redis.getEndTime();
                long time = new Date().getTime();
    
                long ttl = endTime - time;
    
                //1、校验时间的合法性
                if (time >= startTime && time <= endTime) {
                    //2、校验随机码和商品id
                    String randomCode = redis.getRandomCode();
                    String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
                    if (randomCode.equals(key) && killId.equals(skuId)) {
                        //3、验证购物数量是否合理
                        if (num <= redis.getSeckillLimit()) {
                            //4、验证这个人是否已经购买过。幂等性; 如果只要秒杀成功,就去占位。  userId_SessionId_skuId
                            //SETNX
                            String redisKey = respVo.getId() + "_" + skuId;
                            //自动过期
                            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                            if (aBoolean) {
                                //占位成功说明从来没有买过
                                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                                //120  20ms
                                boolean b = semaphore.tryAcquire(num);
                                if (b) {
                                    //秒杀成功;
                                    //快速下单。发送MQ消息  10ms
                                    String timeId = IdWorker.getTimeId();
                                    SeckillOrderTo orderTo = new SeckillOrderTo();
                                    orderTo.setOrderSn(timeId);
                                    orderTo.setMemberId(respVo.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);
                                  .......
    

    324、商城业务-秒杀服务-秒杀页面完成

    添加秒杀成功或者失败后的页面显示。。。

    325-326、Sentinel-高并发方法论&简介

    326、Sentinel-基本概念

    官网: https://github.com/alibaba/Sentinel/wiki/介绍

    1 、简介

    1 、熔断降级限流

    什么是熔断

    A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是 调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影 响到 A。

    什么是降级

    整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和 页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的 的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。

    异同:

    相同点:

    1、为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我

    2、用户最终都是体验到某个功能不可用

    不同点:

    1、熔断是被调用方故障,触发的系统主动规则2、降级是基于全局考虑,停止一些正常服务,释放资源什么是限流

    对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力

    sentinel在 springcloud Alibaba 中的作用是实现熔断限流。类似于Hystrix豪猪

    Sentinel 基本概念

    • 资源

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

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

    • 规则

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

    谷粒商城-12-p300-p338_第17张图片

    下载地址dashboard: https://github.com/alibaba/Sentinel/releases/download/1.7.1/sentinel-dashboard-1.7.1.jar

    下载jar包以后,使用【java -jar】命令启动即可。

    它使用 8080 端口,用户名和密码都为 : sentinel
    随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

    Sentinel 具有以下特征:

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

    Sentinel-features-overview

    Sentinel 分为两个部分:

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

    327、Sentinel-整合SpringBoot

    谷粒商城-12-p300-p338_第18张图片

    谷粒商城-12-p300-p338_第19张图片

    1、引入依赖

            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
                <version>2.1.0.RELEASEversion>
            dependency>
    

    2、下载sentinel的控制台

    Sentinel 控制台

    Sentinel 控制台提供一个轻量级的控制台,它提供机器发现、单机资源实时监控、集群资源汇总,以及规则管理的功能。您只需要对应用进行简单的配置,就可以使用这些功能。

    注意: 集群资源汇总仅支持 500 台以下的应用集群,有大概 1 - 2 秒的延时。

    谷粒商城-12-p300-p338_第20张图片

    Figure 1. Sentinel Dashboard

    开启该功能需要3个步骤:

    获取控制台

    您可以从 release 页面 下载最新版本的控制台 jar 包。

    您也可以从最新版本的源码自行构建 Sentinel 控制台:

    • 下载 控制台 工程
    • 使用以下命令将代码打包成一个 fat jar: mvn clean package
    启动控制台

    Sentinel 控制台是一个标准的 Spring Boot 应用,以 Spring Boot 的方式运行 jar 包即可。

    java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
    

    如若8080端口冲突,可使用 -Dserver.port=新端口 进行设置。

    配置sentinel控制台地址信息
    在控制台调整参数。【默认所有的流控设置保存在内存中,重启失效】

    2、配置yaml

    配置控制台信息

    application.yml

    spring:
      cloud:
        sentinel:
          transport:
            port: 8719
            dashboard: localhost:8080
    

    这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。

    更多 Sentinel 控制台的使用及问题参考: Sentinel 控制台文档 以及 Sentinel FAQ

    spring.application.name=gulimall-seckill
    server.port=25000
    spring.cloud.nacos.discovery.server-addr=119.3.105.108:8848
    spring.redis.host=119.3.105.108
    
    
    spring.rabbitmq.virtual-host=/
    spring.rabbitmq.host=119.3.105.108
    spring.thymeleaf.cache=false
    spring.session.store-type=redis
    
    #spring.task.scheduling.pool.size=5
    spring.task.execution.pool.core-size=5
    spring.task.execution.pool.max-size=50
    
    
    #sentinel控制台
    spring.cloud.sentinel.transport.port=8719
    spring.cloud.sentinel.transport.dashboard=localhost:8333
    

    328、Sentinel-自定义流控响应

    Endpoint 支持

    在使用 Endpoint 特性之前需要在 Maven 中添加 spring-boot-starter-actuator 依赖,并在配置中允许 Endpoints 的访问。

    • Spring Boot 1.x 中添加配置 management.security.enabled=false。暴露的 endpoint 路径为 /sentinel
    • Spring Boot 2.x 中添加配置 management.endpoints.web.exposure.include=*。暴露的 endpoint 路径为 /actuator/sentinel

    Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。

     			 <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-actuatorartifactId>
            dependency>
    
    management.endpoints.web.exposure.include=*
    

    谷粒商城-12-p300-p338_第21张图片

    我们实现自定义的返回

    package com.atguigu.guliamll.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.BizCodeEnume;
    import com.atguigu.common.utils.R;
    import org.springframework.context.annotation.Configuration;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Configuration
    public class SeckillSentinelConfig {
    
        public SeckillSentinelConfig(){
            WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler(){
                @Override
                public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
                    R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
                    response.setCharacterEncoding("UTF-8");
                    response.setContentType("application/json");
                    response.getWriter().write(JSON.toJSONString(error));
                }
            });
        }
    }
    

    image-20220508163140554

    329、Sentinel-全服务引入

    参照以上的步骤

    每一个微服务都导入 actuator ();并配合management.endpoints.web.exposure.include=*
    自定义sentinel流控返回数据
    

    common的完整配置

    
    
        <dependencies>
            
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-boot-starterartifactId>
                <version>3.2.0version>
            dependency>
    
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <version>1.18.8version>
            dependency>
    
            
            <dependency>
                <groupId>org.apache.httpcomponentsgroupId>
                <artifactId>httpcoreartifactId>
                <version>4.4.12version>
            dependency>
    
    
            <dependency>
                <groupId>commons-langgroupId>
                <artifactId>commons-langartifactId>
                <version>2.6version>
            dependency>
    
            
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>8.0.17version>
            dependency>
    
            <dependency>
                <groupId>javax.servletgroupId>
                <artifactId>servlet-apiartifactId>
                <version>2.5version>
                <scope>providedscope>
            dependency>
    
            
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
            dependency>
            
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
            dependency>
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-seataartifactId>
            dependency>
    
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
            dependency>
    
            <dependency>
                <groupId>com.alibaba.cspgroupId>
                <artifactId>sentinel-datasource-nacosartifactId>
                <version>1.7.1version>
            dependency>
    
            
    
    
    
    
    
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-zipkinartifactId>
            dependency>
    
    
    
            <dependency>
                <groupId>javax.validationgroupId>
                <artifactId>validation-apiartifactId>
                <version>2.0.1.Finalversion>
            dependency>
    
            
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>fastjsonartifactId>
                <version>1.2.15version>
            dependency>
            <dependency>
                <groupId>org.apache.httpcomponentsgroupId>
                <artifactId>httpclientartifactId>
                <version>4.2.1version>
            dependency>
            <dependency>
                <groupId>org.apache.httpcomponentsgroupId>
                <artifactId>httpcoreartifactId>
                <version>4.2.1version>
            dependency>
            <dependency>
                <groupId>commons-langgroupId>
                <artifactId>commons-langartifactId>
                <version>2.6version>
            dependency>
            <dependency>
                <groupId>org.eclipse.jettygroupId>
                <artifactId>jetty-utilartifactId>
                <version>9.3.7.v20160115version>
            dependencies>
    
    

    330、Sentinel-流控模式&效果

    流控规则

    资源名:唯一名称,默认请求路径
    针对来源:sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
    阈值类型/单机值:
    QPS(每秒钟的请求数量):当调用该api就QPS达到阈值的时候,进行限流
    线程数.当调用该api的线程数达到阈值的时候,进行限流
    是否集群:不需要集群
    流控模式:
    直接:api达到限流条件时,直接限流。分为QPS和线程数
    关联:当关联的资到阈值时,就限流自己。别人惹事,自己买单
    链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
    流控效果:
    快速失败:直接抛异常
    warm up:根据codeFactor(冷加载因子,默认3)的值,从阈值codeFactor,经过预热时长,才达到设置的QPS阈值
    重要属性:

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

    流控模式–直接:

    谷粒商城-12-p300-p338_第22张图片

    限流表现:当超过阀值,就会被降级。

    1s内多次刷新网页,localhost:8401/testA

    返回Blocked by Sentienl(flow limiting)

    流控模式–关联:

    当与A关联的资源B达到阀值后,就限流A自己
    B惹事,A挂了。支付达到阈值,限流下单接口。B阈值达到1,A就挂
    用post访问B让B忙,访问A发现挂了

    谷粒商城-12-p300-p338_第23张图片

    流控效果–预热Warm up:

    访问数量慢慢升高

    阈值初一coldFactor(默认3),经过预热时长后才会达到阈值。

    谷粒商城-12-p300-p338_第24张图片

    谷粒商城-12-p300-p338_第25张图片

    流控效果–排队等待:

    匀速排队(Ru1eConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,即让请求以均匀的速度通过对应的是漏桶算法。详细文档可以参考流量控制-匀速器模式,具体的例子可以参见PaceFlowDemo

    该方式的作用如下图所示

    谷粒商城-12-p300-p338_第26张图片

    这种方式主要用于处理间隔性突发的流量,伊消息列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的月耖则处于空闲状态,我们希系统能够在接下来的空闲期间逐渐处理这些请求,而不是第一秒就拒绝多余的请求

    331、Sentinel-熔断降级

    熔断降级

    新增降级规则:降低策略:RT

    RT(平均响应时间,秒级)

    平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级

    窗口期过后关闭断路器

    RT最大4900(更大的需要通过-Dcsp.Sentinel.statistic.max.rt=XXXX才能生效)

    异常比例(秒级)
    QPS>=5且异常比例(秒级统计)超过阈值时,触发降级,时间窗口结束后,关闭降级

    sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。

    当资源被降级后,在接下来的降级时间窗囗之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)。

    降级策略–RT

    谷粒商城-12-p300-p338_第27张图片

    降级策略–异常比例:

    异常比例(DEGRADE-GRADE-EXCEPTION-RATIO):当资源的每秒请求量>=5,并且每秒异常总数占通过的比值超过阈值(DegradeRule中的count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRu1e中的timeWindow,,以s为单位)之内,对这个方法的调用都会自动地返回。异常b阈值范围是[0.0,l.0],代表0%一100%。

    降级测录–异常数:

    异常数(DEGRADE-GRADE-EXCEPTION-COUNT):当资源近1分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若timeWindow小于60s,则结束熔断状态后仍可能再进入熔断状态。

    时间窗口一定要大于等于60秒。

    时间窗口结束后关闭降级

    localhost:8401/testE , 第一次访问绝对报错,因为除数不能为零,
    我们看到error窗口,但是达到5次报错后,进入熔断后降级。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0uG4jp95-1615737211171)(images\1597821618735.png)]

    热点Key限流

    何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的TopK数据,并对其访问进行限制。比如:

    商品ID为参数,统计一段时间内最常购买的商品ID并进行限制
    用户ID为参数,针对一段时间内频繁访问的用户ID进行限制
    参数限流会统计传入参数中的参数,并根据配置流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

    controller层写一个demo:

    @GetMapping("/testhotkey")
    @SentinelResource(value = "testhotkey", blockHandler = "deal_testhotkey")
    //这个value是随意的值,并不和请求路径必须一致
    //在填写热点限流的 资源名 这一项时,可以填 /testhotkey 或者是 @SentinelResource的value的值
    public String testHotKey(
            @RequestParam(value="p1", required = false) String p1,
            @RequestParam(value = "p2", required = false) String p2
    ){
        return "testHotKey__success";
    }
    
    //类似Hystrix 的兜底方法
    public String deal_testhotkey(String p1, String p2, BlockException e){
        return "testhotkey__fail"; 
    }
    

    谷粒商城-12-p300-p338_第28张图片

    谷粒商城-12-p300-p338_第29张图片

    说明:

    @SentinelResource :处理的是Sentine1控制台配置的违规情况,有blockHandler方法配置的兜底处理

    @RuntimeException:int age=10/0,这个是java运行时报出的运行时异异常RunTimeException,@Sentine1Resource不管

    系统规则

    一般配置在网关或者入口应用中,但是这个东西有点危险,不但值不合适,就相当于系统瘫痪。

    系统自适应限流

    Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

    系统规则包含下面几个重要的属性:

    Field 说明 默认值
    highestSystemLoad load1 触发值,用于触发自适应控制阶段 -1 (不生效)
    avgRt 所有入口流量的平均响应时间 -1 (不生效)
    maxThread 入口流量的最大并发数 -1 (不生效)
    qps 所有入口资源的 QPS -1 (不生效)
    highestCpuUsage 当前系统的 CPU 使用率(0.0-1.0) -1 (不生效)

    @SentinelResource配置

    @SentinelResource 注解,主要是指定资源名(也可以用请求路径作为资源名),和指定降级处理方法的。

    例如:

    package com.dkf.springcloud.controller;
    
    import com.alibaba.csp.sentinel.annotation.SentinelResource;
    import com.alibaba.csp.sentinel.slots.block.BlockException;
    import com.dkf.springcloud.entities.CommonResult;
    import com.dkf.springcloud.entities.Payment;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class RateLimitController {
    
        @GetMapping("/byResource")						//处理降级的方法名
        @SentinelResource(value = "byResource", blockHandler = "handleException")
        public CommonResult byResource(){
            return new CommonResult(200, "按照资源名限流测试0K", new Payment(2020L,"serial001"));
        }
    
        //降级方法
        public CommonResult handleException(BlockException e){
            return new CommonResult(444, e.getClass().getCanonicalName() + "\t 服务不可用");
        }
    }
    
    

    谷粒商城-12-p300-p338_第30张图片

    很明显,上面虽然自定义了兜底方法,但是耦合度太高,下面要解决这个问题。

    自定义全局BlockHandler处理类
    写一个 CustomerBlockHandler 自定义限流处理类:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-khnkVraZ-1615737211177)(images\1597903188558.png)]

    整合 openfeign 服务降级

    我们现在的案例是product 服务feign调用seckill服务,根据skuid 查询秒杀的商品

    * 4、使用Sentinel来保护feign远程调用:熔断;
    *    1)、调用方的熔断保护:feign.sentinel.enabled=true
    *    2)、调用方手动指定远程服务的降级策略。远程服务被降级处理。触发我们的熔断回调方法
    *    3)、超大浏览的时候,必须牺牲一些远程服务。在服务的提供方(远程服务)指定降级策略;
    *      提供方是在运行。但是不运行自己的业务逻辑,返回的是默认的降级数据(限流的数据),
    

    1、在调用方product 加上

    feign.sentinel.enabled=true
    

    2、feign 服务

    package com.atguigu.gulimall.product.feign;
    
    import com.atguigu.common.utils.R;
    import com.atguigu.gulimall.product.feign.fallback.SeckillFeignServiceFallBack;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    /**
     * @创建人: 放生
     * @创建时间: 2022/5/2
     * @描述:
     */
    @FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
    public interface SeckillFeignService {
    
        @GetMapping("/sku/seckill/{skuId}")
        R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
    
    }
    

    3、降级的方法

    package com.atguigu.gulimall.product.feign.fallback;
    
    
    import com.atguigu.common.exception.BizCodeEnume;
    import com.atguigu.common.utils.R;
    import com.atguigu.gulimall.product.feign.SeckillFeignService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    /**
     * @创建人: 放生
     * @创建时间: 2022/5/8
     * @描述:
     */
    @Slf4j
    @Component
    public class SeckillFeignServiceFallBack implements SeckillFeignService {
        @Override
        public R getSkuSeckillInfo(Long skuId) {
            log.info("熔断方法调用...getSkuSeckillInfo");
            return R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(),BizCodeEnume.TOO_MANY_REQUEST.getMsg());
        }
    }
    
    

    332、Sentinel-自定义受保护资源

    # 5、自定义受保护的资源
    *   1)、代码
    *    try(Entry entry = SphU.entry("seckillSkus")){
    *        //业务逻辑
    *    }
    *     catch(Execption e){}
    *
    *   2)、基于注解。
    *   @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
    *
    *   无论是1,2方式一定要配置被限流以后的默认返回.
    *   url请求可以设置统一返回:WebCallbackManager
    

    1、代码实现

    SeckillServiceImpl

     /**
         * blockHandler 函数会在原方法被限流/降级/系统保护的时候调用,而 fallback 函数会针对所有类型的异常。
         * @return
         */
        //返回当前时间可以参与的秒杀商品信息
      @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
        @Override
        public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
            //1、确定当前时间属于哪个秒杀场次。
            //1970 -
            long time = new Date().getTime();
    
            try(Entry entry = SphU.entry("seckillSkus")){
                Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
                for (String key : keys) {
                    //seckill:sessions:1582250400000_1582254000000
                    String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
                    String[] s = replace.split("_");
                    Long start = Long.parseLong(s[0]);
                    Long end = Long.parseLong(s[1]);
                    if (time >= start && time <= end) {
                        //2、获取这个秒杀场次需要的所有商品信息
                        List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                        List<String> list = hashOps.multiGet(range);
                        if (list != null) {
                            List<SecKillSkuRedisTo> collect = list.stream().map(item -> {
                                SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class);
    //                        redis.setRandomCode(null); 当前秒杀开始就需要随机码
                                return redis;
                            }).collect(Collectors.toList());
                            return collect;
                        }
                        break;
                    }
                }
            }catch (BlockException e){
                log.error("资源被限流,{}",e.getMessage());
            }
    
            return null;
        }
    
       public  List<SecKillSkuRedisTo> blockHandler(BlockException e){
            log.error("getCurrentSeckillSkusResource被限流了..");
            return null;
        }
    
    

    2、添加流控

    谷粒商城-12-p300-p338_第31张图片

    333、Sentinel-网关流控

    1、在gateway加入依赖

            
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
                <version>2.1.0.RELEASEversion>
            dependency>
    

    2、配置流控

    谷粒商城-12-p300-p338_第32张图片

    3、更多配置

    参考官网 网关限流 · alibaba/Sentinel Wiki (github.com)

    334、Sentinel-定制网关流控返回

    添加以下配置类

    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.BizCodeEnume;
    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;
    /**
     * @创建人: 放生
     * @创建时间: 2022/5/8
     * @描述:
     */
    
    @Configuration
    public class SentinelGatewayConfig {
    
        //TODO 响应式编程
        //GatewayCallbackManager
        public SentinelGatewayConfig(){
            GatewayCallbackManager.setBlockHandler(new BlockRequestHandler(){
                //网关限流了请求,就会调用此回调  Mono Flux
                @Override
                public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
    
                    R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
                    String errJson = JSON.toJSONString(error);
    
    //                Mono aaa = Mono.just("aaa");
                    Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);
                    return body;
                }
            });
    
    //        FlowRule flowRule = new FlowRule();
    //        flowRule.setRefResource("gulimall_seckill_route");
            flowRule.set
    //        FlowRuleManager.loadRules(Arrays.asList(flowRule));
        }
    }
    
    

    335、Sleuth-链路追踪-基本概念&整合

    由于微服务项目模块众多,相互之间的调用关系十分复杂,因此为了分析工作过程中的调用关系,需要使用zipkin来进行链路追踪

    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 时间戳便可以得到整个请求所消耗的时间。

    官方文档:

    https://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.1.3.RELEASE/single/spring-cloud-sleuth.html

    如果服务调用顺序如下

    谷粒商城-12-p300-p338_第33张图片

    Span 之间的父子关系如下:

    谷粒商城-12-p300-p338_第34张图片

    3 、整合 Sleuth

    在common服务中导入

    <dependency> 
      <groupId>org.springframework.cloudgroupId> 
      <artifactId>spring-cloud-starter-sleuthartifactId> 
    dependency>
    

    每一个服务添加

    logging.level.org.springframework.cloud.openfeign=debug
    logging.level.org.springframework.cloud.sleuth=debug
    

    4、测试

    启动测试的各个服务,然后服务调用的时候查看控制台,就能输出相关的日志了

    3、发起一次远程调用,观察控制台 
    DEBUG [user-service,541450f08573fff5,541450f08573fff5,false] 
    user-service:服务名 541450f08573fff5:是 TranceId,一条链路中,只有一个 TranceId 541450f08573fff5:是 spanId,链路中的基本工作单元 id false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察
    

    336、Sleuth-链路追踪-整合Zipkin效果

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

    https://zipkin.io/

    谷粒商城-12-p300-p338_第35张图片

    1、docker 安装 zipkin 服务器

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

    2、导入依赖

    放在common模块中

    
    <dependency> 
    	<groupId>org.springframework.cloudgroupId> 
    	<artifactId>spring-cloud-starter-zipkinartifactId> 
    dependency> 
    zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用
    

    3、添加 zipkin 相关配置

    每一个服务都要加入

    spring.zipkin.base-url=http://192.168.56.10:9411/
    spring.zipkin.discovery-client-enabled=false
    spring.zipkin.sender.type=web
    spring.sleuth.sampler.probability=1
    

    yaml 配置就采用

    spring:
        zipkin:
          base-url: http://192.168.56.10:9411
          sender:
            type: web
          # 取消nacos对zipkin的服务发现
          discovery-client-enabled: false
        #采样取值介于 0到1之间,1则表示全部收集
        sleuth:
          sampler:
            probability: 1
    

    发送远程请求,测试 zipkin。服务调用链追踪信息统计

    谷粒商城-12-p300-p338_第36张图片

    image-20220508180911248

    Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据 持久化,自然就是得将数据存储至数据库。好在 Zipkin 支持将数据存储至:

    • 内存(默认)

    • MySQL

    • Elasticsearch

    • Cassandra

    Zipkin 数据持久化相关的官方文档地址如下:

    https://github.com/openzipkin/zipkin#storage-component

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

    据库的官方文档如下:

    elasticsearch-storage:

    https://github.com/openzipkin/zipkin/tree/master/zipkin-server#elasticsearch-storage

    zipkin-storage/elasticsearch

    https://github.com/openzipkin/zipkin/tree/master/zipkin-storage/elasticsearch

    通过 docker 的方式

    docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200 openzipkin/zipkin-dependencies
    
    环境变量
    STORAGE_TYPE 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详
    见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables
    ES_HOSTS Elasticsearch地址,多个使用,分隔。默认 http∶//localhost∶9200
    ES_PIPELINE 指定span被索引之前的pipeline(pipeline是Elasticsearch的概念)
    ES_TIMEOUT 连接Elasticsearch的超时时间,单位是毫秒;默认10000(10秒)
    ES_INDEX Zipkin所使用的索引(Zipkin会每天建索引)前缀,默认是 zipkin
    ES_DATE_SEPARATOR Zipkin建立索引的日期分隔符,默认是-
    ES_INDEX_SHARDS shard(shard是Elasticsearch的概念)个数,默认5
    ES_INDEX_REPLICAS 副本(replica是Elasticsearch的概念)个数,默认1
    ES_USERNAME/ES_PASSWORD Elasticsearch账号密码
    ES_HTTP_LOGGING 控制Elasticsearch Api的日志级别,可选项为BASIC、HEADERS、BODY

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

    环境变量 含义
    STORAGE_TYPE 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详
    见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables
    ES_INDEX 生成每日索引名称时使用的索引前缀。默认为"zipkin"
    ES_DATE_SEPARATOR 在索引中生成日期时使用的分隔符。默认为’-',所以查询的索引看起来像zipkin-yyy-DD-mm,可以改为".",这样查询索引就变成zipkin-yy.MM.dd。示例∶ ES_DATE_SEPARATOR=.
    ES_HOSTS ElasticSearch主机列表,多个主机使用逗号分隔。默认为 localhost∶9200
    ES_NODES_WAN_ONLY 如设为true,则表示仅使用ES_HOSTS所设置的值,默认为false。当
    ElasticSearch集群运行在Docker中时,可将该环境变量设为true

    337、Sleuth-链路追踪-Zipkin界面分析

    谷粒商城-12-p300-p338_第37张图片

    谷粒商城-12-p300-p338_第38张图片

    谷粒商城-12-p300-p338_第39张图片

    338、分布式高级篇总结

    信息统计

    [外链图片转存中…(img-i4xtgoWc-1652453950665)]

    [外链图片转存中…(img-GrnFn7be-1652453950665)]

    [外链图片转存中…(img-rSkPzWMD-1652453950666)]

    Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据 持久化,自然就是得将数据存储至数据库。好在 Zipkin 支持将数据存储至:

    • 内存(默认)

    • MySQL

    • Elasticsearch

    • Cassandra

    Zipkin 数据持久化相关的官方文档地址如下:

    https://github.com/openzipkin/zipkin#storage-component

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

    据库的官方文档如下:

    elasticsearch-storage:

    https://github.com/openzipkin/zipkin/tree/master/zipkin-server#elasticsearch-storage

    zipkin-storage/elasticsearch

    https://github.com/openzipkin/zipkin/tree/master/zipkin-storage/elasticsearch

    通过 docker 的方式

    docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200 openzipkin/zipkin-dependencies
    
    环境变量
    STORAGE_TYPE 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详
    见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables
    ES_HOSTS Elasticsearch地址,多个使用,分隔。默认 http∶//localhost∶9200
    ES_PIPELINE 指定span被索引之前的pipeline(pipeline是Elasticsearch的概念)
    ES_TIMEOUT 连接Elasticsearch的超时时间,单位是毫秒;默认10000(10秒)
    ES_INDEX Zipkin所使用的索引(Zipkin会每天建索引)前缀,默认是 zipkin
    ES_DATE_SEPARATOR Zipkin建立索引的日期分隔符,默认是-
    ES_INDEX_SHARDS shard(shard是Elasticsearch的概念)个数,默认5
    ES_INDEX_REPLICAS 副本(replica是Elasticsearch的概念)个数,默认1
    ES_USERNAME/ES_PASSWORD Elasticsearch账号密码
    ES_HTTP_LOGGING 控制Elasticsearch Api的日志级别,可选项为BASIC、HEADERS、BODY

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

    环境变量 含义
    STORAGE_TYPE 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详
    见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables
    ES_INDEX 生成每日索引名称时使用的索引前缀。默认为"zipkin"
    ES_DATE_SEPARATOR 在索引中生成日期时使用的分隔符。默认为’-',所以查询的索引看起来像zipkin-yyy-DD-mm,可以改为".",这样查询索引就变成zipkin-yy.MM.dd。示例∶ ES_DATE_SEPARATOR=.
    ES_HOSTS ElasticSearch主机列表,多个主机使用逗号分隔。默认为 localhost∶9200
    ES_NODES_WAN_ONLY 如设为true,则表示仅使用ES_HOSTS所设置的值,默认为false。当
    ElasticSearch集群运行在Docker中时,可将该环境变量设为true

    337、Sleuth-链路追踪-Zipkin界面分析

    [外链图片转存中…(img-5D5seBop-1652453950666)]

    [外链图片转存中…(img-0IFPjln2-1652453950666)]

    [外链图片转存中…(img-NNh56s4w-1652453950666)]

    338、分布式高级篇总结

    你可能感兴趣的:(java,https,安全,http)