300、商城业务-支付-支付宝沙箱&代码
1、进入“蚂蚁金服开放平台” https://open.alipay.com/platform/home.htm
2、下载支付宝官方demo,进行配置和测试
文档地址
https://open.alipay.com/platform/home.htm 支付宝&蚂蚁金服开发者平台
https://docs.open.alipay.com/catalog 开发者文档
https://docs.open.alipay.com/270/106291/ 全部文档=>电脑网站支付文档;下载 demo
配置使用沙箱进行测试
1、使用 RSA 工具生成签名
2、下载沙箱版钱包
3、运行官方 demo 进行测试
301、商城业务-支付-RSA、加密加签、密钥等
1、什么是公钥、私钥、加密、签名和验签?
1、公钥私钥
公钥和私钥是一个相对概念 它们的公私性是相对于生成者来说的。 一对密钥生成后,保存在生成者手里的就是私钥, 生成者发布出去大家用的就是公钥2、加密和数字签名
加密是指:
-
我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解密的技术。
-
公钥和私钥都可以用来加密,也都可以用来解密。
-
但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。
-
加密的目的是:
-
为了确保数据传输过程中的不可读性,就是不想让别人看到。
签名:
-
给我们将要发送的数据,做上一个唯一签名(类似于指纹)
-
用来互相验证接收方和发送方的身份;
-
在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以用来达到数据的明文传输。
验签
-
支付宝为了验证请求的数据是否商户本人发的,
-
商户为了验证响应的数据是否支付宝发的
2、支付宝支付流程
加密-对称加密
加密-非对称加密
支付宝的加解密过程
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/
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 {//在支付宝创建的应用的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;}
(4) 订单支付与同步通知
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 {@AutowiredOrderFeignService 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
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 、秒杀业务
秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化) + 独立部署。
限流方式:
-
前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
-
nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
-
网关限流,限流的过滤器
-
代码中使用分布式信号量
-
rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。
2 、秒杀流程
见秒杀流程图
秒杀架构图
秒杀系统关注的问题
- 1、服务单一职责+独立部署
- 秒杀服务即使自己扛不住压力,挂掉。不要影响别人
- 2、秒杀链接加密
- 防止恶意攻击,模拟秒杀请求,1000次/s攻击
- 防止链接暴露,自己工作人员,提前秒杀商品,我们的案列中加入了一个校验码
- 3、库存预热+快速扣减
- 秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求
- 4、动静分离
- nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
使用cDN网络,分担本集群压力
- nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
- 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:skus
,map-key是skuId+商品随机码
(2) redis存储模型设计
秒杀场次存储的List
可以当做hash key
在SECKILL_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的属性//skuInfoprivate SkuInfoVo skuInfoVo;//当前商品秒杀的开始时间private Long startTime;//当前商品秒杀的结束时间private Long endTime;//当前商品秒杀的随机码private String randomCode;
}
3 、限流
参照 Alibaba Sentinel
4、搭建秒杀服务
勾选上 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.108spring.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 {@AutowiredSeckillService seckillService;@AutowiredRedissonClient 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 {@Overridepublic PageUtils queryPage(Map<String, Object> params) {IPage<SeckillSessionEntity> page = this.page(new Query<SeckillSessionEntity>().getPage(params),new QueryWrapper<SeckillSessionEntity>());return new PageUtils(page);}@AutowiredSeckillSkuRelationService seckillSkuRelationService;@Overridepublic List<SeckillSessionEntity> getLates3DaySession() {//计算最近三天
// Date date = new Date(); //2020-12-12 13:59:16List<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、商城业务-秒杀服务-秒杀商品上架
代码实现
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 {@AutowiredSeckillService seckillService;@AutowiredRedissonClient 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 {@AutowiredCouponFeignService couponFeignService;@AutowiredProductFeignService productFeignService;@AutowiredStringRedisTemplate redisTemplate;@AutowiredRabbitTemplate rabbitTemplate;@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.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是很容易被暴露的,防止他人提前准备准备开始,或者开发人员内部知道skuidString 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-skuIdList<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){// 遍历sessionsessions.stream().forEach(session -> {BoundHashOperations<String, Object, Object> ops =stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); // "seckill:skus:";// 遍历skusession.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
加分布式锁 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_1593995400000String 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 keySet<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、商城业务-秒杀服务-秒杀系统设计
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<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// /order/order/status/2948294820984028420String 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 秒杀后续的流程,简化了收货地址等信息。@Overridepublic 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、校验随机码和商品idString 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//SETNXString 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 20msboolean b = semaphore.tryAcquire(num);if (b) {//秒杀成功;//快速下单。发送MQ消息 10msString 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、商城业务-秒杀服务-秒杀效果完成
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序列化机制,进行消息转换*/@Beanpublic 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 {...........@Beanpublic Queue orderSeckillOrderQueue(){//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> argumentsreturn new Queue("order.seckill.order.queue",true,false,false);}@Beanpublic 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 {@AutowiredOrderService 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);}}
}
5、秒杀服务秒杀成功后发送数据到mq
// TODO 上架秒杀商品的时候,每一个数据都有过期时间。// TODO 秒杀后续的流程,简化了收货地址等信息。@Overridepublic 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、校验随机码和商品idString 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//SETNXString 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 20msboolean b = semaphore.tryAcquire(num);if (b) {//秒杀成功;//快速下单。发送MQ消息 10msString 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,甚至服务名称作为资源名来标示资源。
- 规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规 则。所有规则可以动态实时调整。
下载地址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 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
327、Sentinel-整合SpringBoot
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 秒的延时。
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: 8719dashboard: 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.108spring.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=*
我们实现自定义的返回
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(){@Overridepublic 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));}});}
}
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请求进行限制 |
流控模式–直接:
限流表现:当超过阀值,就会被降级。
1s内多次刷新网页,localhost:8401/testA
返回Blocked by Sentienl(flow limiting)
流控模式–关联:
当与A关联的资源B达到阀值后,就限流A自己
B惹事,A挂了。支付达到阈值,限流下单接口。B阈值达到1,A就挂
用post访问B让B忙,访问A发现挂了
流控效果–预热Warm up:
访问数量慢慢升高
阈值初一coldFactor(默认3),经过预热时长后才会达到阈值。
流控效果–排队等待:
匀速排队(Ru1eConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,即让请求以均匀的速度通过对应的是漏桶算法。详细文档可以参考流量控制-匀速器模式,具体的例子可以参见PaceFlowDemo
该方式的作用如下图所示
这种方式主要用于处理间隔性突发的流量,伊消息列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的月耖则处于空闲状态,我们希系统能够在接下来的空闲期间逐渐处理这些请求,而不是第一秒就拒绝多余的请求
331、Sentinel-熔断降级
熔断降级
新增降级规则:降低策略:RT
RT(平均响应时间,秒级)
平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级
窗口期过后关闭断路器
RT最大4900(更大的需要通过-Dcsp.Sentinel.statistic.max.rt=XXXX才能生效)
异常比例(秒级)
QPS>=5且异常比例(秒级统计)超过阈值时,触发降级,时间窗口结束后,关闭降级
sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗囗之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)。
降级策略–RT
降级策略–异常比例:
异常比例(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";
}
说明:
@SentinelResource :处理的是Sentine1控制台配置的违规情况,有blockHandler方法配置的兜底处理
@RuntimeException:int age=10/0,这个是java运行时报出的运行时异异常RunTimeException,@Sentine1Resource不管
系统规则
一般配置在网关或者入口应用中,但是这个东西有点危险,不但值不合适,就相当于系统瘫痪。
系统自适应限流
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统规则包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
highestSystemLoad | load1 触发值,用于触发自适应控制阶段 | -1 (不生效) |
avgRt | 所有入口流量的平均响应时间 | -1 (不生效) |
maxThread | 入口流量的最大并发数 | -1 (不生效) |
qps | 所有入口资源的 QPS | -1 (不生效) |
highestCpuUsage | 当前系统的 CPU 使用率(0.0-1.0) | -1 (不生效) |
@SentinelResource配置
@SentinelResource 注解,主要是指定资源名(也可以用请求路径作为资源名),和指定降级处理方法的。
例如:
package com.dkf.springcloud.controller;import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class RateLimitController {@GetMapping("/byResource") //处理降级的方法名@SentinelResource(value = "byResource", blockHandler = "handleException")public CommonResult byResource(){return new CommonResult(200, "按照资源名限流测试0K", new Payment(2020L,"serial001"));}//降级方法public CommonResult handleException(BlockException e){return new CommonResult(444, e.getClass().getCanonicalName() + "\t 服务不可用");}
}
很明显,上面虽然自定义了兜底方法,但是耦合度太高,下面要解决这个问题。
自定义全局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 {@Overridepublic 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")@Overridepublic 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_1582254000000String 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、添加流控
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、配置流控
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 响应式编程//GatewayCallbackManagerpublic SentinelGatewayConfig(){GatewayCallbackManager.setBlockHandler(new BlockRequestHandler(){//网关限流了请求,就会调用此回调 Mono Flux@Overridepublic 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
如果服务调用顺序如下
Span 之间的父子关系如下:
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/
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:9411sender:type: web# 取消nacos对zipkin的服务发现discovery-client-enabled: false#采样取值介于 0到1之间,1则表示全部收集sleuth:sampler:probability: 1
发送远程请求,测试 zipkin。服务调用链追踪信息统计
Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据 持久化,自然就是得将数据存储至数据库。好在 Zipkin 支持将数据存储至:
-
内存(默认)
-
MySQL
-
Elasticsearch
-
Cassandra
Zipkin 数据持久化相关的官方文档地址如下:
https://github.com/openzipkin/zipkin#storage-component
Zipkin 支持的这几种存储方式中,内存显然是不适用于生产的,这一点开始也说了。而使用MySQL 的话,当数据量大时,查询较为缓慢,也不建议使用。Twitter 官方使用的是 Cassandra作为 Zipkin 的存储数据库,但国内大规模用 Cassandra 的公司较少,而且 Cassandra 相关文档也不多。 综上,故采用 Elasticsearch 是个比较好的选择,关于使用 Elasticsearch 作为 Zipkin 的存储数
据库的官方文档如下:
elasticsearch-storage:
https://github.com/openzipkin/zipkin/tree/master/zipkin-server#elasticsearch-storage
zipkin-storage/elasticsearch
https://github.com/openzipkin/zipkin/tree/master/zipkin-storage/elasticsearch
通过 docker 的方式
docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200 openzipkin/zipkin-dependencies
环境变量 | |
---|---|
STORAGE_TYPE | 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详 见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables |
ES_HOSTS | Elasticsearch地址,多个使用,分隔。默认 http∶//localhost∶9200 |
ES_PIPELINE | 指定span被索引之前的pipeline(pipeline是Elasticsearch的概念) |
ES_TIMEOUT | 连接Elasticsearch的超时时间,单位是毫秒;默认10000(10秒) |
ES_INDEX | Zipkin所使用的索引(Zipkin会每天建索引)前缀,默认是 zipkin |
ES_DATE_SEPARATOR | Zipkin建立索引的日期分隔符,默认是- |
ES_INDEX_SHARDS | shard(shard是Elasticsearch的概念)个数,默认5 |
ES_INDEX_REPLICAS | 副本(replica是Elasticsearch的概念)个数,默认1 |
ES_USERNAME/ES_PASSWORD | Elasticsearch账号密码 |
ES_HTTP_LOGGING | 控制Elasticsearch Api的日志级别,可选项为BASIC、HEADERS、BODY |
使用 es 时 Zipkin Dependencies 支持的环境变量
环境变量 | 含义 |
---|---|
STORAGE_TYPE | 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详 见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables |
ES_INDEX | 生成每日索引名称时使用的索引前缀。默认为"zipkin" |
ES_DATE_SEPARATOR | 在索引中生成日期时使用的分隔符。默认为’-',所以查询的索引看起来像zipkin-yyy-DD-mm,可以改为".",这样查询索引就变成zipkin-yy.MM.dd。示例∶ ES_DATE_SEPARATOR=. |
ES_HOSTS | ElasticSearch主机列表,多个主机使用逗号分隔。默认为 localhost∶9200 |
ES_NODES_WAN_ONLY | 如设为true,则表示仅使用ES_HOSTS所设置的值,默认为false。当 ElasticSearch集群运行在Docker中时,可将该环境变量设为true |
337、Sleuth-链路追踪-Zipkin界面分析
338、分布式高级篇总结
信息统计
[外链图片转存中…(img-i4xtgoWc-1652453950665)]
[外链图片转存中…(img-GrnFn7be-1652453950665)]
[外链图片转存中…(img-rSkPzWMD-1652453950666)]
Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据 持久化,自然就是得将数据存储至数据库。好在 Zipkin 支持将数据存储至:
-
内存(默认)
-
MySQL
-
Elasticsearch
-
Cassandra
Zipkin 数据持久化相关的官方文档地址如下:
https://github.com/openzipkin/zipkin#storage-component
Zipkin 支持的这几种存储方式中,内存显然是不适用于生产的,这一点开始也说了。而使用MySQL 的话,当数据量大时,查询较为缓慢,也不建议使用。Twitter 官方使用的是 Cassandra作为 Zipkin 的存储数据库,但国内大规模用 Cassandra 的公司较少,而且 Cassandra 相关文档也不多。 综上,故采用 Elasticsearch 是个比较好的选择,关于使用 Elasticsearch 作为 Zipkin 的存储数
据库的官方文档如下:
elasticsearch-storage:
https://github.com/openzipkin/zipkin/tree/master/zipkin-server#elasticsearch-storage
zipkin-storage/elasticsearch
https://github.com/openzipkin/zipkin/tree/master/zipkin-storage/elasticsearch
通过 docker 的方式
docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200 openzipkin/zipkin-dependencies
环境变量 | |
---|---|
STORAGE_TYPE | 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详 见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables |
ES_HOSTS | Elasticsearch地址,多个使用,分隔。默认 http∶//localhost∶9200 |
ES_PIPELINE | 指定span被索引之前的pipeline(pipeline是Elasticsearch的概念) |
ES_TIMEOUT | 连接Elasticsearch的超时时间,单位是毫秒;默认10000(10秒) |
ES_INDEX | Zipkin所使用的索引(Zipkin会每天建索引)前缀,默认是 zipkin |
ES_DATE_SEPARATOR | Zipkin建立索引的日期分隔符,默认是- |
ES_INDEX_SHARDS | shard(shard是Elasticsearch的概念)个数,默认5 |
ES_INDEX_REPLICAS | 副本(replica是Elasticsearch的概念)个数,默认1 |
ES_USERNAME/ES_PASSWORD | Elasticsearch账号密码 |
ES_HTTP_LOGGING | 控制Elasticsearch Api的日志级别,可选项为BASIC、HEADERS、BODY |
使用 es 时 Zipkin Dependencies 支持的环境变量
环境变量 | 含义 |
---|---|
STORAGE_TYPE | 指定存储类型,可选项为elasticsearch、mysql、cassandra等,详 见∶https∶//github.com/openzipkin/zipkin/tree/master/zipkin-server#environment-variables |
ES_INDEX | 生成每日索引名称时使用的索引前缀。默认为"zipkin" |
ES_DATE_SEPARATOR | 在索引中生成日期时使用的分隔符。默认为’-',所以查询的索引看起来像zipkin-yyy-DD-mm,可以改为".",这样查询索引就变成zipkin-yy.MM.dd。示例∶ ES_DATE_SEPARATOR=. |
ES_HOSTS | ElasticSearch主机列表,多个主机使用逗号分隔。默认为 localhost∶9200 |
ES_NODES_WAN_ONLY | 如设为true,则表示仅使用ES_HOSTS所设置的值,默认为false。当 ElasticSearch集群运行在Docker中时,可将该环境变量设为true |
337、Sleuth-链路追踪-Zipkin界面分析
[外链图片转存中…(img-5D5seBop-1652453950666)]
[外链图片转存中…(img-0IFPjln2-1652453950666)]
[外链图片转存中…(img-NNh56s4w-1652453950666)]