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


    十五、商品秒杀

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

    - id: gulimall-coupon
      uri: lb://gulimall-coupon
      predicates:
        - Path=/api/coupon/**
      filters:
        - RewritePath=/api/(?>/?.*),/$\{segment}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    @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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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);
      
      • 1
      • 2
      • 3
    2. 默认只有一个线程池:

      在有的spring版本中不生效

      image-20220824130256326

      image-20220824130330327

      修改配置文件:

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

      image-20220825125719011

    5.秒杀商品的查询

    查询近三天的秒杀商品:

    @GetMapping("/latest3DaySession")
    public R getLatest3DaySession(){
        List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
        return R.ok().setData(sessions);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    @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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    @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);
                });
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93

    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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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) {
            $("
  • "
    ).append($("")) .append($("

    "+item.skuInfo.skuTitle+"

    "
    )) .append($("" + item.seckillPrice + "")) .append($("" + item.skuInfo.price + "")) .appendTo("#seckillSkuContent"); }) } })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    /**
     * 返回当前时间可以参与的商品秒杀的信息
     * @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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    @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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    @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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    10.2 消息队列设置

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

    消息发送流程:

    image-20220827144902243

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

    配置rabbitMQ相关消息:

    spring:
      rabbitmq:
        host: 192.168.137.128
        port: 5672
        virtual-host: /
        #    publisher-confirms: true
        publisher-returns: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    创建消息队列:

    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);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    @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);
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    测试:

    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.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    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  //控制台与后端微服务之间传输数据的端口
    
    • 1
    • 2

    重启服务:

    出现报错: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.bootgroupId>
         <artifactId>spring-boot-starter-actuatorartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

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

    实时监控:

    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));
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    image-20220828125719149

    4.Sentinel 整合 feign

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

    image-20220828131812594

    配置feign开启熔断降级:

    feign.sentinel.enabled=true
    
    • 1

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

    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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    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)

    导入依赖

    
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
                <version>2.2.6.RELEASEversion>
    
            dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    重启服务,重启控制台的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;
    
    /**
     * 

    Title: SentinelGateWayConfig

    * 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; }); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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.cloudgroupId>
    	<artifactId>spring-cloud-starter-sleuthartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    2、打开 debug 日志

    logging:
    	level:
    		org.springframework.cloud.openfeign: debug
    		org.springframework.cloud.sleuth: debug
    
    • 1
    • 2
    • 3
    • 4

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

    DEBUG [user-service,541450f08573fff5,541450f08573fff5,false]
    user-service:服务名
    
    • 1
    • 2
    • 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
    
    • 1

    2、导入

    <dependency>
    	<groupId>org.springframework.cloudgroupId>
    	<artifactId>spring-cloud-starter-zipkinartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    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%
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    发送远程请求,测试 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
    
    • 1

    image-20220828175232707

  • 相关阅读:
    ros1 实现Server端自定义四 Topic模式控制海龟运动
    电脑乐园杂志电脑乐园杂志社电脑乐园编辑部2023年第3期目录
    五个做原型的好处和意义
    前端学习笔记--React
    c语言提高学习笔记——02-c提高03day
    nvidia-smi常用选项汇总
    什么是Executors框架?
    手把手带你学SQL—牛客网SQL 查找后排序
    Java -【字符串,数组,哈希表】常用操作
    简单钟表动画
  • 原文地址:https://blog.csdn.net/qq_52476654/article/details/126572324