谷粒商城15-商品秒杀、Sentinel高并发、高并发方法论

十五、商品秒杀

1.后台管理系统增加秒杀

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

查询秒杀场次关联的秒杀商品

@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();

        //场次id
        String promotionSessionId = (String) params.get("promotionSessionId");
        if (!StringUtils.isEmpty(promotionSessionId)){
            queryWrapper.eq("promotion_session_id",promotionSessionId);
        }

        IPage<SeckillSkuRelationEntity> page = this.page(
                new Query<SeckillSkuRelationEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

}

2.定时任务上架商品秒杀

例如:在每天的晚上11点,查询明天所有的秒杀商品

image-20220820103317965

3.cron表达式

  • 七位表达式:秒、分、时、日、月、周、年
  • 周(日——六)对应数字 1——7
  • 但SpringBoot 整合的cron 并不包含年,同时周(一——日)对应数字 1——7。

在线生成器:cron.qqe2.com

image-20220822154838101

image-20220822155101234

4.SpringBoot 整合 cron

SpringBoot 整合的 cron 只有六位字符。同时周(一——日)对应数字 1——7。

使用:

  • 两个注解
  • image-20220822162000516

任务的阻塞性:如果每秒执行一次任务,当当前任务阻塞时,后续的任务会在当前任务完成阻塞之后的一秒后开始执行。

解决方法:

  1. 可以让业务以异步的方式运行,自己提交到线程池

    CompletableFuture.runAsync(() -> {
        xxxxService.xxx();
    },executor);
    
  2. 默认只有一个线程池:

    在有的spring版本中不生效

    image-20220824130256326

    image-20220824130330327

    修改配置文件:

    spring.task.scheduling.pool.size=5
    
  3. @EnableAsync@Async

    image-20220825125719011

5.秒杀商品的查询

查询近三天的秒杀商品:

@GetMapping("/latest3DaySession")
public R getLatest3DaySession(){
    List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
    return R.ok().setData(sessions);
}
@Override
public List<SeckillSessionEntity> getLatest3DaySession() {
    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().plusDays(2);
    LocalTime max = LocalTime.MAX;
    LocalDateTime end = LocalDateTime.of(now,max);
    String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"));
    return format;
}

6.保存秒杀商品到缓存

@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class SeckillSkuScheduled {

    @Autowired
    SeckillService seckillService;

    @Autowired
    RedissonClient redissonClient;

    private final String upload_lock = "seckill:upload:lock";

    @Async
    @Scheduled(cron = "0 * * * * ?")
    public void hello() throws InterruptedException {
        log.info("上架秒杀的商品信息...");

        //分布式锁
        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            seckillService.uploadSeckillSkuLatest3Days();
        }finally {
            lock.unlock();
        }
    }
}
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    CouponFeignService couponFeignService;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;

    @Autowired
    ProductFeignService productFeignService;

    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 r = couponFeignService.getLatest3DaySession();
        if (r.getCode() == 0){
            List<SeckillSessionsWithSkus> data = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });
            //上架商品
            //缓存到Redis
            //1.缓存活动信息
            saveSessionInfos(data);
            //2.缓存活动的关联商品信息
            saveSessionSkuInfos(data);
        }
    }

    /**
     * 缓存活动信息
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions){
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
            //缓存活动信息
            stringRedisTemplate.opsForList().leftPushAll(key,collect);
        });
    }

    /**
     * 缓存活动相关联的商品信息
     * @param sessions
     */
    private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions){
        sessions.stream().forEach(session -> {
            //准备hash操作
            BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //缓存商品
                SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                //1.sku的基本信息
                R info = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if (info.getCode() == 0){
                    SkuInfoTo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoTo>() {
                    });
                    redisTo.setSkuInfo(skuInfo);
                }

                //2.sku的秒杀信息
                BeanUtils.copyProperties(seckillSkuVo,redisTo);

                //3.设置上架商品的秒杀时间信息
                redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());

                //4.随机码
                String token = UUID.randomUUID().toString().replace("-", "");
                redisTo.setRandomCode(token);

                //5.使用库存作为分布式的信号量
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                //商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                String s = JSON.toJSONString(redisTo);
                ops.put(seckillSkuVo.getSkuId().toString(),s);
            });
        });
    }
}

7.分布式服务和缓存的幂等性

执行上面的代码发现,当定时任务被触发 redis 中回不断地缓存相同的数据,违背了缓存的幂等性

同时,如果不同的服务在相同时间定时任务被触发,也会向redis 中缓存相同的数据,所有需要引入分布式锁。

7.1 服务的幂等性:加分布式锁

@EnableAsync
public class SeckillSkuScheduled {

    @Autowired
    SeckillService seckillService;

    @Autowired
    RedissonClient redissonClient;

    private final String upload_lock = "seckill:upload:lock";

    @Async
    @Scheduled(cron = "0 * * * * ?")
    public void hello() throws InterruptedException {
        log.info("上架秒杀的商品信息...");

        //分布式锁
        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            seckillService.uploadSeckillSkuLatest3Days();
        }finally {
            lock.unlock();
        }
    }
}

7.2 活动信息、库存的幂等性

image-20220826135058596

image-20220826135140937

结果:

image-20220826134910510

8.在首页展示秒杀商品

在product服务中

  1. 确定当前时间属于哪个秒杀场次
  2. 确定当前秒杀场次所需要的商品信息
/**
 * 返回当前时间可以参与的秒杀商品信息
 * @return
 */
@ResponseBody
@GetMapping("currentSeckillSkus")
public R getCurrentSeckillSkus(){
    List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
    return R.ok().setData(vos);
}
==================================================================================
    
  $.get("http://seckill.gulimall.com/currentSeckillSkus", function (res) {
    if (res.data.length > 0) {
      res.data.forEach(function (item) {
        $("<li οnclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />"))
                .append($("<p>"+item.skuInfo.skuTitle+"</p>"))
                .append($("<span>" + item.seckillPrice + "</span>"))
                .append($("<s>" + item.skuInfo.price + "</s>"))
                .appendTo("#seckillSkuContent");
      })
    }
  })
/**
 * 返回当前时间可以参与的商品秒杀的信息
 * @return
 */
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
    //1.确定当前时间属于哪个秒杀场次
    long cuTime = new Date().getTime();

    Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
    for (String key : keys) {
        String time = key.replace(SESSIONS_CACHE_PREFIX, "");
        String[] s = time.split("_");
        Long startT = Long.parseLong(s[0]);
        Long endT = Long.parseLong(s[1]);
        if (cuTime >= startT && cuTime <= endT){
            //2.获取这个秒杀场次需要的所有商品信息
            List<String> range = stringRedisTemplate.opsForList().range(key,0, -1);
            BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            List<String> list = hashOps.multiGet(range);
            if (list != null){
                List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
                    SeckillSkuRedisTo redisTo = JSON.parseObject((String) item, SeckillSkuRedisTo.class);
                    return redisTo;
                }).collect(Collectors.toList());
                return collect;
            }
            break;
        }
    }
    return null;
}

image-20220826160739436

9.商品详情页渲染

/**
 * 返回商品详情页的秒杀信息
 * @param skuId
 * @return
 */
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId){
    SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
    return R.ok().setData(to);
}
@Override
public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    Set<String> keys = hashOps.keys();
    if (keys != null && keys.size() > 0){
        String regx = "\\d-" + skuId;
        for (String key : keys) {
            if (Pattern.matches(regx,key)){
                String json = hashOps.get(key);
                SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);

                //随机码
                long currentTime = new Date().getTime();
                Long startTime = skuRedisTo.getStartTime();
                Long endTime = skuRedisTo.getEndTime();
                if (!(currentTime >= startTime && currentTime <= endTime)){
                    skuRedisTo.setRandomCode(null);
                }
                return skuRedisTo;
            }
        }
    }
    return null;
}

如果当前商品处于秒杀中,显示秒杀价格,如果在以后的场次中,显示开始秒杀的时间。

image-20220827111807419

image-20220827111753659

10.商品秒杀流程

image-20220827112156811

image-20220827112254293

image-20220827135515205

10.1 发送消息

商品模块获取到秒杀的各种信息:

image-20220827155801062

前端绑定随机码等数据,发送给秒杀模块

image-20220827155658686

image-20220827155830622

秒杀模块秒杀商品:

@ResponseBody
@GetMapping("/kill")
public R secKill(@RequestParam("killId") String killId,@RequestParam("key") String key,@RequestParam("num") Integer num){
    String orderSn = seckillService.kill(killId,key,num);
    return R.ok().setData(orderSn);
}
@Override
public String kill(String killId, String key, Integer num) {
    MemberResponseTo memberResponseTo = LoginUserInterceptor.loginUser.get();

    //1.获取当前秒杀商品的详细信息
    BoundHashOperations<String, String, String> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    String s = ops.get(killId);
    if (StringUtils.isEmpty(s)){
        return null;
    }else {
        SeckillSkuRedisTo redis = JSON.parseObject(s,SeckillSkuRedisTo.class);
        //校验 合法性
        //1.校验时间
        Long startTime = redis.getStartTime();
        Long endTime = redis.getEndTime();
        long time = new Date().getTime();
        long ttl = endTime - startTime;
        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 = memberResponseTo.getId() + "-" +skuId;
                    //自动过期
                    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey,num.toString(),ttl, TimeUnit.MILLISECONDS);
                    if (aBoolean){
                        //从未买过,占位
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        try {
                            boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                            if (b){
                                //生成订单号
                                String timeId = IdWorker.getTimeId();
                                SeckillOrderTo orderTo = new SeckillOrderTo();
                                orderTo.setOrderSn(timeId);
                                orderTo.setMemberId(memberResponseTo.getId());
                                orderTo.setNum(num);
                                orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                                orderTo.setSkuId(redis.getSkuId());
                                orderTo.setSeckillPrice(redis.getSeckillPrice());
                                //发送MQ消息
                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                                return timeId;
                            }
                            return null;
                        } catch (InterruptedException e) {
                            return null;
                        }
                    }
                }
            }
        }
        return null;
    }
}

10.2 消息队列设置

秒杀模块发送MQ消息给订单模块监听的队列,由订单模块监听并创建秒杀订单。

消息发送流程:

image-20220827144902243

在订单模块接收秒杀模块发送的消息,并处理

配置rabbitMQ相关消息:

spring:
  rabbitmq:
    host: 192.168.137.128
    port: 5672
    virtual-host: /
    #    publisher-confirms: true
    publisher-returns: true

创建消息队列:

image-20220827145633916

监听消息队列:

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

监听到秒杀订单消息->创建订单:

@Override
    public void createSeckillOrder(SeckillOrderTo seckillOrder) {
        MemberResponseTo memberResponseVo = LoginUserInterceptor.loginUser.get();
        //1. 创建订单
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(seckillOrder.getOrderSn());
        orderEntity.setMemberId(seckillOrder.getMemberId());
        if (memberResponseVo!=null){
            orderEntity.setMemberUsername(memberResponseVo.getUsername());
        }
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        orderEntity.setCreateTime(new Date());
        orderEntity.setPayAmount(seckillOrder.getSeckillPrice().multiply(new BigDecimal(seckillOrder.getNum())));
        this.save(orderEntity);
        //2. 创建订单项
        R r = productFeignService.getSpuInfoBySkuId(seckillOrder.getSkuId());
        if (r.getCode() == 0) {
            SeckillSkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SeckillSkuInfoVo>() {
            });
            OrderItemEntity orderItemEntity = new OrderItemEntity();
            orderItemEntity.setOrderSn(seckillOrder.getOrderSn());
            orderItemEntity.setSpuId(skuInfo.getSpuId());
            orderItemEntity.setCategoryId(skuInfo.getCatalogId());
            orderItemEntity.setSkuId(skuInfo.getSkuId());
            orderItemEntity.setSkuName(skuInfo.getSkuName());
            orderItemEntity.setSkuPic(skuInfo.getSkuDefaultImg());
            orderItemEntity.setSkuPrice(skuInfo.getPrice());
            orderItemEntity.setSkuQuantity(seckillOrder.getNum());
            orderItemService.save(orderItemEntity);
        }
    }

测试:

image-20220827171958232

image-20220827172013629

image-20220827172042398

十六、Sentinel 高并发

1.SpringCloud Aliababa Sentinel

1.1 熔断降级限流

  • 什么是熔断

    A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影响到 A。

  • 什么是降级

    整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。

异同:

  • 相同点:

    • 为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我
    • 用户最终都是体验到某个功能不可用
  • 不同点:

    • 熔断是被调用方故障,触发的系统主动规则
    • 降级是基于全局考虑,停止一些正常服务,释放资源
  • 什么是限流

    对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力

1.2 Sentinel 简介

  • 官方文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

  • 项目地址:https://github.com/alibaba/Sentinel

    随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

  • Sentinel 具有以下特征:

    • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
    • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
    • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
    • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

image-20220827174306992

  • Sentinel 分为两个部分:

    • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时 对 Dubbo / Spring Cloud 等框架也有较好的支持。
    • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
  • Sentinel 基本概念

    • 资源

      资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提 供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文 档中,我们都会用资源来描述代码块。

  • 只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下, 可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

  • 规则

    围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规 则。所有规则可以动态实时调整。

Hystrix 与 Sentinel 比较

image-20220827174827376

  • Hystric隔离是线程池隔离,对于某个请求如只允许50个线程并发访问,多的并发会被拒绝,多个请求对应多个线程池,这样会浪费线程池资源,增加服务器压力,而Sential使用的是类似redis的信号量

  • Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例
    如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,
    避免影响到其它的资源而导致级联故障。

2.官方文档 quick-start

https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5

什么是熔断降级

除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关 系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。

image-20220827174752512

熔断降级设计理念

在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法:

  • Hystrix 通过 线程池隔离 的方式,来对依赖(在 Sentinel 的概念中对应 资源)进行了隔 离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成 本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。

  • Sentinel 对这个问题采取了两种手段:

    • 通过并发线程数进行限制

      和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其 它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个 资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步 堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积 的线程完成任务后才开始继续接收请求。

    • 通过响应时间对资源进行降级

      除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。 当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的 时间窗口之后才重新恢复

整合限流测试

官方文档:quick-start (sentinelguard.io)

sentinel的使用主要包括这三步:

  1. 定义资源
  2. 定义规则
  3. 检验规则是否生效

image-20220827180032679

3.SpringBoot 整合 Sentinel

官方文档:Sentinel · alibaba/spring-cloud-alibaba Wiki (github.com)

3.1 导入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

3.2 启动Sentinel 的控制台

在官网下载对应版本的 sentinel jar包 。Releases · alibaba/Sentinel (github.com)

image-20220827181512490

直接启动默认使用的是80端口,可能会被占用

使用命令:java -jar sentinel-dashboard-1.8.1.jar --server.port=8033

启动成功:

image-20220827181900448

访问8033端口,账号密码默认都是 sentinel。

sentinel 的控制台是懒加载机制,只有当请求进来的时候,才会有各种操作选项。

image-20220827182042839

配置控制台地址:

spring.cloud.sentinel.transport.dashboard=localhost:8033
spring.cloud.sentinel.transport.port=8719  //控制台与后端微服务之间传输数据的端口

重启服务:

出现报错:The Bean Validation API is on the classpath but no implementation could be found
Add an implementation, such as Hibernate Validator, to the classpath
以及依赖循环

参考文章:【已解决】报错:Add an implementation, such as Hibernate Validator, to the classpat 导包之后依旧依赖循环_HotRabbit.的博客-CSDN博客

发起请求之后:

image-20220828121340105

设置每秒QPS 为1,即每秒只能通过一个请求:

image-20220828121415040

image-20220828121453512

3.3 信息审计功能引入

前面的测试存在问题

  • 在控制台调整限流的参数,都保存在内存中,重启失效
  • 为了保证能够持久的保存限流规则,需要导入信息审计模块

导入依赖

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

新版已经不需要暴露端口了

实时监控:

image-20220828124430693

3.4 自定义Sentinel 限流返回信息

WebCallbackManager已经不能使用了

package com.henu.soft.merist.seckill.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.henu.soft.merist.common.exception.BizCodeEnume;
import com.henu.soft.merist.common.utils.R;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;



@Component
public class SecKillSentinelConfig implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(error));
    }


}

image-20220828125719149

4.Sentinel 整合 feign

上面Sentinel 没有识别到 feign 远程调用的链路,接下来整合feign

image-20220828131812594

配置feign开启熔断降级:

feign.sentinel.enabled=true

指定远程调用失败返回的配置类

image-20220828132517234

package com.henu.soft.merist.gulimall.product.feign.fallback;

import com.henu.soft.merist.common.exception.BizCodeEnume;
import com.henu.soft.merist.common.utils.R;
import com.henu.soft.merist.gulimall.product.feign.SeckillFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SeckillFeignServiceFallback implements SeckillFeignService {
    @Override
    public R getSkuSeckillInfo(Long skuId) {
        log.info("熔断触发");
        return R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
    }
}

其中遇到了一些版本问题的报错,一些参考解决方法:

feign整合sentinel报错java.lang.ClassNotFoundException: feign.hystrix.FallbackFactory$Default_Albertliuc的博客-CSDN博客

Caused by: java.lang.ClassNotFoundException: com.netflix.config.DeploymentContext$ContextKey_yandajiangjun的博客-CSDN博客

image-20220828162143846

5.自定义受保护的资源

1.try catch方法

try(Entry entry = SphU.entry("seckillSkus")) 设置了资源名为 seckillSku,可以在控制台中熔断降级

image-20220828164736659

2.注解@SentinelReource

设置资源名为getCurrentSeckillSkusResource

image-20220828164913531

image-20220828165352556

更多定义资源的方法可以参考官网:basic-api-resource-rule (sentinelguard.io)

6.网关流控

api-gateway-flow-control (sentinelguard.io)

导入依赖

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

        </dependency>

重启服务,重启控制台的jar包

gateway模块有api管理服务

image-20220828170856643

请求链路也能获取到其他服务的请求

image-20220828170926043

可以直接在流控规则中配置各个微服务的流控

API 名称就是网关配置的id

image-20220828171226620

网关流控的各种熟悉在官网都有:

image-20220828171306231

定制网关流控返回

package com.henu.soft.merist.gulimall.gateway.config;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.fastjson.JSON;
import com.henu.soft.merist.common.exception.BizCodeEnume;
import com.henu.soft.merist.common.utils.R;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

/**
 * <p>Title: SentinelGateWayConfig</p>
 * Description:
 * date:2020/7/10 17:57
 */
@Configuration
public class SentinelGateWayConfig {

    public SentinelGateWayConfig(){
        GatewayCallbackManager.setBlockHandler((exchange, t) ->{
            // 网关限流了请求 就会回调这个方法
            R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
            String errJson = JSON.toJSONString(error);
            Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);
            return body;
        });
    }
}

7.SpringCloud Sleuth + Zipkin服务链路追踪

7.1 为什么用

  • 定位问题微服务,更好的熔断降级
  • 微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务
    单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要
    体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以
    定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,
    参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。
  • 链路追踪组件有 Google 的 Dapper,Twitter 的 Zipkin,以及阿里的 Eagleeye (鹰眼)等,它
    们都是非常优秀的链路追踪开源组件。

7.2 基本术语

  • 每经过调用一个微服务,更新span,记录cs、sr的时间戳

  • 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

如果服务调用顺序如下:

image-20220828173739199

那么用以上概念完整的表示出来如下:

image-20220828173751551

Span 之间的父子关系如下:

image-20220828173802091

7.3 整合 Sleuth

1、服务提供者与消费者导入依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2、打开 debug 日志

logging:
	level:
		org.springframework.cloud.openfeign: debug
		org.springframework.cloud.sleuth: debug

3、发起一次远程调用,观察控制台

DEBUG [user-service,541450f08573fff5,541450f08573fff5,false]
user-service:服务名
  • 541450f08573fff5:是 TranceId,一条链路中,只有一个 TranceId
  • 541450f08573fff5:是 spanId,链路中的基本工作单元 id
  • false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察

7.4 整合 zipkin 可视化观察

  • 通过 Sleuth 产生的调用链监控信息,可以得知微服务之间的调用链路,但监控信息只输出 到控制台不方便查看。我们需要一个图形化的工具-zipkin。
  • Zipkin 是 Twitter 开源的分布式跟 踪系统,主要用来收集系统的时序数据,从而追踪系统的调用问题。
  • zipkin 官网地址: https://zipkin.io

image-20220828174732471

1、docker 安装 zipkin 服务器

docker run -d -p 9411:9411 openzipkin/zipkin

2、导入

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用

3、添加 zipkin 相关配置

spring:
	application:
		name: user-service
	zipkin:
		base-url: http://192.168.56.10:9411/ # zipkin 服务器的地址
		# 关闭服务发现,否则 Spring Cloud 会把 zipkin 的 url 当做服务名称
		discoveryClientEnabled: false
		sender:
			type: web # 设置使用 http 的方式传输数据
	sleuth:
		sampler:
			probability: 1 # 设置抽样采集率为 100%,默认为 0.1,即 10%

发送远程请求,测试 zipkin。

image-20220828174935625

7.5 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-dependencie

image-20220828175232707

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HotRabbit.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值