300、商城业务-支付-支付宝沙箱&代码
1、进入“蚂蚁金服开放平台” https://open.alipay.com/platform/home.htm
文档地址
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 进行测试
公钥和私钥是一个相对概念 它们的公私性是相对于生成者来说的。 一对密钥生成后,保存在生成者手里的就是私钥, 生成者发布出去大家用的就是公钥2、加密和数字签名
加密是指:
我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解密的技术。
公钥和私钥都可以用来加密,也都可以用来解密。
但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。
加密的目的是:
为了确保数据传输过程中的不可读性,就是不想让别人看到。
签名:
给我们将要发送的数据,做上一个唯一签名(类似于指纹)
用来互相验证接收方和发送方的身份;
在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以用来达到数据的明文传输。
验签
支付宝为了验证请求的数据是否商户本人发的,
商户为了验证响应的数据是否支付宝发的
加密-对称加密
加密-非对称加密
支付宝的加解密过程
因为我们要开发支付功能,支付宝会有回调地址,所以在开发过程中也需要我们的地址能够被支付宝回调成功,所以需要内网穿透。
内网穿透功能可以允许我们使用外网的网址来访问主机;
正常的外网需要访问我们项目的流程是:
1、买服务器并且有公网固定 IP
2、买域名映射到服务器的 IP
3、域名需要进行备案和审核
1、开发测试(微信、支付宝)
2、智慧互联
3、远程控制
4、私有云
1、natapp:https://natapp.cn/ 优惠码:022B93FD(9 折)[仅限第一次使用]
2、续断:www.zhexi.tech 优惠码:SBQMEA(95 折)[仅限第一次使用]
3、花生壳:https://www.oray.com/
商户私钥
加一个对应的签名,支付宝端会使用商户公钥
对签名进行验签,只有数据明文和签名对应的时候才能说明传输正确支付宝私钥
加一个对应的签名,商户端收到支付成功数据之后也会使用支付宝公钥
验签,成功后才能确认
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-sdk-javaartifactId>
<version>4.9.28.ALLversion>
dependency>
成功调用该接口后,返回的数据就是支付页面的html,因此后续会使用@ResponseBody
我们可以在异步通知回调地址(notify_url)接口修改我们的订单的状态(支付成功修改订单状态)还有一个return_url返回到我们支付成功后要跳转的页面
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
//在支付宝创建的应用的id
private String app_id = "2016102600763190";
// 商户私钥,您的PKCS8格式RSA2私钥
private String merchant_private_key = "MjXN6Hnj8k2GAriRFt0BS9gjihbl9Rt38VMNbBi3Vt3Cy6TOwANLLJ/DfnYjRqwCG81fkyKlDqdsamdfCiTysCa0gQKBgQDYQ45LSRxAOTyM5NliBmtev0lbpDa7FqXL0UFgBel5VgA1Ysp0+6ex2n73NBHbaVPEXgNMnTdzU3WF9uHF4Gj0mfUzbVMbj/YkkHDOZHBggAjEHCB87IKowq/uAH/++Qes2GipHHCTJlG6yejdxhOsMZXdCRnidNx5yv9+2JI37QKBgQCw0xn7ZeRBIOXxW7xFJw1WecUV7yaL9OWqKRHat3lFtf1Qo/87cLl+KeObvQjjXuUe07UkrS05h6ijWyCFlBo2V7Cdb3qjq4atUwScKfTJONnrF+fwTX0L5QgyQeDX5a4yYp4pLmt6HKh34sI5S/RSWxDm7kpj+/MjCZgp6Xc51g==";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
private String alipay_public_key = "MIIBIjA74UKxt2F8VMIRKrRAAAuIMuawIsl4Ye+G12LK8P1ZLYy7ZJpgZ+Wv5nOs3DdoEazgCERj/ON8lM1KBHZOAV+TkrIcyi7cD1gfv4a1usikrUqm8/qhFvoiUfyHJFv1ymT7C4BI6aHzQ2zcUlSQPGoPl4C11tgnSkm3DlH2JZKgaIMcCOnNH+qctjNh9yIV9zat2qUiXbxmrCTtxAmiI3I+eVsUNwvwIDAQAB";
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url="http://**.natappfree.cc/payed/notify";
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
private String return_url="http://order.gulimall.com/memberOrder.html";
// 签名方式
private String sign_type = "RSA2";
// 字符编码格式
private String charset = "utf-8";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
public String pay(PayVo vo) throws AlipayApiException {
//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
//1、根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);
//2、创建一个支付请求 //设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = vo.getOut_trade_no();
//付款金额,必填
String total_amount = vo.getTotal_amount();
//订单名称,必填
String subject = vo.getSubject();
//商品描述,可空
String body = vo.getBody();
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);
return result;
}
/**
* 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;
}
设置成功回调地址为订单详情页
设置成功回调地址为订单详情页
// 页面跳转同步通知页面路径 需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";
}
支付成功后跳转到我们指定的list 的页面
@Controller
public class MemberWebController {
@Autowired
OrderFeignService orderFeignService;
/**
* 订单分页查询
* @param pageNum
* @param model
* @return
*/
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
Model model, HttpServletRequest request){
//获取到支付宝给我们传来的所有请求数据;
// request。验证签名,如果正确可以去修改。
//查出当前登录的用户的所有订单列表数据
Map<String,Object> page =new HashMap<>();
page.put("page",pageNum.toString());
//
R r = orderFeignService.listWithItem(page);
System.out.println(JSON.toJSONString(r));
model.addAttribute("orders",r);
return "orderList";
}
}
package com.atguigu.gulimall.member.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2022/5/7
* @描述:
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
@PostMapping("/order/order/listWithItem")
public R listWithItem(@RequestBody Map<String, Object> params);
}
我们整个支付宝会有两个回调地址,一个是return_url,一个是notify_url,我们之前是在return_url地址中处理支付成功后跳转的地址,我们可以在notify_url处理支付成功后,修改订单的状态逻辑。虽然也可以在return_url地址中一同处理订单的业务逻辑,但是这个地址支付宝只回调一次,而notify_url只要我们没有响应支付宝“success”,会在24h内回调8次(最大努力通知)
success
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url="http://**.natappfree.cc/payed/notify";
将外网映射到本地的order.gulimall.com:80
由于回调的请求头不是order.gulimall.com
,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置
用内网穿透的地址测试访问通我们的notify_url
@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";
}
}
@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);
}
@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
各参数详细意义见[支付宝开放平台异步通知](
由于可能出现订单已经过期后,库存已经解锁,但支付成功后再修改订单状态的情况,需要设置支付有效时间,只有在有效期内才能进行支付(就是比如:客户打开支付页面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\"}");
超时后订单显示:“抱歉您的交易因超时已失败”’
秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化) + 独立部署。
限流方式:
前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
网关限流,限流的过滤器
代码中使用分布式信号量
rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。
见秒杀流程图
秒杀架构图
后vue的后台管理系统里上架秒杀,打开F12看url然后去编写逻辑
点击关联商品可以添加秒杀里的商品。可以看sms数据库里的seckill_sky
nginx–>gateway–>redis分布式信号了–> 秒杀服务
gulimall-seckill
redisson 信号量
的形式存储在redis中秒杀活动:存在在scekill:sesssions
这个redis-key里,。value为 skyIds[]
秒杀活动里具体商品项:是一个map,redis-key是seckill:skus
,map-key是skuId+商品随机码
秒杀场次存储的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的属性
//skuInfo
private SkuInfoVo skuInfoVo;
//当前商品秒杀的开始时间
private Long startTime;
//当前商品秒杀的结束时间
private Long endTime;
//当前商品秒杀的随机码
private String randomCode;
}
参照 Alibaba Sentinel
勾选上 data-redis, web,openfeign ,devtools,lombok
<dependencies>
<dependency>
<groupId>com.atguigu.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.12.0version>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
applivation.properties
spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=119.3.105.108:8848
spring.redis.host=119.3.105.108
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=119.3.105.108
spring.thymeleaf.cache=false
spring.session.store-type=redis
#spring.task.scheduling.pool.size=5
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50
bootstrap.properties
spring.cloud.nacos.config.server-addr=119.3.105.108:8848
spring.application.name=gulimall-seckill
server.port=25000
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
312、商城业务-秒杀服务-SpringBoot整合定时任务与异步任务
秒杀服务定时上架秒杀商品
表达式:https://cron.qqe2.com/
此处定时任务用于定时查询秒杀活动
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);
}
}
package com.atguigu.gulimall.seckill.scheduled;
import com.atguigu.gulimall.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 秒杀商品的定时上架;
* 每天晚上3点;上架最近三天需要秒杀的商品。
* 当天00:00:00 - 23:59:59
* 明天00:00:00 - 23:59:59
* 后天00:00:00 - 23:59:59
*/
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
//TODO 幂等性处理
// @Scheduled(cron = "*/3 * * * * ?")
@Scheduled(cron = "0 * * * * ?") //每分钟执行一次吧,上线后调整为每天晚上3点执行
// @Scheduled(cron = "0 0 3 * * ?") 线上模式
public void uploadSeckillSkuLatest3Days(){
//1、重复上架无需处理
log.info("上架秒杀的商品信息...");
// 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态。
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try{
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}
}
基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class SaticScheduleTask {
//3.添加定时任务
@Scheduled(cron = "0/5 * * * * ?")
//或直接指定时间间隔,例如:5秒
//@Scheduled(fixedRate=5000)
private void configureTasks() {
System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
}
}
Cron表达式参数分别表示:
秒(0~59) 例如0/5表示每5秒
分(0~59)
时(0~23)
日(0~31)的某天,需计算
月(0~11)
周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)
@Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。
// Cron表达式范例:
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
显然,使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到实时生效的效果,可以使用接口来完成定时任务。
查询三天内需要秒杀的商品
package com.atguigu.gulimall.coupon.service.impl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.coupon.dao.SeckillSessionDao;
import com.atguigu.gulimall.coupon.entity.SeckillSessionEntity;
import com.atguigu.gulimall.coupon.entity.SeckillSkuRelationEntity;
import com.atguigu.gulimall.coupon.service.SeckillSessionService;
import com.atguigu.gulimall.coupon.service.SeckillSkuRelationService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<SeckillSessionEntity> page = this.page(
new Query<SeckillSessionEntity>().getPage(params),
new QueryWrapper<SeckillSessionEntity>()
);
return new PageUtils(page);
}
@Autowired
SeckillSkuRelationService seckillSkuRelationService;
@Override
public List<SeckillSessionEntity> getLates3DaySession() {
//计算最近三天
// Date date = new Date(); //2020-12-12 13:59:16
List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
if(list!=null && list.size()>0){
List<SeckillSessionEntity> collect = list.stream().map(session -> {
Long id = session.getId();
List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
session.setRelationSkus(relationEntities);
return session;
}).collect(Collectors.toList());
return collect;
}
return null;
}
private String startTime(){
LocalDate now = LocalDate.now();
LocalTime min = LocalTime.MIN;
LocalDateTime start = LocalDateTime.of(now, min);
String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
private String endTime(){
LocalDate now = LocalDate.now();
LocalDate localDate = now.plusDays(2);
LocalDateTime of = LocalDateTime.of(localDate, LocalTime.MAX);
String format = of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
}
package com.atguigu.guliamll.seckill.scheduled;
import com.atguigu.guliamll.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @创建时间: 2022/5/8
* @创建人: 放生
* @描述:
* 秒杀商品的定时上架;
* 每天晚上3点;上架最近三天需要秒杀的商品。
* 当天00:00:00 - 23:59:59
* 明天00:00:00 - 23:59:59
* 后天00:00:00 - 23:59:59
*/
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
//TODO 幂等性处理
// @Scheduled(cron = "*/3 * * * * ?")
@Scheduled(cron = "0 * * * * ?") //每分钟执行一次吧,上线后调整为每天晚上3点执行
// @Scheduled(cron = "0 0 3 * * ?") 线上模式
public void uploadSeckillSkuLatest3Days(){
//1、重复上架无需处理
log.info("上架秒杀的商品信息...");
// 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态。
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try{
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}
}
具体实现 uploadSeckillSkuLatest3Days
package com.atguigu.guliamll.seckill.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.to.mq.SeckillOrderTo;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberRespVo;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.guliamll.seckill.feign.CouponFeignService;
import com.atguigu.guliamll.seckill.feign.ProductFeignService;
import com.atguigu.guliamll.seckill.interceptor.LoginUserInterceptor;
import com.atguigu.guliamll.seckill.service.SeckillService;
import com.atguigu.guliamll.seckill.to.SecKillSkuRedisTo;
import com.atguigu.guliamll.seckill.vo.SeckillSesssionsWithSkus;
import com.atguigu.guliamll.seckill.vo.SkuInfoVo;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @创建人: 放生
* @创建时间: 2022/5/8
* @描述:
*/
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
ProductFeignService productFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
RedissonClient redissonClient;
private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码
/**
* 上架秒杀商品
*/
@Override
public void uploadSeckillSkuLatest3Days() {
//1、扫描最近三天需要参与秒杀的活动
R session = couponFeignService.getLates3DaySession();
if (session.getCode() == 0) {
//上架商品
List<SeckillSesssionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSesssionsWithSkus>>() {
});
//缓存到redis
//1、缓存活动信息
saveSessionInfos(sessionData);
//2、缓存活动的关联商品信息
saveSessionSkuInfos(sessionData);
}
}
private void saveSessionInfos(List<SeckillSesssionsWithSkus> sesssions) {
if (sesssions != null) {
sesssions.stream().forEach(sesssion -> {
Long startTime = sesssion.getStartTime().getTime();
Long endTime = sesssion.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey) {
List<String> collect = sesssion.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString()).collect(Collectors.toList());
//缓存活动信息
redisTemplate.opsForList().leftPushAll(key, collect);
//TODO 设置过期时间[已完成]
redisTemplate.expireAt(key, new Date(endTime));
}
});
}
}
private void saveSessionSkuInfos(List<SeckillSesssionsWithSkus> sesssions) {
if (sesssions != null) {
sesssions.stream().forEach(sesssion -> {
//准备hash操作
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
sesssion.getRelationSkus().stream().forEach(seckillSkuVo -> {
//4、随机码? seckill?skuId=1&key=dadlajldj;
// 随机码是因为商品的id是很容易被暴露的,防止他人提前准备准备开始,或者开发人员内部知道skuid
String token = UUID.randomUUID().toString().replace("-", "");
if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())) {
//缓存商品
SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
//1、sku的基本数据
R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (skuInfo.getCode() == 0) {
SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
redisTo.setSkuInfo(info);
}
//2、sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo, redisTo);
//3、设置上当前商品的秒杀时间信息
redisTo.setStartTime(sesssion.getStartTime().getTime());
redisTo.setEndTime(sesssion.getEndTime().getTime());
redisTo.setRandomCode(token);
String jsonString = JSON.toJSONString(redisTo);
//TODO 每个商品的过期时间不一样。所以,我们在获取当前商品秒杀信息的时候,做主动删除,代码在 getSkuSeckillInfo 方法里面
ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);
//如果当前这个场次的商品的库存信息已经上架就不需要上架
//5、使用库存作为分布式的信号量 限流;
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
//商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
//TODO 设置过期时间。
semaphore.expireAt(sesssion.getEndTime());
}
});
});
}
}
}
private void saveSessionInfo(List<SeckillSessionsWithSkus> sessions){
if(sessions != null){
sessions.stream().forEach(session -> {
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime; // "seckill:sessions:";
Boolean hasKey = stringRedisTemplate.hasKey(key);
// 防止重复添加活动到redis中
if(!hasKey){
// 获取所有商品id // 格式:活动id-skuId
List<String> skus = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId()).collect(Collectors.toList());
// 缓存活动信息
stringRedisTemplate.opsForList().leftPushAll(key, skus);
}
});
}
}
前面已经缓存了sku项的活动信息,但是只有活动id和skuID,接下来我们要保存完整是sku信息到redis中
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){
if(sessions != null){
// 遍历session
sessions.stream().forEach(session -> {
BoundHashOperations<String, Object, Object> ops =
stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); // "seckill:skus:";
// 遍历sku
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
// 1.商品的随机码
String randomCode = UUID.randomUUID().toString().replace("-", "");
// 缓存中没有再添加
if(!ops.hasKey(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId())){
// 2.缓存商品
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
BeanUtils.copyProperties(seckillSkuVo, redisTo);
// 3.sku的基本数据 sku的秒杀信息
R info = productFeignService.skuInfo(seckillSkuVo.getSkuId());
if(info.getCode() == 0){
SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
redisTo.setSkuInfoVo(skuInfo);
}
// 4.设置当前商品的秒杀信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
// 设置随机码
redisTo.setRandomCode(randomCode);
// 活动id-skuID 秒杀sku信息
ops.put(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId(), JSON.toJSONString(redisTo));
// 5.使用库存作为分布式信号量 限流
RSemaphore semaphore =
redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);//"seckill:stock:";
// 在信号量中设置秒杀数量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
}
});
});
}
就是我如果上架的服务部署了多个定时任务的上架功能,会存在重复上架的问题,我们的方案是采用分布式锁来解决,也可以采用分布式的定时任务,比如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();
}
}
前面已经在redis中缓存了秒杀活动的各种信息,现在写获取缓存中当前时间段在秒杀的sku,用户点击页面后发送请求
@GetMapping(value = "/getCurrentSeckillSkus")
@ResponseBody // 用户网页发请求
public R getCurrentSeckillSkus() {
//获取到当前可以参加秒杀商品的信息
List<SeckillSkuRedisTo> vos = secKillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
// 1.确定当前时间属于那个秒杀场次
long time = new Date().getTime();
// 定义一段受保护的资源
try (Entry entry = SphU.entry("seckillSkus")){
Set<String> keys = stringRedisTemplate.keys(SESSION_CACHE_PREFIX + "*");
for (String key : keys) {
// seckill:sessions:1593993600000_1593995400000
String replace = key.replace("seckill:sessions:", "");
String[] split = replace.split("_");
long start = Long.parseLong(split[0]);
long end = Long.parseLong(split[1]);
if(time >= start && time <= end){
// 2.获取这个秒杀场次的所有商品信息
List<String> range = stringRedisTemplate.opsForList().range(key, 0, 100);
BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<String> list = hashOps.multiGet(range);
if(list != null){
return list.stream().map(item -> {
SeckillSkuRedisTo redisTo = JSON.parseObject(item, SeckillSkuRedisTo.class);
// redisTo.setRandomCode(null);
return redisTo;
}).collect(Collectors.toList());
}
break;
}
}
}catch (BlockException e){
log.warn("资源被限流:" + e.getMessage());
}
return null;
}
<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) {
$(" + item.skuId + ")'> ").append($(""))
.append($(""
+item.skuInfoVo.skuTitle+""))
.append($("" + item.seckillPrice + ""))
.append($("" + item.skuInfoVo.price + ""))
.appendTo("#seckillSkuContent");
})
}
})
function toDetail(skuId) {
location.href = "http://item.gulimall.com/" + skuId + ".html";
}
script>
随机码是在秒杀活动开始才暴露出去。
用户看到秒杀活动点击秒杀商品了,如果时间段正确,返回随机码。购买时带着
@ResponseBody
@GetMapping(value = "/getSeckillSkuInfo/{skuId}")
public R getSeckillSkuInfo(@PathVariable("skuId") Long skuId) {
SeckillSkuRedisTo to = secKillService.getSeckillSkuInfo(skuId);
return R.ok().setData(to);
}
@Override
public SeckillSkuRedisTo getSeckillSkuInfo(Long skuId) {
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
//获取所有商品的hash key
Set<String> keys = ops.keys();
for (String key : keys) {
//通过正则表达式匹配 数字-当前skuid的商品
if (Pattern.matches("\\d-" + skuId,key)) {
String v = ops.get(key);
SeckillSkuRedisTo redisTo = JSON.parseObject(v, SeckillSkuRedisTo.class);
//当前商品参与秒杀活动
if (redisTo!=null){
long current = System.currentTimeMillis();
//当前活动在有效期,暴露商品随机码返回
if (redisTo.getStartTime() < current && redisTo.getEndTime() > current) {
return redisTo;
}
//当前商品不再秒杀有效期,则隐藏秒杀所需的商品随机码
redisTo.setRandomCode(null);
return redisTo;
}
}
}
return null;
}
在查询商品详情页的接口中查询秒杀对应信息
@Override // SkuInfoServiceImpl
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
....;
// 6.查询当前sku是否参与秒杀优惠
CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
R skuSeckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
if (skuSeckillInfo.getCode() == 0) {
SeckillInfoVo seckillInfoVo = skuSeckillInfo.getData(new TypeReference<SeckillInfoVo>() {});
skuItemVo.setSeckillInfoVo(seckillInfoVo);
}
}, executor);
注意所有的时间都是距离1970的差值
更改商品详情页的显示效果
<li style="color: red" th:if="${item.seckillSkuVo != null}">
<span th:if="${#dates.createNow().getTime() < item.seckillSkuVo.startTime}">
商品将会在[[${#dates.format(new java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀
span>
<span th:if="${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}">
秒杀价 [[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]
span>
li>
<div class="box-btns-two"
th:if="${item.seckillSkuVo == null }">
<a class="addToCart" href="http://cart.gulimall.com/addToCart" th:attr="skuId=${item.info.skuId}">
加入购物车
a>
div>
<div class="box-btns-two"
th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}">
<a class="seckill" href="#"
th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}">
立即抢购
a>
div>
时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量
,通过校验的则秒杀成功,发送消息创建订单秒杀按钮:
<div class="box-btns-two"
th:if="${item.seckillInfoVo != null && (#dates.createNow().getTime() >= item.seckillInfoVo.startTime && #dates.createNow().getTime() <= item.seckillInfoVo.endTime)}">
<a id="secKillA"
th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfoVo.promotionSessionId},code=${item.seckillInfoVo.randomCode}">
立即抢购
a>
div>
<div class="box-btns-two"
th:if="${item.seckillInfoVo == null || (#dates.createNow().getTime() < item.seckillInfoVo.startTime || #dates.createNow().ge`tTime() > item.seckillInfoVo.endTime)}">
<a id="addToCartA" th:attr="skuId=${item.info.skuId}">
加入购物车
a>
div>
秒杀函数:
要判断是否登入
$("#secKillA").click(function () {
var isLogin = [[${session.loginUser != null}]]
if (isLogin) {
var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");
var num = $("#numInput").val();
location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + $(this).attr("code") + "&num=" + num;
} else {
layer.msg("请先登录!")
}
return false;
})
添加是否登入的拦截器
package com.atguigu.guliamll.seckill.interceptor;
/**
* @创建人: 放生
* @创建时间: 2022/5/8
* @描述:
*/
import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.vo.MemberRespVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// /order/order/status/2948294820984028420
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/kill", uri);
if(match){
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute!=null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
return true;
}
}
上一章节已经写好了,我们的页面,秒杀按钮,请求路径等,接下来编写秒杀的后端接口
@GetMapping("/kill")
public String secKill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num,
Model model){
String orderSn = seckillService.kill(killId,key,num);
model.addAttribute("orderSn",orderSn);
//1、判断是否登录
return "success";
}
// TODO 上架秒杀商品的时候,每一个数据都有过期时间。
// TODO 秒杀后续的流程,简化了收货地址等信息。
@Override
public String kill(String killId, String key, Integer num) {
long s1 = System.currentTimeMillis();
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
//1、获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String json = hashOps.get(killId);
if (StringUtils.isEmpty(json)) {
return null;
} else {
SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
//校验合法性
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
long time = new Date().getTime();
long ttl = endTime - time;
//1、校验时间的合法性
if (time >= startTime && time <= endTime) {
//2、校验随机码和商品id
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
//3、验证购物数量是否合理
if (num <= redis.getSeckillLimit()) {
//4、验证这个人是否已经购买过。幂等性; 如果只要秒杀成功,就去占位。 userId_SessionId_skuId
//SETNX
String redisKey = respVo.getId() + "_" + skuId;
//自动过期
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功说明从来没有买过
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//120 20ms
boolean b = semaphore.tryAcquire(num);
if (b) {
//秒杀成功;
//快速下单。发送MQ消息 10ms
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(respVo.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redis.getPromotionSessionId());
orderTo.setSkuId(redis.getSkuId());
orderTo.setSeckillPrice(redis.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
long s2 = System.currentTimeMillis();
log.info("耗时...{}", (s2 - s1));
return timeId;
}
return null;
} else {
//说明已经买过了
return null;
}
}
} else {
return null;
}
} else {
return null;
}
}
return null;
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
package com.atguigu.guliamll.seckill.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class MyRabbitConfig {
/**
* 使用JSON序列化机制,进行消息转换
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
package com.atguigu.gulimall.order.config;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2022/5/6
* @描述:
*/
@Configuration
public class MyMQConfig {
...........
@Bean
public Queue orderSeckillOrderQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
return new Queue("order.seckill.order.queue",true,false,false);
}
@Bean
public Binding orderSeckillOrderQueueBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map arguments
*/
return new Binding("order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
}
}
package com.atguigu.gulimall.order.listener;
import com.atguigu.common.to.mq.SeckillOrderTo;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.service.OrderService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @创建人: 放生
* @创建时间: 2022/5/8
* @描述:
*/
@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
try{
log.info("准备创建秒杀单的详细信息。。。");
orderService.createSeckillOrder(seckillOrder);
//手动调用支付宝收单;
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
// TODO 上架秒杀商品的时候,每一个数据都有过期时间。
// TODO 秒杀后续的流程,简化了收货地址等信息。
@Override
public String kill(String killId, String key, Integer num) {
long s1 = System.currentTimeMillis();
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
//1、获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String json = hashOps.get(killId);
if (StringUtils.isEmpty(json)) {
return null;
} else {
SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
//校验合法性
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
long time = new Date().getTime();
long ttl = endTime - time;
//1、校验时间的合法性
if (time >= startTime && time <= endTime) {
//2、校验随机码和商品id
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
//3、验证购物数量是否合理
if (num <= redis.getSeckillLimit()) {
//4、验证这个人是否已经购买过。幂等性; 如果只要秒杀成功,就去占位。 userId_SessionId_skuId
//SETNX
String redisKey = respVo.getId() + "_" + skuId;
//自动过期
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功说明从来没有买过
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//120 20ms
boolean b = semaphore.tryAcquire(num);
if (b) {
//秒杀成功;
//快速下单。发送MQ消息 10ms
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(respVo.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redis.getPromotionSessionId());
orderTo.setSkuId(redis.getSkuId());
orderTo.setSeckillPrice(redis.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
.......
添加秒杀成功或者失败后的页面显示。。。
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 等应用容器。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
<version>2.1.0.RELEASEversion>
dependency>
Sentinel 控制台提供一个轻量级的控制台,它提供机器发现、单机资源实时监控、集群资源汇总,以及规则管理的功能。您只需要对应用进行简单的配置,就可以使用这些功能。
注意: 集群资源汇总仅支持 500 台以下的应用集群,有大概 1 - 2 秒的延时。
Figure 1. Sentinel Dashboard
开启该功能需要3个步骤:
您可以从 release 页面 下载最新版本的控制台 jar 包。
您也可以从最新版本的源码自行构建 Sentinel 控制台:
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控制台地址信息
在控制台调整参数。【默认所有的流控设置保存在内存中,重启失效】
application.yml
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8080
这里的 spring.cloud.sentinel.transport.port
端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。
更多 Sentinel 控制台的使用及问题参考: Sentinel 控制台文档 以及 Sentinel FAQ
spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=119.3.105.108:8848
spring.redis.host=119.3.105.108
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=119.3.105.108
spring.thymeleaf.cache=false
spring.session.store-type=redis
#spring.task.scheduling.pool.size=5
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50
#sentinel控制台
spring.cloud.sentinel.transport.port=8719
spring.cloud.sentinel.transport.dashboard=localhost:8333
在使用 Endpoint 特性之前需要在 Maven 中添加 spring-boot-starter-actuator
依赖,并在配置中允许 Endpoints 的访问。
management.security.enabled=false
。暴露的 endpoint 路径为 /sentinel
management.endpoints.web.exposure.include=*
。暴露的 endpoint 路径为 /actuator/sentinel
Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
management.endpoints.web.exposure.include=*
我们实现自定义的返回
package com.atguigu.guliamll.seckill.config;
import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler;
import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.R;
import org.springframework.context.annotation.Configuration;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class SeckillSentinelConfig {
public SeckillSentinelConfig(){
WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler(){
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().write(JSON.toJSONString(error));
}
});
}
}
参照以上的步骤
每一个微服务都导入 actuator ();并配合management.endpoints.web.exposure.include=*
自定义sentinel流控返回数据
common的完整配置
<dependencies>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.8version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpcoreartifactId>
<version>4.4.12version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.17version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>servlet-apiartifactId>
<version>2.5version>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
<version>1.7.1version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>2.0.1.Finalversion>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.15version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.2.1version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpcoreartifactId>
<version>4.2.1version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
<dependency>
<groupId>org.eclipse.jettygroupId>
<artifactId>jetty-utilartifactId>
<version>9.3.7.v20160115version>
dependencies>
资源名:唯一名称,默认请求路径
针对来源: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
该方式的作用如下图所示
这种方式主要用于处理间隔性突发的流量,伊消息列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的月耖则处于空闲状态,我们希系统能够在接下来的空闲期间逐渐处理这些请求,而不是第一秒就拒绝多余的请求
新增降级规则:降低策略: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)]
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的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 注解,主要是指定资源名(也可以用请求路径作为资源名),和指定降级处理方法的。
例如:
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)]
我们现在的案例是product 服务feign调用seckill服务,根据skuid 查询秒杀的商品
* 4、使用Sentinel来保护feign远程调用:熔断;
* 1)、调用方的熔断保护:feign.sentinel.enabled=true
* 2)、调用方手动指定远程服务的降级策略。远程服务被降级处理。触发我们的熔断回调方法
* 3)、超大浏览的时候,必须牺牲一些远程服务。在服务的提供方(远程服务)指定降级策略;
* 提供方是在运行。但是不运行自己的业务逻辑,返回的是默认的降级数据(限流的数据),
feign.sentinel.enabled=true
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);
}
package com.atguigu.gulimall.product.feign.fallback;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.product.feign.SeckillFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @创建人: 放生
* @创建时间: 2022/5/8
* @描述:
*/
@Slf4j
@Component
public class SeckillFeignServiceFallBack implements SeckillFeignService {
@Override
public R getSkuSeckillInfo(Long skuId) {
log.info("熔断方法调用...getSkuSeckillInfo");
return R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(),BizCodeEnume.TOO_MANY_REQUEST.getMsg());
}
}
# 5、自定义受保护的资源
* 1)、代码
* try(Entry entry = SphU.entry("seckillSkus")){
* //业务逻辑
* }
* catch(Execption e){}
*
* 2)、基于注解。
* @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
*
* 无论是1,2方式一定要配置被限流以后的默认返回.
* url请求可以设置统一返回:WebCallbackManager
SeckillServiceImpl
/**
* blockHandler 函数会在原方法被限流/降级/系统保护的时候调用,而 fallback 函数会针对所有类型的异常。
* @return
*/
//返回当前时间可以参与的秒杀商品信息
@SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
@Override
public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
//1、确定当前时间属于哪个秒杀场次。
//1970 -
long time = new Date().getTime();
try(Entry entry = SphU.entry("seckillSkus")){
Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
for (String key : keys) {
//seckill:sessions:1582250400000_1582254000000
String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] s = replace.split("_");
Long start = Long.parseLong(s[0]);
Long end = Long.parseLong(s[1]);
if (time >= start && time <= end) {
//2、获取这个秒杀场次需要的所有商品信息
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<String> list = hashOps.multiGet(range);
if (list != null) {
List<SecKillSkuRedisTo> collect = list.stream().map(item -> {
SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class);
// redis.setRandomCode(null); 当前秒杀开始就需要随机码
return redis;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
}catch (BlockException e){
log.error("资源被限流,{}",e.getMessage());
}
return null;
}
public List<SecKillSkuRedisTo> blockHandler(BlockException e){
log.error("getCurrentSeckillSkusResource被限流了..");
return null;
}
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
<version>2.1.0.RELEASEversion>
dependency>
参考官网 网关限流 · alibaba/Sentinel Wiki (github.com)
添加以下配置类
package com.atguigu.gulimall.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.R;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @创建人: 放生
* @创建时间: 2022/5/8
* @描述:
*/
@Configuration
public class SentinelGatewayConfig {
//TODO 响应式编程
//GatewayCallbackManager
public SentinelGatewayConfig(){
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler(){
//网关限流了请求,就会调用此回调 Mono Flux
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
String errJson = JSON.toJSONString(error);
// Mono aaa = Mono.just("aaa");
Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);
return body;
}
});
// FlowRule flowRule = new FlowRule();
// flowRule.setRefResource("gulimall_seckill_route");
flowRule.set
// FlowRuleManager.loadRules(Arrays.asList(flowRule));
}
}
由于微服务项目模块众多,相互之间的调用关系十分复杂,因此为了分析工作过程中的调用关系,需要使用zipkin来进行链路追踪
微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。
链路追踪组件有 Google 的 Dapper,Twitter 的 Zipkin,以及阿里的 Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件。
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 之间的父子关系如下:
在common服务中导入
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
每一个服务添加
logging.level.org.springframework.cloud.openfeign=debug
logging.level.org.springframework.cloud.sleuth=debug
启动测试的各个服务,然后服务调用的时候查看控制台,就能输出相关的日志了
3、发起一次远程调用,观察控制台
DEBUG [user-service,541450f08573fff5,541450f08573fff5,false]
user-service:服务名 541450f08573fff5:是 TranceId,一条链路中,只有一个 TranceId 541450f08573fff5:是 spanId,链路中的基本工作单元 id false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察
上一个章节,我们在控制台查看日志很不方便,接下来我们整个zipkin (可视化通过 Sleuth 产生的调用链监控信息,可以得知微服务之间的调用链路,但监控信息只输出 到控制台不方便查看。我们需要一个图形化的工具-zipkin。Zipkin 是 Twitter 开源的分布式跟 踪系统,主要用来收集系统的时序数据,从而追踪系统的调用问题。zipkin 官网地址如下:
https://zipkin.io/
docker run -d -p 9411:9411 openzipkin/zipkin
放在common模块中
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用
每一个服务都要加入
spring.zipkin.base-url=http://192.168.56.10:9411/
spring.zipkin.discovery-client-enabled=false
spring.zipkin.sender.type=web
spring.sleuth.sampler.probability=1
yaml 配置就采用
spring:
zipkin:
base-url: http://192.168.56.10:9411
sender:
type: web
# 取消nacos对zipkin的服务发现
discovery-client-enabled: false
#采样取值介于 0到1之间,1则表示全部收集
sleuth:
sampler:
probability: 1
发送远程请求,测试 zipkin。服务调用链追踪信息统计
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 |
信息统计
[外链图片转存中…(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 |
[外链图片转存中…(img-5D5seBop-1652453950666)]
[外链图片转存中…(img-0IFPjln2-1652453950666)]
[外链图片转存中…(img-NNh56s4w-1652453950666)]