atguigu7 秒杀_分布式锁/分布式信号量_MQ_Sentinel (seckill)

  • Post author:
  • Post category:其他


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


sentinel_价值成长的博客-CSDN博客


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;
}



版权声明:本文为xd592319702原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。