目录

十一、支付

1、内网穿透

1)、简介

2)、使用场景

3)、内网穿透常用软件和安装

2、支付整合

1)、支付宝加密原理

2)、配置支付宝沙箱环境

3)、订单支付与同步通知

4)、订单列表页渲染完成

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

6)、收单

十二、秒杀服务

1、秒杀服务后台管理系统调整

2、搭建秒杀服务环境

3、定时任务

4、商品上架

1)、秒杀架构思路

2)、存储模型设计

3)、定时上架

4)、获取最近三天的秒杀信息

5)、在redis中保存秒杀场次信息

6)、在redis中保存秒杀商品信息

5、幂等性保证

6、获取当前的秒杀商品

1)、获取当前秒杀商品

2)首页获取并拼装数据

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

8、秒杀系统设计

9、登录检查

10、秒杀

1)、秒杀接口

2)、创建订单

11、秒杀页面完成


十一、支付

1、内网穿透

1)、简介

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

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

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

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

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

2)、使用场景

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

2、智慧互联

3、远程控制

4、私有云

3)、内网穿透常用软件和安装

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

第一步:登录

第二步:安装客户端

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

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

第四步:新建隧道

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

2、支付整合

1)、支付宝加密原理

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

2)、配置支付宝沙箱环境

1、导入依赖

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

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

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

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

@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {//在支付宝创建的应用的idprivate   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.doprivate  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;}

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

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

添加配置

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

修改支付页的支付宝按钮

          <li><img src="/static/order/pay/img/zhifubao.png" style="weight:auto;height:30px;" alt=""><a th:href="'http://order.gulimall.com/payOrder?orderSn='+${submitOrderResp.order.orderSn}">支付宝</a></li>

3)、订单支付与同步通知

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

@Controller
public class PayWebController {@AutowiredAlipayTemplate alipayTemplate;@AutowiredOrderService orderService;@ResponseBody@GetMapping("/payOrder")public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {//        PayVo payVo = new PayVo();
//        payVo.setBody();//订单备注
//        payVo.setOut_trade_no();//订单号
//        payVo.setSubject();//订单主题
//        payVo.setTotal_amount();//订单金额PayVo payVo = orderService.getOrderPay(orderSn);//返回的是一个页面。将此页面直接交给浏览器就行String pay = alipayTemplate.pay(payVo);System.out.println(pay);return "hello";}
}

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

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

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

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

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

运行结果

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

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

@ResponseBody@GetMapping(value = "payOrder", produces = "text/html")public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {//        PayVo payVo = new PayVo();
//        payVo.setBody();//订单备注
//        payVo.setOut_trade_no();//订单号
//        payVo.setSubject();//订单主题
//        payVo.setTotal_amount();//订单金额PayVo payVo = orderService.getOrderPay(orderSn);//返回的是一个页面。将此页面直接交给浏览器就行String pay = alipayTemplate.pay(payVo);System.out.println(pay);return pay;}

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

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

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

gulimall-member

添加thymeleaf模板引擎

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

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

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

修改静态资源访问路径

做登录拦截添加SpringSession依赖

        <!--整合SpringSession完成session共享问题--><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><!--引入redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency>

添加配置

spring.session.store-type=redisspring.redis.host=172.20.10.9

主启动类添加SpringSession自动开启

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

@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer(){return new GenericJackson2JsonRedisSerializer();}
}

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

@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();AntPathMatcher matcher = new AntPathMatcher();boolean status = matcher.match("/member/**", requestURI);if (status){return true;}MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if (attribute != null){loginUser.set(attribute);return true;}else {//没登录就去登录request.getSession().setAttribute("msg","请先进行登录");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}

把登录拦截器配置到spring里

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

@Configuration
public class MemberWebConfig implements WebMvcConfigurer {@AutowiredLoginUserInterceptor loginUserInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}
}

在gulimall-gateway配置路由:

        - id: gulimall_member_routeuri: lb://gulimall-memberpredicates:- Host=member.gulimall.com

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

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

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

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

4)、订单列表页渲染完成

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

@Controller
public class MemberWebController {@AutowiredOrderFeignService orderFeignService;@GetMapping("/memberOrder.html")public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){//查出当前登录用户的所有订单列表数据Map<String,Object> page = new HashMap<>();page.put("page",pageNum.toString());//分页查询当前用户的所有订单及对应订单项R r = orderFeignService.listWithItem(page);model.addAttribute("orders",r);return "orderList";}
}

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

@FeignClient("gulimall-order")
public interface OrderFeignService {@PostMapping("/order/order/listWithItem")public R listWithItem(@RequestBody Map<String, Object> params);
}

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

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

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

远程服务:gulimall-order

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

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

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

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

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

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

修改OrderEntity

修改orderList.html

        <table class="table" th:each="order:${orders.page.list}"><tr><td colspan="7" style="background:#F7F7F7" ><span style="color:#AAAAAA">2017-12-09 20:50:10</span><span><ruby style="color:#AAAAAA">订单号:</ruby>[[${order.orderSn}]] 70207298274</span><span>谷粒商城<i class="table_i"></i></span><i class="table_i5 isShow"></i></td></tr><tr class="tr" th:each="item,itemStat:${order.itemEntities}"><td colspan="3" style="border-right: 1px solid #ccc"><img style="height: 60px; width: 60px;" th:src="${item.skuPic}" alt="" class="img"><div><p style="width: 242px; height: auto;overflow: auto">[[${item.skuName}]]</p><div><i class="table_i4"></i>找搭配</div></div><div style="margin-left:15px;">x[[${item.skuQuantity}]]</div><div style="clear:both"></div></td><td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">[[${order.receiverName}]]<i><i class="table_i1"></i></i></td><td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}" style="padding-left:10px;color:#AAAAB1;"><p style="margin-bottom:5px;">总额 ¥[[${order.payAmount}]]</p><hr style="width:90%;"><p>在线支付</p></td><td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}"><ul><li style="color:#71B247;" th:if="${order.status==0}">待付款</li><li style="color:#71B247;" th:if="${order.status==1}">已付款</li><li style="color:#71B247;" th:if="${order.status==2}">已发货</li><li style="color:#71B247;" th:if="${order.status==3}">已完成</li><li style="color:#71B247;" th:if="${order.status==4}">已取消</li><li style="color:#71B247;" th:if="${order.status==5}">售后中</li><li style="color:#71B247;" th:if="${order.status==6}">售后完成</li><li style="margin:4px 0;" class="hide"><i class="table_i2"></i>跟踪<i class="table_i3"></i><div class="hi"><div class="p-tit">普通快递   运单号:390085324974</div><div class="hideList"><ul><li>[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您的快件已签收,感谢您使用韵达快递)签收</li><li>[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您的快件已签收,感谢您使用韵达快递)签收</li><li>[北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描</li><li>[北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636</li></ul></div></div></li><li class="tdLi">订单详情</li></ul></td><td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}"><button>确认收货</button><p style="margin:4px 0; ">取消订单</p><p>催单</p></td></tr></table>

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

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

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

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

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

    /payed/notify异步通知转发至订单服务

设置异步通知的地址

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

修改内网穿透

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

配置好之后,重启nginx

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

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

再次重启niginx

修改登录拦截器给他放行

@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();AntPathMatcher matcher = new AntPathMatcher();boolean status = matcher.match("/order/order/status/**", requestURI);boolean payed = matcher.match("/payed/**", requestURI);if (status || payed)return true;MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if (attribute != null){loginUser.set(attribute);return true;}else {//没登录就去登录request.getSession().setAttribute("msg","请先进行登录");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}

2)验证签名

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

@RestController
public class OrderPayedListener {@AutowiredOrderService orderService;@AutowiredAlipayTemplate alipayTemplate;/*** 支付宝成功异步通知* @param request* @return*/@PostMapping("/payed/notify")public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) 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.handlePayResult(vo);return "success";}else {System.out.println("支付宝异步通知验签失败");return "error";}}}

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

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

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

 String handlePayResult(PayAsyncVo vo);

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

    /*** 处理支付成功返回结果* @param vo* @return*/@Overridepublic String handlePayResult(PayAsyncVo vo) {//1、保存交易流水PaymentInfoEntity infoEntity = new PaymentInfoEntity();infoEntity.setAlipayTradeNo(vo.getTrade_no());infoEntity.setAlipayTradeNo(vo.getOut_trade_no());infoEntity.setPaymentStatus(vo.getTrade_status());infoEntity.setCallbackTime(vo.getNotify_time());paymentInfoService.save(infoEntity);//2、修改订单状态信息if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")){//支付成功状态String outTradeNo = vo.getOut_trade_no();this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());}return "success";}

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

@Mapper
public interface OrderDao extends BaseMapper<OrderEntity> {void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);}

OrderDao.xml

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

6)、收单

添加超时时间

十二、秒杀服务

1、秒杀服务后台管理系统调整

配置网关

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

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

@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {@Overridepublic PageUtils queryPage(Map<String, Object> params) {QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();//场次id不是nullString promotionSessionId = (String) params.get("promotionSessionId");if (!StringUtils.isEmpty(promotionSessionId)){queryWrapper.eq("promotion_session_id",promotionSessionId);}IPage<SeckillSkuRelationEntity> page = this.page(new Query<SeckillSkuRelationEntity>().getPage(params),queryWrapper);return new PageUtils(page);}

2、搭建秒杀服务环境

搭建秒杀服务环境

1)、导入pom.xml依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-seckill</artifactId><version>0.0.1-SNAPSHOT</version><name>gulimall-seckill</name><description>秒杀</description><properties><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR8</spring-cloud.version></properties><dependencies><!--以后使用redis.client作为所有分布式锁,分布式对象等功能框架--><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.4</version></dependency><dependency><groupId>com.auguigu.gulimall</groupId><artifactId>gulimall-commom</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build><repositories><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url></repository></repositories></project>

2)、添加配置

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

3)、主启动类添加注解

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {public static void main(String[] args) {SpringApplication.run(GulimallSeckillApplication.class, args);}}

3、定时任务

定时任务

1、@EnableScheduling 开启定时任务

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

3、自动配置类 TaskSchedulingAutoConfiguration

异步任务

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

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

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

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

配置定时任务参数

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

4、商品上架

1)、秒杀架构思路

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

2)、存储模型设计

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

import lombok.Data;
import java.util.Date;
import java.util.List;/*** @Description: SeckillSkuRedisTo* @Author: WangTianShun* @Date: 2020/12/13 10:27* @Version 1.0*/
@Data
public class SeckillSessionWithSkus {private Long id;/*** 场次名称*/private String name;/*** 每日开始时间*/private Date startTime;/*** 每日结束时间*/private Date endTime;/*** 启用状态*/private Integer status;/*** 创建时间*/private Date createTime;private List<SeckillSkuVo> relationEntities;}

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

package com.atguigu.gulimall.seckill.vo;import lombok.Data;import java.math.BigDecimal;/*** @Description: SkuInfoVo* @Author: WangTianShun* @Date: 2020/12/13 10:59* @Version 1.0*/
@Data
public class SkuInfoVo {private Long skuId;/*** spuId*/private Long spuId;/*** sku名称*/private String skuName;/*** sku介绍描述*/private String skuDesc;/*** 所属分类id*/private Long catalogId;/*** 品牌id*/private Long brandId;/*** 默认图片*/private String skuDefaultImg;/*** 标题*/private String skuTitle;/*** 副标题*/private String skuSubtitle;/*** 价格*/private BigDecimal price;/*** 销量*/private Long saleCount;
}

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

package com.atguigu.gulimall.seckill.vo;import lombok.Data;import java.math.BigDecimal;/*** @Description: SeckillSkuVo* @Author: WangTianShun* @Date: 2020/12/13 10:30* @Version 1.0*/
@Data
public class SeckillSkuVo {private Long id;/*** 活动id*/private Long promotionId;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 秒杀总量*/private Integer seckillCount;/*** 每人限购数量*/private Integer seckillLimit;/*** 排序*/private Integer seckillSort;}

添加代码如下:

package com.atguigu.gulimall.seckill.vo;import lombok.Data;
import java.util.Date;
import java.util.List;/*** @Description: SeckillSkuRedisTo* @Author: WangTianShun* @Date: 2020/12/13 10:27* @Version 1.0*/
@Data
public class SeckillSessionWithSkus {private Long id;/*** 场次名称*/private String name;/*** 每日开始时间*/private Date startTime;/*** 每日结束时间*/private Date endTime;/*** 启用状态*/private Integer status;/*** 创建时间*/private Date createTime;private List<SeckillSkuVo> relationEntities;}

3)、定时上架

  • 开启对定时任务的支持

配置定时任务 添加“com.atguigu.gulimall.seckill.config.ScheduledConfig”类,代码如下

@EnableAsync // 开启对异步的支持,防止定时任务之间相互阻塞
@EnableScheduling // 开启对定时任务的支持
@Configuration
public class ScheduledConfig {
}
  • 每天凌晨三点远程调用coupon服务上架最近三天的秒杀商品
  • 由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法

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

@Slf4j
@Service
public class SeckillSkuScheduled {@AutowiredSeckillService seckillService;//TODO 幂等性处理@Scheduled(cron = "0 0 3 * * ?")public void uploadSeckillSkuLatest3Days(){//重复上架无需处理log.info("上架秒杀的信息......");seckillService.uploadSeckillSkuLatest3Days();}
}

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

public interface SeckillService {void uploadSeckillSkuLatest3Days();
}

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

@Service
public class SeckillServiceImpl implements SeckillService {@AutowiredCouponFeignService couponFeignService;@AutowiredStringRedisTemplate redisTemplate;@AutowiredProductFeignService productFeignService;@AutowiredRedissonClient 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:";//+商品随机码@Overridepublic void uploadSeckillSkuLatest3Days() {// 1、扫描最近三天需要参与秒杀的活动R session = couponFeignService.getLasts3DaySession();if (session.getCode() == 0){// 上架商品List<SeckillSessionWithSkus> data = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {});// 缓存到redis// 1、缓存活动信息saveSessionInfos(data);// 2、缓存获得关联商品信息saveSessionSkuInfos(data);}}}

4)、获取最近三天的秒杀信息

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

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

@FeignClient("gulimall-coupon")
public interface CouponFeignService {@GetMapping("/coupon/seckillsession/lasts3DaySession")R getLasts3DaySession();
}

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

    @GetMapping("/lasts3DaySession")public R getLasts3DaySession(){List<SeckillSessionEntity> session = seckillSessionService.getLasts3DaySession();return R.ok().setData(session);}

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

 List<SeckillSessionEntity> getLasts3DaySession();

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

    @Overridepublic List<SeckillSessionEntity> getLasts3DaySession() {//计算最近三天LocalDate now = LocalDate.now();LocalDate plus = now.plusDays(3);List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));if (null != list && 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.setRelationEntities(relationEntities);return session;}).collect(Collectors.toList());return collect;}return null;}/*** 起始时间* @return*/private String  startTime(){LocalDate now = LocalDate.now();LocalTime time = LocalTime.MIN;LocalDateTime start = LocalDateTime.of(now, time);String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return format;}/*** 结束时间* @return*/private String endTime(){LocalDate now = LocalDate.now();LocalDate localDate = now.plusDays(2);LocalTime time = LocalTime.MIN;LocalDateTime end = LocalDateTime.of(localDate, time);String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return format;}

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

@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {@Overridepublic PageUtils queryPage(Map<String, Object> params) {QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();//场次id不是nullString promotionSessionId = (String) params.get("promotionSessionId");if (!StringUtils.isEmpty(promotionSessionId)){queryWrapper.eq("promotion_session_id",promotionSessionId);}IPage<SeckillSkuRelationEntity> page = this.page(new Query<SeckillSkuRelationEntity>().getPage(params),queryWrapper);return new PageUtils(page);}}

5)、在redis中保存秒杀场次信息

   private void saveSessionInfos(List<SeckillSessionWithSkus> sessions){if (!CollectionUtils.isEmpty(sessions)){sessions.stream().forEach(session -> {Long startTime = session.getStartTime().getTime();Long endTime = session.getEndTime().getTime();String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;Boolean hasKey = redisTemplate.hasKey(key);if (!hasKey){List<String> collect = session.getRelationEntities().stream().map(item -> item.getPromotionId().toString() +"_"+ item.getSkuId().toString()).collect(Collectors.toList());// 缓存活动信息redisTemplate.opsForList().leftPushAll(key, collect);}});}}

6)、在redis中保存秒杀商品信息

    private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions){// 准备hash操作BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);sessions.stream().forEach(session -> {session.getRelationEntities().stream().forEach(seckillSkuVo -> {// 缓存商品SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();// 1、sku的基本信息R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());if (0  == r.getCode()){SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {});redisTo.setSkuInfo(skuInfo);}// 2、sku的秒杀信息BeanUtils.copyProperties(seckillSkuVo, redisTo);// 3、设置当前商品的秒杀时间信息redisTo.setStartTime(session.getStartTime().getTime());redisTo.setEndTime(session.getEndTime().getTime());// 4、随机码String token = UUID.randomUUID().toString().replace("_", "");redisTo.setRandomCode(token);// 5、使用库存作为分布式信号量 限流RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);// 商品可以秒杀的数量作为信号量semaphore.trySetPermits(seckillSkuVo.getSeckillCount());String jsonString = JSON.toJSONString(redisTo);ops.put(seckillSkuVo.getSkuId(), jsonString);});});}

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

@FeignClient("gulimall-product")
public interface ProductFeignService {@RequestMapping("/product/skuinfo/info/{skuId}")R getSkuInfo(@PathVariable("skuId") Long skuId);
}

5、幂等性保证

  • 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态

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

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

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

private void saveSessionInfos(List<SeckillSessionWithSkus> sessions){sessions.stream().forEach(session -> {Long startTime = session.getStartTime().getTime();Long endTime = session.getEndTime().getTime();String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;Boolean hasKey = redisTemplate.hasKey(key);if (!hasKey){List<String> collect = session.getRelationEntities().stream().map(item -> item.getPromotionId().toString() +"_"+ item.getSkuId().toString()).collect(Collectors.toList());// 缓存活动信息redisTemplate.opsForList().leftPushAll(key, collect);}});}private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions){// 准备hash操作BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);sessions.stream().forEach(session -> {// 随机码String token = UUID.randomUUID().toString().replace("_", "");session.getRelationEntities().stream().forEach(seckillSkuVo -> {if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())){// 缓存商品SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();// 1、sku的基本信息R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());if (0  == r.getCode()){SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {});redisTo.setSkuInfo(skuInfo);}// 2、sku的秒杀信息BeanUtils.copyProperties(seckillSkuVo, redisTo);// 3、设置当前商品的秒杀时间信息redisTo.setStartTime(session.getStartTime().getTime());redisTo.setEndTime(session.getEndTime().getTime());// 4、随机码redisTo.setRandomCode(token);String jsonString = JSON.toJSONString(redisTo);ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);// 如果当前这个场次的商品的库存信息已经上架就不需要上架// 5、使用库存作为分布式信号量 限流RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);// 商品可以秒杀的数量作为信号量semaphore.trySetPermits(seckillSkuVo.getSeckillCount());}});});

6、获取当前的秒杀商品

1)、获取当前秒杀商品

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

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

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

    /*** 返回当前时间可以参与的秒杀商品信息* @return*/List<SeckillSkuRedisTo> getCurrentSeckillSkus();

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

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

2)首页获取并拼装数据

配置网关

        - id: gulimall_seckill_routeuri: lb://gulimall-seckillpredicates:- Host=seckill.gulimall.com

配置域名

<div class="swiper-slide"><!-- 动态拼装秒杀商品信息 --><ul id="seckillSkuContent"></ul>
</div>
<script type="text/javascript">function search() {var keyword=$("#searchText").val()window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;}$.get("http://seckill.gulimall.com/getCurrentSeckillSkus", function (res) {if (res.data.length > 0) {res.data.forEach(function (item) {$("<li onclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />")).append($("<p>"+item.skuInfo.skuTitle+"</p>")).append($("<span>" + item.seckillPrice + "</span>")).append($("<s>" + item.skuInfo.price + "</s>")).appendTo("#seckillSkuContent");})}})function toDetail(skuId) {location.href = "http://item.gulimall.com/" + skuId + ".html";}

首页效果展示

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

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

@Overridepublic SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {SkuItemVo skuItemVo = new SkuItemVo();// 使用异步编排CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {//1、sku基本信息获取    pms_sku_infoSkuInfoEntity info = getById(skuId);skuItemVo.setInfo(info);return info;}, executor);CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {//3、获取spu的销售属性组合List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());skuItemVo.setSaleAttr(saleAttrVos);}, executor);CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {//4、获取spu的介绍 pms_spu_info_descSpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());skuItemVo.setDesc(desc);}, executor);CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {//5、获取spu的规格参数信息List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());skuItemVo.setGroupAttrs(attrGroupVos);}, executor);CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {//2、sku的图片信息      pms_sku_imagesList<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);skuItemVo.setImages(images);}, executor);CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {// 3、查询当前sku是否参与秒杀优惠R r = seckillFeignService.getSkuSecKillInfo(skuId);if (0 == r.getCode()) {SeckillInfoVo skillInfo = r.getData(new TypeReference<SeckillInfoVo>() {});skuItemVo.setSeckillInfo(skillInfo);}}, executor);//等待所有任务都完成CompletableFuture.allOf(saleAttrFuture, descFuture, baseAttrFuture, imageFuture, seckillFuture).get();return skuItemVo;}

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

@Data
public class SeckillInfoVo {private Long id;/*** 活动id*/private Long promotionId;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 秒杀总量*/private Integer seckillCount;/*** 每人限购数量*/private Integer seckillLimit;/*** 排序*/private Integer seckillSort;/*** 当前商品秒杀的开始时间*/private Long startTime;/*** 当前商品秒杀的结束时间*/private Long endTime;/*** 当前商品秒杀的随机码*/private String randomCode;
}

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

远程调用gulimall-seckill

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

@FeignClient("gulimall-seckill")
public interface SeckillFeignService {@GetMapping("/sku/seckill/{skuId}")R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}

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

    @GetMapping("/sku/seckill/{skuId}")public R getSkuSecKillInfo(@PathVariable("skuId") Long skuId){SeckillSkuRedisTo to = seckillService.getSkuSecKillInfo(skuId);return R.ok().setData(to);}

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

SeckillSkuRedisTo getSkuSecKillInfo(Long skuId);

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

    @Overridepublic SeckillSkuRedisTo getSkuSecKillInfo(Long skuId) {// 1、找到所有需要参与秒杀的商品的keyBoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);Set<String> keys = hashOps.keys();if (null != keys){//6_4String regx = "\\d_" + skuId;for (String key : keys) {if (Pattern.matches(regx, key)){String json = hashOps.get(key);SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);//随机码long current = new Date().getTime();if (current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()){}else {skuRedisTo.setRandomCode(null);}return skuRedisTo;}}}return null;}

item.html

         <div class="box-summary clear"><ul><li>京东价</li><li><span>¥</span><span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span></li><li style="color: red" th:if="${item.seckillInfo != null}"><span th:if="${#dates.createNow().getTime() <= item.seckillInfo.startTime}">商品将会在[[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀</span><span th:if="${item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime}">秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]</span></li><li><a href="/static/item/">预约说明</a></li></ul></div>

详情页效果展示:

8、秒杀系统设计

9、登录检查

引入SpringSession依赖和redis

        <!--整合SpringSession完成session共享问题--><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><!--引入redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency>

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

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

SpringSession的配置

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

@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer(){return new GenericJackson2JsonRedisSerializer();}
}

主启动类开启RedisHttpSession这个功能

@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {public static void main(String[] args) {SpringApplication.run(GulimallSeckillApplication.class, args);}}

添加用户登录拦截器

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

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

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

/*** @Description: 把拦截器配置到spring中,否则用户登录拦截器不生效* @Author: WangTianShun* @Date: 2020/12/16 13:44* @Version 1.0*/
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {@AutowiredLoginUserInterceptor loginUserInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}
}

修改item.html

                         <div class="box-btns-two" th:if="${item.seckillInfo != null && (item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime)}"><a href="#" id="seckillA" th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfo.promotionSessionId},code=${item.seckillInfo.randomCode}">立即抢购</a></div><div class="box-btns-two" th:if="${item.seckillInfo == null || (item.seckillInfo.startTime > #dates.createNow().getTime() || #dates.createNow().getTime() > item.seckillInfo.endTime)}"><a href="#" id="addToCart" th:attr="skuId=${item.info.skuId}">加入购物车</a></div>
  • 前端要考虑秒杀系统设计的限流思想
  • 在进行立即抢购之前,前端先进行判断是否登录
                $("#seckillA").click(function(){var isLogin = [[${session.loginUser != null}]];//trueif (isLogin){var killId = $(this).attr("sessionId") +"_"+ $(this).attr("skuId");var key = $(this).attr("code");var num = $("#numInput").val();location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + key + "&num=" + num;}else {alert("秒杀请先登录!");}return false;});

10、秒杀

1)、秒杀接口

  • 点击立即抢购时,会发送请求

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

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

    @GetMapping("/kill")public R seckill(@RequestParam("killId") String killId,@RequestParam("key") String key,@RequestParam("num") Integer num){// 1、判断是否登录(登录拦截器已经自动处理)String orderSn = seckillService.kill(killId, key, num);return R.ok().setData(orderSn);}

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

 String kill(String killId, String key, Integer num);

使用队列削峰 做流量削峰

引入rabbitMQ依赖

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

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

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

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

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

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

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

重要

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

@Overridepublic String kill(String killId, String key, Integer num) {long s1 = System.currentTimeMillis();// 从拦截器获取用户信息MemberResponseVO repsVo = LoginUserInterceptor.loginUser.get();// 1、获取当前商品的详细信息BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps("SKUKILL_CACHE_PREFIX");String json = hashOps.get(killId);if (!StringUtils.isEmpty(json)){SeckillSkuRedisTo redis = JSON.parseObject(json, SeckillSkuRedisTo.class);// 校验合法性Long startTime = redis.getStartTime();Long endTime = redis.getEndTime();long current = new Date().getTime();long ttl = endTime - startTime; //场次存活时间// 1.1校验时间的合法性if (current >= startTime && current <= endTime){// 1.2校验随机码和商品idString randomCode = redis.getRandomCode();String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();if (randomCode.equals(key) && skuId.equals(killId)){// 1.3、验证购物的数量是否合理if (num <= redis.getSeckillLimit()){// 1.4、验证这个人是否购买过。幂等性处理。如果只要秒杀成功,就去占位  userId_sessionId_skillId// SETNXString redisKey = repsVo.getId() + "_" + skuId;// 1.4.1 自动过期--通过在redis中使用 用户id-skuId 来占位看是否买过Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);if (ifAbsent){// 1.4.2 占位成功,说明该用户未秒杀过该商品,则继续尝试获取库存信号量RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);boolean b = semaphore.tryAcquire(num);if (b){// 秒杀成功// 快速下单发送MQ消息 10msString timeId = IdWorker.getTimeId();SeckillOrderTo orderTo = new SeckillOrderTo();orderTo.setOrderSn(timeId);orderTo.setMemberId(repsVo.getId());orderTo.setNum(num);orderTo.setPromotionSessionId(redis.getPromotionSessionId());orderTo.setSkuId(redis.getSkuId());orderTo.setSeckillPrice(redis.getSeckillPrice());rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);long s2 = System.currentTimeMillis();log.info("耗时..." + (s2-s1));return timeId;}return null;}else {// 说明已经买过return null;}}}else {return null;}}else {return null;}}return null;}

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

@Data
public class SeckillOrderTo {/*** 订单号*/private String orderSn;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 购买数量*/private Integer num;/*** 会员id*/private Long memberId;}

2)、创建订单

gulimall-order

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

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

    @Beanpublic Queue orderSeckillOrderQueue(){return new Queue("order.seckill.order.queue", true, false, false);}@Beanpublic Binding orderSeckillOrderQueueBinding(){return new Binding("order.seckill.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.seckill.order",null);}

监听消息队列

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

@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic 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);}}
}

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

    void createSeckillOrder(SeckillOrderTo seckillOrder);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
    @Overridepublic void createSeckillOrder(SeckillOrderTo seckillOrder) {// TODO 保存订单信息OrderEntity orderEntity = new OrderEntity();orderEntity.setOrderSn(seckillOrder.getOrderSn());orderEntity.setMemberId(seckillOrder.getMemberId());orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));orderEntity.setPayAmount(multiply);this.save(orderEntity);// TODO 保存订单项信息OrderItemEntity entity = new OrderItemEntity();entity.setOrderSn(seckillOrder.getOrderSn());entity.setRealAmount(multiply);//TODO 获取当前sku的详细信息进行设置entity.setSkuQuantity(seckillOrder.getNum());orderItemService.save(entity);}

11、秒杀页面完成

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

引入thymeleaf依赖

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

在配置里关闭thymeleaf缓存

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

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

    @GetMapping("/kill")public String seckill(@RequestParam("killId") String killId,@RequestParam("key") String key,@RequestParam("num") Integer num,Model model){// 1、判断是否登录(登录拦截器已经自动处理)String orderSn = seckillService.kill(killId, key, num);model.addAttribute("orderSn", orderSn);return "success";}
<div class="main"><div class="success-wrap"><div class="w" id="result"><div class="m succeed-box"><div th:if="${orderSn != null}" class="mc success-cont"><h1>恭喜,秒杀成功,订单号[[${orderSn}]]</h1><h2>正在准备订单数据,10s以后自动跳转支付 <a style="color: red" th:href="${'http://order.gulimall.com/payOrder?orderSn='+orderSn}">去支付</a></h2></div><div th:if="${orderSn == null}"><h1>手气不好,秒杀失败,下次再来</h1></div></div></div></div></div>

十三、Sentinel服务流控、熔断和降级

sentinel的基础知识参考:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

简介:

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

1、Sentinel-整合SpringBoot

1、整合sentinel

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

2)、下载Sentinel的控制台

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

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

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

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

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

        <!--sentinel熔断、限流--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><!--统计审计信息--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>

找到项目里sentinel对应的版本

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

https://github.com/alibaba/Sentinel

 

java -jar sentinel-dashboard-1.7.1.jar --server.port=8080

打开http://localhost:8080

用户名:sentinel

密码:sentinel

在配置里配置Sentinel

#Sentinel
#Sentinel控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
#Sentinel传输端口
spring.cloud.sentinel.transport.port=8719
#暴露的 endpoint 路径为 /actuator/sentinel
#Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、
#当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。
management.endpoints.web.exposure.include=*

2、流量的限制

1)、Sentinel-自定义流控响应

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

/*** @Description: Sentinel-自定义流控响应* @Author: WangTianShun* @Date: 2020/12/17 10:55* @Version 1.0*/
@Configuration
public class SeckillSentinelConfig implements BlockExceptionHandler {/*** 2.2.0以后的版本实现的是BlockExceptionHandler;以前的版本实现的是WebCallbackManager* @param httpServletRequest* @param httpServletResponse* @param e* @throws Exception*/@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {R error = R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(), BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());httpServletResponse.setCharacterEncoding("UTF-8");httpServletResponse.setContentType("application/json");httpServletResponse.getWriter().write(JSON.toJSONString(error));}/*** 因为版本冲突导致无法引入 WebCallbackManager*/
//    public SeckillSentinelConfig() {
//        WebCallbackManager.setUrlBlockHandler((request, response, ex) -> {
//            R error = R.error(BizCodeEnum.TOO_MANY_REQUESTS_EXCEPTION.getCode(), BizCodeEnum.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
//            response.setCharacterEncoding("UTF-8");
//            response.setContentType("application/json");
//            response.getWriter().write(JSON.toJSONString(error));
//        });
//    }
}

http://seckill.gulimall.com/getCurrentSeckillSkus

未做,自己尝试失败,后面全部为笔记,没有运行

2)、Sentinel全服务引入

  • 每个服务引入actuator依赖
       <!--统计审计信息--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>

每个配置文件配置以下参数

#Sentinel
#Sentinel控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
#暴露的 endpoint 路径为 /actuator/sentinel
#Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、
#当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。
management.endpoints.web.exposure.include=*

3、feign的流控和降级

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

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

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

1)、fegin的熔断

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

1)、调用方的熔断保护开启  feign.sentinel.enabled=true

2)、调用方手动指定远程服务的降级策略。远程服务被降级处理,触发我们的熔断回调方法

3)、超大浏览的时候,必须牺牲一些远程服务。在服务的提供方(远程服务)指定降级策略; 提供方是在运行。但是不运行自己的业务逻辑。返回的是默认的降级数据(限流的数据)

  • gulimall-product服务远程调用gulimall-seckill服务。调用前直接把gulimall-seckill服务宕机,以前不使用sentinel的熔断的时候直接调用会报错。
  • 现在使用sentinel的熔断保护机制,远程调用失败,我们会在调用方实现远程调用类,自定义返回信息给页面。

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

@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {@GetMapping("/sku/seckill/{skuId}")R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}

自定义一远程调用失败的实现类

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

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

2)、fegin的降级

3)、Sentinel-自定义受保护资源

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

1)、代码:

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

// 业务逻辑

}catch(Exception e){

}

2)、基于注解

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

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

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

3.1)、基于代码的限流

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

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

3.2)、基于注解的自定义限流

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

4、网关流控

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

gulimall-gateway引入依赖

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

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

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

谷粒商城-个人笔记(高级篇五)相关推荐

  1. 谷粒商城-个人笔记(高级篇二)

    目录 二.商城业务-首页 1.整合thymeleaf渲染首页 1).在"gulimall-product"项目中导入前端代码: 2).渲染一级分类数据&&整合dev ...

  2. 谷粒商城--秒杀服务--高级篇笔记十二

    谷粒商城–秒杀服务–高级篇笔记十二 1.后台添加秒杀商品 未配置秒杀服务相关网关 1.1 配置网关 - id: coupon_routeuri: lb://gulimall-couponpredica ...

  3. 谷粒商城--订单服务--高级篇笔记十一

    1.页面环境搭建 1.1 静态资源导入nginx 等待付款 --------->detail 订单页 --------->list 结算页 --------->confirm 收银页 ...

  4. 谷粒商城项目之高级篇笔记(一)

    目录 1 商城业务 1.1 商品上架 1.1.1 商品Mapping 1.1.2 建立product索引 1.1.3 上架细节 1.1.4 数据一致性 1.1.5 代码实现 1)先查库存(商品微服务远 ...

  5. 【谷粒商城】框架扩充篇(3/4)

    gitee个人代码:https://gitee.com/HanFerm/gulimall 笔记-基础篇-1(P1-P28):https://blog.csdn.net/hancoder/article ...

  6. M5(项目)-01-尚硅谷谷粒商城项目分布式基础篇开发文档

    M5(项目)-01-尚硅谷谷粒商城项目分布式基础篇开发文档 分布式基础篇 一.环境搭建 各种开发软件的安装 虚拟机: docker,mysql,redis 主机: Maven, idea(后端),Vs ...

  7. R语言学习笔记——高级篇:第十四章-主成分分析和因子分析

    R语言 R语言学习笔记--高级篇:第十四章-主成分分析和因子分析 文章目录 R语言 前言 一.R中的主成分和因子分析 二.主成分分析 2.1.判断主成分的个数 2.2.提取主成分 2.3.主成分旋转 ...

  8. 数据库MySQL学习笔记高级篇(周阳)

    数据库MySQL学习笔记高级篇 1. mysql的架构介绍 mysql简介 高级Mysql mysqlLinux版的安装 mysql配置文件 mysql逻辑架构介绍 mysql存储引擎 2. 索引优化 ...

  9. *谷粒商城项目笔记*

    谷粒商城项目笔记 一 项目介绍(略) 二分布式的基础概念 1,微服务 拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行 2, 集群是个物理形态,分布式是个工作方式. 例如:京东是个 ...

最新文章

  1. python 开发板-MicroPython:STM32 上 的 Python 开发
  2. createprocess失败代码2_Win7 中 Visual C++ 2015安装失败解决方法
  3. 利用C语言 Python校正图像,情人节来了,教你个用 Python 表白的技巧
  4. js的nextSibling,属性兼容IE和FF等浏览器
  5. JQuery Datatables 动态配置参数异步加载数据
  6. Hadoop系列之五:MapReduce进阶(2)
  7. 面向对象的两大迷思,再给你们解答一次
  8. 树莓派2代干货帖(第一天)按图索骥的搭建
  9. 合肥工业大学计算机学院王院长,合肥工业大学计算机与信息学院硕士研究生导师介绍:唐益明...
  10. 三维模型 检索 代码_文章导读|一种基于拉普拉斯算子和联合贝叶斯模型的三维形状检索方法...
  11. STM32 F7xx + LAN8720+LWIP1.4.1调试坑点记录
  12. 人工智能是一个骗局?
  13. 样本量估算:随机对照试验(两组均数)比较的样本量计算方法
  14. 如何把avi转换为mp4?视频转换用嗨格式视频转换器
  15. c#机器人聊天软件,C#制造简易QQ聊天机器人
  16. 简述网卡的作用和工作原理_网卡的功能和工作原理(1)
  17. np.digitize 用法详解
  18. python ADF单位根检验,序列平稳性检验
  19. 【阅读笔记】Cost Volume Pyramid Based Depth Inference for Multi-View Stereo
  20. Can't connect to MySQL server (10060)异常解决方法

热门文章

  1. 校招与内推:软实力的技巧
  2. 谈谈NITE 2的第一个程序UserViewer
  3. 杭电acm1283 最简单的计算机
  4. OpenMVS详细安装教程(ubuntu18.04)
  5. 精华-oracle电子书汇总(申请加精)
  6. 表妹问:区块链是啥?
  7. 口袋中有红、黄、蓝、白、黑5种颜色的球若干个,每次从口袋中先后取出3个球,求得到三种不同颜色的球的可能取法,并输出每种排列的情况
  8. 台式中端计算机配置单,电脑主机中端配置清单
  9. LeetCode知识点总结 - 1974
  10. python利用tkinter制作查询热映电影软件