谷粒商城-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/ 全部文档=>电脑网image-20220507191654615站支付文档;下载 demo

配置使用沙箱进行测试

1、使用 RSA 工具生成签名

2、下载沙箱版钱包

3、运行官方 demo 进行测试

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

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

1、公钥私钥

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

加密是指:

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

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

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

  • 加密的目的是:

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

签名:

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

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

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

验签

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

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

2、支付宝支付流程

image-20220507191912626

加密-对称加密

image-20220508114402723

加密-非对称加密

image-20220508114443106

支付宝的加解密过程

image-20220508114609336

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/

image-20220508114520304

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

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

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

1、引入依赖

    <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
        <!--        导入支付宝的SDK-->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.9.28.ALL</version>
        </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

image-20220507221950677

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 、秒杀流程

见秒杀流程图

image-20220508111940609

image-20220508112205786

秒杀架构图

image-20220508114704189

秒杀系统关注的问题

  • 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、搭建秒杀服务

image-20220507225627871

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

5、pom

<dependencies>

        <dependency>
            <groupId>com.atguigu.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <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>
        <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-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</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>

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、商城业务-秒杀服务-秒杀商品上架

image-20220508111940609

代码实现

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

image-20220508114808044

加分布式锁 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) {
        $("<li οnclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfoVo.skuDefaultImg + "' />"))
                .append($("<p>"+item.skuInfoVo.skuTitle+"</p>"))
                .append($("<span>" + item.seckillPrice + "</span>"))
                .append($("<s>" + item.skuInfoVo.price + "</s>"))
                .appendTo("#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、商城业务-秒杀服务-秒杀系统设计

image-20220508114914307

image-20220508114935565

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、商城业务-秒杀服务-秒杀效果完成

image-20220508141921391

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </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<String, Object> arguments
        return new Queue("order.seckill.order.queue",true,false,false);
    }

    @Bean
    public Binding orderSeckillOrderQueueBinding(){
        /**
         * String destination, DestinationType destinationType, String exchange, String routingKey,
         * 			Map<String, Object> arguments
         */
        return new Binding("order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);
    }

}

4、在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,甚至服务名称作为资源名来标示资源。

  • 规则

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

image-20210620234415040

下载地址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

image-20220508160542180

image-20220508160637092

1、引入依赖

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>

2、下载sentinel的控制台

Sentinel 控制台

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

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

image-20220508161005949

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.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
management.endpoints.web.exposure.include=*

image-20220508162249338

我们实现自定义的返回

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>
        <!--        mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.12</version>
        </dependency>


        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!--    导入mysql驱动    -->
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>

        <!--        服务注册/发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--        配置中心来做配置管理-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
            <version>1.7.1</version>
        </dependency>

        <!--        链路追踪-->
<!--        <dependency>-->
<!--            <groupId>org.springframework.cloud</groupId>-->
<!--            <artifactId>spring-cloud-starter-sleuth</artifactId>-->
<!--        </dependency>-->

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



        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>

        <!--        导入org.apache.http.client.HttpClient依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-util</artifactId>
            <version>9.3.7.v20160115</version>
        </dependency
    </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请求进行限制

流控模式–直接:

image-20210620184144366

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

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

返回Blocked by Sentienl(flow limiting)

流控模式–关联:

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

image-20210620184210908

流控效果–预热Warm up:

访问数量慢慢升高

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

image-20210620184227258

image-20210620184255169

流控效果–排队等待:

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

该方式的作用如下图所示

image-20210620184309360

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

331、Sentinel-熔断降级

熔断降级

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

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

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

窗口期过后关闭断路器

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

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

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

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

降级策略–RT

image-20210620184334748

降级策略–异常比例:

异常比例(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"; 
}

image-20210620184415680

image-20210620184428080

说明:

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

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

系统规则

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

系统自适应限流

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

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

Field说明默认值
highestSystemLoadload1 触发值,用于触发自适应控制阶段-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 服务不可用");
    }
}

image-20210620184819954

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

自定义全局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、添加流控

image-20220508172448366

333、Sentinel-网关流控

1、在gateway加入依赖

        <!-- https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-spring-cloud-gateway-adapter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>

2、配置流控

image-20220508174148928

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

如果服务调用顺序如下

image-20220508175324203

Span 之间的父子关系如下:

image-20220508180036664

3 、整合 Sleuth

在common服务中导入

<dependency> 
  <groupId>org.springframework.cloud</groupId> 
  <artifactId>spring-cloud-starter-sleuth</artifactId> 
</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/

image-20220508180718058

1、docker 安装 zipkin 服务器

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

2、导入依赖

放在common模块中


<dependency> 
	<groupId>org.springframework.cloud</groupId> 
	<artifactId>spring-cloud-starter-zipkin</artifactId> 
</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。服务调用链追踪信息统计

image-20220508181509433

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_HOSTSElasticsearch地址,多个使用,分隔。默认 http∶//localhost∶9200
ES_PIPELINE指定span被索引之前的pipeline(pipeline是Elasticsearch的概念)
ES_TIMEOUT连接Elasticsearch的超时时间,单位是毫秒;默认10000(10秒)
ES_INDEXZipkin所使用的索引(Zipkin会每天建索引)前缀,默认是 zipkin
ES_DATE_SEPARATORZipkin建立索引的日期分隔符,默认是-
ES_INDEX_SHARDSshard(shard是Elasticsearch的概念)个数,默认5
ES_INDEX_REPLICAS副本(replica是Elasticsearch的概念)个数,默认1
ES_USERNAME/ES_PASSWORDElasticsearch账号密码
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_HOSTSElasticSearch主机列表,多个主机使用逗号分隔。默认为 localhost∶9200
ES_NODES_WAN_ONLY如设为true,则表示仅使用ES_HOSTS所设置的值,默认为false。当
ElasticSearch集群运行在Docker中时,可将该环境变量设为true

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

image-20220508183629647

image-20220508183654839

image-20220508183803674

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_HOSTSElasticsearch地址,多个使用,分隔。默认 http∶//localhost∶9200
ES_PIPELINE指定span被索引之前的pipeline(pipeline是Elasticsearch的概念)
ES_TIMEOUT连接Elasticsearch的超时时间,单位是毫秒;默认10000(10秒)
ES_INDEXZipkin所使用的索引(Zipkin会每天建索引)前缀,默认是 zipkin
ES_DATE_SEPARATORZipkin建立索引的日期分隔符,默认是-
ES_INDEX_SHARDSshard(shard是Elasticsearch的概念)个数,默认5
ES_INDEX_REPLICAS副本(replica是Elasticsearch的概念)个数,默认1
ES_USERNAME/ES_PASSWORDElasticsearch账号密码
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_HOSTSElasticSearch主机列表,多个主机使用逗号分隔。默认为 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、分布式高级篇总结

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值