0. 前言
https://blog.csdn.net/xd592319702/article/details/127003415
0.1 秒杀架构:
0.2 分布式下定时任务问题:引入分布式锁
秒杀商品上架时,不能多个服务同时上架,防止重复上架
秒杀时,不能多个服务同时秒杀成功,防止重复秒杀
0.3 秒杀系统关注的问题和解决:
秒杀 问题 + 解决
1. 服务单一职责+独立部署:
秒杀服务即使自己扛不住压力,挂掉。不要影响别人
解决:新增秒杀服务
2. 秒杀链接加密
防止恶意攻击,模拟秒杀请求,1000次ls攻击。
防止链接暴露.自己工作人员,提前秒杀商品。
解决:请求需要随机码,在秒杀开始时随机码才会放在商品信息中
3. 库存预热+快速扣减【限流,并发信号量】
秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求
解决:库存放入redis中,使用分布式信号量扣减+限流
4. 动静分离
nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
使用CDN网络,分担本集群压力
解决:nginx
例:10万个人来访问商品详情页,这个详情页会发送63个请求,但是只有3个请求到达后端,60个请求是前端的。一共30万请求到达后端,600万个请求到达nginx或cdn
5. 恶意请求拦截
识别非法攻击请求并进行拦截,网关层拦截,放行到后太服务的请求都是正常请求
在网关层拦截:一些不带令牌的请求循环发送
解决:使用网关拦截
本系统做了登录拦截器【在各微服务创建的,未登录跳转登录页面】
6. 流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车
1、输入验证码需要时间,将流量错开了【速度有快有慢】
2、加入购物车,然后再结算【速度有快有慢】
解决:使用购物车逻辑
7. 限流&熔断&降级
前踹限流+后端限流
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
前端限流:1、每次点击1s后才能再次点击
2、验证登录
后端限流:1、网关限流,例如访问秒杀的流量到达10W等2S再将请求传过去【其中10W是集群的峰值】
2、就算是合理的10次也只放行1-2次
3、熔断:远程访问失败,快速返回,并且下次不要再请求这个节点【防止请求长时间等待】
4、降级:请求量太大了,直接将请求转发到一个错误页面
出现一种情况:集群的处理能力是10W,网关放行了10W的请求,但此时秒杀服务掉线了2台,处理能力下降导致请求堆积,最后资源耗尽服务器全崩了
解决:spring alibaba sentinel
以前是Hystrix,现在不更新了就不用了
8. 队列削峰
100万个商品,每个商品的秒杀库存是100,会产生1亿的流量到后台,全部放入队列中,然后订单监听队列一个个创建订单扣减库存
解决:秒杀服务将创建订单的请求存入mq,订单服务监听mq。
优点:要崩只会崩秒杀服务,不会打垮其他服务【商品服务、订单服务、购物车服务】【第一套实现逻辑会导致这些问题】【看秒杀请求的两种实现】
0.4 redis秒杀方案:
京东秒杀:商品价格改为秒杀价格 -> 加入购物车 -> 下单
本项目:单独的秒杀微服务模块:秒杀商品上架 -> 秒杀 -> 下单
1. 秒杀商品定时上架 (Redisson分布式锁、Redisson信号量、随机码)
定时任务:
定时任务不该阻塞,Spring Schedule 默认是阻塞的,以下是定时任务的实现:
1)、可以让业务以异步的方式,自己提交到线程池
CompletableFuture.runAsync(() -> {},execute);
2)、支持定时任务线程池;
设置 TaskSchedulingProperties
spring.task.scheduling.pool.size: 5
3)、使用异步任务让定时任务异步执行(推荐)
解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
定时任务
1、@EnableScheduling 开启定时任务
2、@Scheduled 开启一个定时任务
3、自动配置类 TaskSchedulingAutoConfiguration
异步任务
1、@EnableAsync 开启异步任务功能
2、@Async 给希望异步执行的方法上标注
3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在TaskExecutionProperties
1.1 seckill:
config:
ScheduledConfig.java
package com.atguigu.gulimall.seckill.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableAsync
@EnableScheduling
@Configuration
public class ScheduledConfig {
}
scheduled:
SeckillSkuScheduled.java
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();
}
}
}
service:
SeckillService.java
SeckillServiceImpl.java
@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));
}
});
}
// 在 Redis 中保存秒杀商品信息(使用Redisson信号量、随机码)
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;
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());
}
});
});
}
@Override
public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
//1、找到所有需要参与秒杀的商品的key
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
Set<String> keys = hashOps.keys();
if (keys != null && keys.size() > 0) {
String regx = "\\d_" + skuId;
for (String key : keys) {
//6_4
if (Pattern.matches(regx, key)) {
String json = hashOps.get(key);
SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
//TODO 加入非空判断
if (skuRedisTo == null) return null;
//随机码
long current = new Date().getTime();
if (current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()) {
//TODO
} else {
//TODO 当前商品已经过了秒杀时间要直接删除
hashOps.delete(key);
skuRedisTo.setRandomCode(null);
}
return skuRedisTo;
}
;
}
}
return null;
}
2. 秒杀流程
- 点击立即抢购时,会发送请求
-
秒杀请求会对请求校验
时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量
,通过校验的则秒杀成功,发送消息创建订单
2.1 seckill:
controller:
SeckillController.java
@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";
}
service:
SeckillService.java
SeckillServiceImpl.java
// 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;
}
2.2 order:
config:
MyMQConfig.java
@Bean
public Queue orderSeckillOrderQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
return new Queue("order.seckill.order.queue",true,false,false);
}
@Bean
public Binding orderSeckillOrderQueueBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map<String, Object> arguments
*/
return new Binding("order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
}
listener:
OrderSeckillListener.java
@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);
}
}
}
service:
OrderService.java
OrderServiceImpl.java
@Override
public void createSeckillOrder(SeckillOrderTo seckillOrder) {
//TODO 保存订单信息
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(seckillOrder.getOrderSn());
orderEntity.setMemberId(seckillOrder.getMemberId());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));
orderEntity.setPayAmount(multiply);
this.save(orderEntity);
//TODO 保存订单项信息
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setOrderSn(seckillOrder.getOrderSn());
orderItemEntity.setRealAmount(multiply);
//TODO 获取当前SKU的详细信息进行设置 productFeignService.getSpuInfoBySkuId()
orderItemEntity.setSkuQuantity(seckillOrder.getNum());
orderItemService.save(orderItemEntity);
}
3. Sentinel
3.1 网关层面限流 (第一层)
pom.xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 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>
config:
SentinelGatewayConfig.java
@Configuration
public class SentinelGatewayConfig {
//TODO 响应式编程
//GatewayCallbackManager
public SentinelGatewayConfig(){
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler(){
//网关限流了请求,就会调用此回调 Mono Flux
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
String errJson = JSON.toJSONString(error);
// Mono<String> aaa = Mono.just("aaa");
Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);
return body;
}
});
// FlowRule flowRule = new FlowRule();
// flowRule.setRefResource("gulimall_seckill_route");
flowRule.set
// FlowRuleManager.loadRules(Arrays.asList(flowRule));
}
}
3.2 代码层面熔断,限流,降级 (第二层)
1、整合Sentinel
1)、导入依赖 spring-cloud-starter-alibaba-sentinel
2)、下载sentinel的控制台
3)、配置sentinel控制台地址信息
4) 、在控制台调整参数。【默认所有的流控设置保存在内存中,重启失效】
2、每一个微服务都导入 actuator ();并配合management.endpoints.web.exposure.include=*
3、自定义sentinel流控返回数据
4、使用Sentinel来保护feign远程调用:熔断;
1)、调用方的熔断保护:feign.sentinel.enabled=true
2)、调用方手动指定远程服务的降级策略。远程服务被降级处理。触发我们的熔断回调方法
3)、超大浏览的时候,必须牺牲一些远程服务。在服务的提供方(远程服务)指定降级策略;
提供方是在运行。但是不运行自己的业务逻辑,返回的是默认的降级数据(限流的数据),
5、自定义受保护的资源
1)、代码
try(Entry entry = SphU.entry("seckillSkus")){
//业务逻辑
}
catch(Execption e){}
2)、基于注解。
@SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
无论是1,2方式一定要配置被限流以后的默认返回.
url请求可以设置统一返回:WebCallbackManager
sentinel配置好后,受保护分为两类:
1). 请求:所有url请求都会被扫描
2). 资源:通过 @SentinelResource 配置的资源
核心:
1). 配置Sentinel 会对 Feign 进行监控
feign.sentinel.enabled=true
2). Feign 的降级:在
@FeignClient
设置
fallback
属性
@FeignClient(value = "kedamall-seckill",fallback = SeckillFeignServiceFallback.class)
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
3.2.1 product: 调用方/服务调用方(熔断,限流)
pom.xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
application.properties
spring.cloud.sentinel.transport.port=8719
spring.cloud.sentinel.transport.dashboard=localhost:8333
management.endpoints.web.exposure.include=*
feign.sentinel.enabled=true
web:
ItemController.java
@Controller
public class ItemController {
@Autowired
SkuInfoService skuInfoService;
/**
* 展示当前sku的详情
* @param skuId
* @return
*/
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId, Model model) throws ExecutionException, InterruptedException {
System.out.println("准备查询"+skuId+"详情");
SkuItemVo vo = skuInfoService.item(skuId);
model.addAttribute("item",vo);
return "item";
}
}
service:
SkuInfoService.java
SkuInfoServiceImpl.java
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息的获取 pms_sku_info
SkuInfoEntity info = this.getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3、获取spu的销售属性组合
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesc(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//5、获取spu的规格参数信息
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
//2、sku的图片信息 pms_sku_images
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> imagesEntities = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(imagesEntities);
}, executor);
CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
//3、远程调用查询当前sku是否参与秒杀优惠活动
R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
if (skuSeckilInfo.getCode() == 0) {
//查询成功
SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
});
skuItemVo.setSeckillSkuVo(seckilInfoData);
if (seckilInfoData != null) {
long currentTime = System.currentTimeMillis();
if (currentTime > seckilInfoData.getEndTime()) {
skuItemVo.setSeckillSkuVo(null);
}
}
}
}, executor);
//等到所有任务都完成
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();
return skuItemVo;
}
feign:
SeckillFeignService.java
@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
feign.fallback:
SeckillFeignServiceFallBack.java
@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());
}
}
3.2.2 seckill:被调方/服务提供方(降级):全局流量大
pom.xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
application.properties
spring.cloud.sentinel.transport.port=8719
spring.cloud.sentinel.transport.dashboard=localhost:8333
management.endpoints.web.exposure.include=*
feign.sentinel.enabled=true
config: 自定义返回
SeckillSentinelConfig.java
@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());
}
}
3.2.3 seckill:自定义受保护的资源
/**
* 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;
}