谷粒商城项目篇15_分布式高级篇_商品秒杀服务、高并发方法论

目录

一、商品秒杀

  1. 后台上架秒杀商品
  2. 定时任务定时上架秒杀商品
  3. 商品秒杀流程

二、高并发方法论

  1. Sentinel

    1. 熔断降级限流
    2. Sentinel简介
    3. Sentinel基本概念
    4. 使用测试限流、降级、熔断
  2. Sleuth + Zipkin服务链路追踪

    • 为什么用
    • 基本术语
    • 整合
    • Zipkim数据持久化

一、商品秒杀

1.后台上架秒杀商品

	# 优惠服务,秒杀服务- id: coupon_routeruri: lb://guli-shop-couponpredicates:- Path=/api/coupon/**filters:- RewritePath=/api/(?.*),/$\{segment}

新增秒杀时间段,关联秒杀商品

package henu.soft.xiaosi.coupon.service.impl;import com.alibaba.cloud.commons.lang.StringUtils;
import jdk.nashorn.internal.ir.CallNode;
import org.springframework.stereotype.Service;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import henu.soft.common.utils.PageUtils;
import henu.soft.common.utils.Query;import henu.soft.xiaosi.coupon.dao.SeckillSkuRelationDao;
import henu.soft.xiaosi.coupon.entity.SeckillSkuRelationEntity;
import henu.soft.xiaosi.coupon.service.SeckillSkuRelationService;@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {/*** 根据秒杀场次的sessionId* 查询秒杀场次的关联商品列表* @param params* @return*/@Overridepublic PageUtils queryPage(Map<String, Object> params) {QueryWrapper<SeckillSkuRelationEntity> wrapper =  new QueryWrapper<SeckillSkuRelationEntity>();// 获取秒杀时间段关联的商品,场次idString promotionSessionId = (String) params.get("promotionSessionId");if (!StringUtils.isEmpty(promotionSessionId)){wrapper.eq("promotion_session_id",promotionSessionId);}IPage<SeckillSkuRelationEntity> page = this.page(new Query<SeckillSkuRelationEntity>().getPage(params),wrapper);return new PageUtils(page);}}

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

定时上架商品,为了不同场次可以上架相同的商品信息,因此redis存储的key为sessionId-skuId 场次信息-商品信息
在这里插入图片描述

1.cron表达式

概念

  • 官网:http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
  • 由6个空格隔开的字符分别表示 秒、分、时、天、月
  • cron表达式在线生成:https://cron.qqe2.com/

在这里插入图片描述

在这里插入图片描述

2.整合SpringBoot

定时任务

  • 创建XxxSchedule类,放置容器
  • 类上加@EnableScheduling
  • 方法上加@Scheduled(cron=“xxx”)开启定时任务

注意

  • 定时任务:不应该阻塞执行,可以使用异步线程池CompletableFuture.runAsync()执行,但是有些版本的springboot配置会不生效,即是阻塞的,因此需要手动异步
  • 异步:让定时任务直接异步执行,类上加注解@EnableAsync,方法上加@Async
package henu.soft.xiaosi.seckill.config;import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;/*** 

Title: ScheduledConfig

* Description:* date:2020/7/6 17:28*/
@EnableAsync @Configuration @EnableScheduling public class ScheduledConfig {}
3.编写代码

秒杀服务调用优惠服务,扫描秒杀商品

在这里插入图片描述

在这里插入图片描述

package henu.soft.xiaosi.coupon.service.impl;import henu.soft.xiaosi.coupon.entity.SeckillSkuRelationEntity;
import henu.soft.xiaosi.coupon.service.SeckillSkuRelationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import henu.soft.common.utils.PageUtils;
import henu.soft.common.utils.Query;import henu.soft.xiaosi.coupon.dao.SeckillSessionDao;
import henu.soft.xiaosi.coupon.entity.SeckillSessionEntity;
import henu.soft.xiaosi.coupon.service.SeckillSessionService;@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {@AutowiredSeckillSkuRelationService skuRelationService;@Overridepublic PageUtils queryPage(Map<String, Object> params) {IPage<SeckillSessionEntity> page = this.page(new Query<SeckillSessionEntity>().getPage(params),new QueryWrapper<SeckillSessionEntity>());return new PageUtils(page);}@Overridepublic List<SeckillSessionEntity> getLate3DaySession() {// 计算最近三天的时间List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));if(list != null && list.size() > 0){return list.stream().map(session -> {// 给每一个活动写入他们的秒杀项Long id = session.getId();List<SeckillSkuRelationEntity> entities = skuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));session.setRelationSkus(entities);return session;}).collect(Collectors.toList());}return null;}private String startTime(){LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);return start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}private String endTime(){LocalDate acquired = LocalDate.now().plusDays(2);return LocalDateTime.of(acquired, LocalTime.MAX).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}}

秒杀服务,需要定时执行的秒杀服务上架

package henu.soft.xiaosi.seckill.scheduel;import henu.soft.xiaosi.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;/*** 

Title: SeckillSkuScheduled

* Description:秒杀商品定时上架 [秒杀的定时任务调度]* date:2020/7/6 17:28*/
@Slf4j @Service public class SeckillSkuScheduled {@Autowiredprivate SeckillService seckillService;@Autowiredprivate RedissonClient redissonClient;private final String upload_lock = "seckill:upload:lock";/*** 这里应该是幂等的* 三秒执行一次:* /3 * * * * ?* 8小时执行一次:0 0 0-8 * * ?*/@Scheduled(cron = "0 0 0-8 * * ?")public void uploadSeckillSkuLatest3Day(){log.info("\n上架秒杀商品的信息");// 1.重复上架无需处理 加上分布式锁 状态已经更新 释放锁以后其他人才获取到最新状态RLock lock = redissonClient.getLock(upload_lock);lock.lock(10, TimeUnit.SECONDS);try {seckillService.uploadSeckillSkuLatest3Day();} finally {lock.unlock();}} }
@Overridepublic void uploadSeckillSkuLatest3Day() {// 1.扫描最近三天要参加秒杀的商品R r = couponFeignService.getLate3DaySession();if(r.getCode() == 0){List<SeckillSessionsWithSkus> sessions = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {});// 2.缓存活动信息saveSessionInfo(sessions);// 3.缓存活动的关联的商品信息saveSessionSkuInfo(sessions);}}
  • 保存秒杀活动时间信息
  • 保存秒杀活动商品信息

// 缓存活动信息,保存到redis
private void saveSessionInfo(List<SeckillSessionsWithSkus> sessions){if(sessions != null){sessions.stream().forEach(session -> {long startTime = session.getStartTime().getTime();long endTime = session.getEndTime().getTime();String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;Boolean hasKey = stringRedisTemplate.hasKey(key);if(!hasKey){// 获取所有商品idList<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId()).collect(Collectors.toList());// 缓存活动信息stringRedisTemplate.opsForList().leftPushAll(key, collect);}});}}private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){if(sessions != null){sessions.stream().forEach(session -> {BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);session.getRelationSkus().stream().forEach(seckillSkuVo -> {// 1.商品的随机码String randomCode = UUID.randomUUID().toString().replace("-", "");if(!ops.hasKey(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId())){// 2.缓存商品及其详细信息SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();BeanUtils.copyProperties(seckillSkuVo, redisTo);// 3.sku的基本数据 sku的秒杀信息R info = productFeignService.skuInfo(seckillSkuVo.getSkuId());if(info.getCode() == 0){SkuInfoVo skuInfo = info.getData( new TypeReference<SkuInfoVo>() {});redisTo.setSkuInfoVo(skuInfo);}// 4.设置当前商品的秒杀信息redisTo.setStartTime(session.getStartTime().getTime());redisTo.setEndTime(session.getEndTime().getTime());redisTo.setRandomCode(randomCode);ops.put(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId(), JSON.toJSONString(redisTo));// 如果当前这个场次的商品库存已经上架就不需要上架了// 5.使用库存作为分布式信号量  限流RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());}});});}}================== redis 不仅需要保存skuId、eeckillPrice等信息,还需要保存秒杀商品的详细信息。方便下单============
package henu.soft.xiaosi.seckill.to;import henu.soft.xiaosi.seckill.vo.SkuInfoVo;
import lombok.Data;import java.math.BigDecimal;/*** 

Title: SeckillSkuRedisTo

* Description:* date:2020/7/6 19:08*/
@Data public class SeckillSkuRedisTo {private Long promotionId;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 商品的秒杀随机码*/private String randomCode;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 秒杀总量*/private BigDecimal seckillCount;/*** 每人限购数量*/private BigDecimal seckillLimit;/*** 排序*/private Integer seckillSort;/*** sku的详细信息*/private SkuInfoVo skuInfoVo;/*** 商品秒杀的开始时间*/private Long startTime;/*** 商品秒杀的结束时间*/private Long endTime; }
4.注意问题
1.设置商品随机码

秒杀请求需要带上随机码,随机码于活动开始才有效
在这里插入图片描述

2.使用分布式信号量解决秒杀库存问题(限流)

在这里插入图片描述

package henu.soft.xiaosi.seckill.config;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.nio.charset.Charset;/*** 

Title: MyRedisConfig

* Description:* date:2020/6/11 12:33*/
@Configuration public class MyRedisConfig {@Value("${ipAddr}")private String ipAddr;@Bean(destroyMethod = "shutdown")public RedissonClient redisson() {Config config = new Config();// 创建单例模式的配置config.useSingleServer().setAddress("redis://" + ipAddr + ":6379");return Redisson.create(config);}MyRedisConfig(){//打开autotype功能,需要强转的类一次添加其后ParserConfig.getGlobalInstance().addAccept("henu.soft.common.to.MemberResponseTo,org.springframework.web.servlet.FlashMap");}@Beanpublic StringRedisTemplate redisTemplate(RedisConnectionFactory factory){StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(factory);StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();//设置key采用String的序列化方式template.setKeySerializer(stringRedisSerializer);//设置hash的key也采用String的序列化方式template.setHashKeySerializer(stringRedisSerializer);FastJson2JsonRedisSerializer fastJson2JsonRedisSerializer =new FastJson2JsonRedisSerializer<Object>(Object.class);//设置value采用的fastjson的序列化方式template.setValueSerializer(fastJson2JsonRedisSerializer);//设置hash的value采用的fastjson的序列化方式template.setHashValueSerializer(fastJson2JsonRedisSerializer);//设置其他默认的序列化方式为fastjsontemplate.setDefaultSerializer(fastJson2JsonRedisSerializer);template.afterPropertiesSet();return template;}public static class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;public FastJson2JsonRedisSerializer(Class<T> clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException {if (bytes == null || bytes.length <= 0) {return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}} }

在这里插入图片描述

3.秒杀商品上架的幂等性

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5.首页展示秒杀商品信息
  • 确定当前时间属于哪个秒杀场次
  • 确定当前秒杀场次所需要的商品信息
/*** 首页展示秒杀商品信息* @return*/@Overridepublic List<SeckillSkuRedisTo> getCurrentSeckillSkus() {// 1.确定当前时间属于那个秒杀场次long time = new Date().getTime();// 定义一段受保护的资源Set<String> keys = stringRedisTemplate.keys(SESSION_CACHE_PREFIX + "*");for (String key : keys) {// seckill:sessions:1593993600000_1593995400000String replace = key.replace("seckill:sessions:", "");String[] split = replace.split("_");long start = Long.parseLong(split[0]);long end = Long.parseLong(split[1]);if(time >= start && time <= end){// 2.获取这个秒杀场次的所有商品信息List<String> range = stringRedisTemplate.opsForList().range(key, 0, 100);BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);List<String> list = hashOps.multiGet(range);if(list != null){return list.stream().map(item -> {SeckillSkuRedisTo redisTo = JSON.parseObject(item, SeckillSkuRedisTo.class);
//						redisTo.setRandomCode(null);return redisTo;}).collect(Collectors.toList());}break;}}return null;
}

/*** 首页展示秒杀商品信息* @return*/@ResponseBody@GetMapping("/currentSeckillSkus")public R getCurrentSeckillSkus(){List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();return R.ok().setData(vos);}
================  页面发ajax请求 ======================$.get("http://seckill.gulishop.cn/currentSeckillSkus",function (resp) {if(resp.data.length > 0){resp.data.forEach(function (item) {$("
  • "
    ).append($("")).append($("

    " + item.skuInfoVo.skuTitle + "

    "
    )).append($("¥" + item.seckillPrice + "")).append($("¥" + item.skuInfoVo.price + "")).appendTo("#seckillSkuContent");})}})
    6.商品详情页显示秒杀信息

    商品服务封装商品信息的时候,调用秒杀服务查询是否当前商品存在秒杀时间段

    在这里插入图片描述

    在这里插入图片描述

    秒杀服务

    /*** 商品详情页显示秒杀时间* @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);}
    
    /*** 商品详情页显示秒杀信息* @param skuId* @return*/@Overridepublic 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 to = JSON.parseObject(json, SeckillSkuRedisTo.class);// 处理一下随机码long current = new Date().getTime();if(current <= to.getStartTime() || current >= to.getEndTime()){to.setRandomCode(null);}return to;}}}return null;}
    
    <li style="color: red" th:if="${item.seckillInfoVo!=null}"><span th:if="${#dates.createNow().getTime() < item.seckillInfoVo.startTime}">商品将会在 [[${#dates.format(new java.util.Date(item.seckillInfoVo.startTime),"yyyy-MM-dd HH:mm:ss")}]] 进行秒杀</span><span th:if="${#dates.createNow().getTime() >= item.seckillInfoVo.startTime && #dates.createNow().getTime() <= item.seckillInfoVo.endTime}">秒杀价: [[${#numbers.formatDecimal(item.seckillInfoVo.seckillPrice,1,2)}]]</span>
    </li>
    

    3.商品秒杀流程

    在这里插入图片描述
    在这里插入图片描述

    1.秒杀是独立的流程

    在这里插入图片描述

    // 不是秒杀的商品显示加入购物车$("#addCartItemA").click(function () {let skuId = $(this).attr("skuId");let num = $("#productNum").val();location.href = "http://cart.gulishop.cn/addCartItem?skuId=" + skuId + "&num=" + num;return false;});// 是秒杀的商品显示 立刻抢购$("#secKillA").click(function () {var isLogin = [[${session.loginUser != null}]]// 前端页面限流,登录之后才能点击if(isLogin){var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");var num = $("#numInput").val();location.href = "http://seckill.gulishop.cn/kill?killId=" + killId + "&key=" + $(this).attr("code") + "&num=" + num;}else{layer.msg("请先登录!")}return false;})

    秒杀服务生成秒杀订单信息

    @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);// 1.判断是否登录model.addAttribute("orderSn", orderSn);return "success";}
    

    登录拦截器实现限流

    /*** 秒杀流程* @param killId* @param key* @param num* @return*/@Overridepublic String kill(String killId, String key, Integer num) {MemberResponseTo memberRsepVo = LoginUserInterceptor.threadLocal.get();// 1.获取当前秒杀商品的详细信息BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);String json = hashOps.get(killId);if(StringUtils.isEmpty(json)){return null;}else{SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);// 校验合法性long time = new Date().getTime();if(time >= redisTo.getStartTime() && time <= redisTo.getEndTime()){// 1.校验随机码跟商品id是否匹配String randomCode = redisTo.getRandomCode();String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();if(randomCode.equals(key) && killId.equals(skuId)){// 2.说明数据合法BigDecimal limit = redisTo.getSeckillLimit();if(num <= limit.intValue()){// 3.验证这个人是否已经购买过了String redisKey = memberRsepVo.getId() + "-" + skuId;// 让数据自动过期long ttl = redisTo.getEndTime() - redisTo.getStartTime();Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl<0?0:ttl, TimeUnit.MILLISECONDS);if(aBoolean){// 占位成功 说明从来没买过RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);boolean acquire = semaphore.tryAcquire(num);if(acquire){// 秒杀成功// 快速下单 发送MQString orderSn = IdWorker.getTimeId() + UUID.randomUUID().toString().replace("-","").substring(7,8);SecKillOrderTo orderTo = new SecKillOrderTo();orderTo.setOrderSn(orderSn);orderTo.setMemberId(memberRsepVo.getId());orderTo.setNum(num);orderTo.setSkuId(redisTo.getSkuId());orderTo.setSeckillPrice(redisTo.getSeckillPrice());orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", orderTo);return orderSn;}}else {return null;}}}else{return null;}}else{return null;}}return null;}
    

    单独的秒杀订单信息放入RabbitMQ

    package henu.soft.common.to.mq;import lombok.Data;import java.math.BigDecimal;@Data
    public class SecKillOrderTo {/*** 订单号*/private String orderSn;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 购买数量*/private Integer num;/*** 会员ID*/private Long memberId;
    }
    2.订单服务创建秒杀信息的队列、绑定
      /*** 商品秒杀队列* @return*/@Beanpublic Queue orderSecKillOrrderQueue() {Queue queue = new Queue("order.seckill.order.queue", true, false, false);return queue;}@Beanpublic Binding orderSecKillOrrderQueueBinding() {//String destination, DestinationType destinationType, String exchange, String routingKey,// 			Map argumentsBinding binding = new Binding("order.seckill.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.seckill.order",null);return binding;}
    

    监听秒杀单信息队列

    package henu.soft.xiaosi.order.listener;import com.rabbitmq.client.Channel;import henu.soft.common.to.mq.SecKillOrderTo;
    import henu.soft.xiaosi.order.service.OrderService;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.annotation.RabbitHandler;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;import java.io.IOException;@Component
    @RabbitListener(queues = "order.seckill.order.queue")
    public class SeckillOrderListener {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic void createOrder(SecKillOrderTo orderTo, Message message, Channel channel) throws IOException {System.out.println("***********接收到秒杀消息");long deliveryTag = message.getMessageProperties().getDeliveryTag();try {orderService.createSeckillOrder(orderTo);channel.basicAck(deliveryTag, false);} catch (Exception e) {channel.basicReject(deliveryTag,true);}}
    }

    保存秒杀订单

    /*** 保存秒杀订单信息* @param*/@Overridepublic void createSeckillOrder(SecKillOrderTo secKillOrderTo) {log.info("\n创建秒杀订单");OrderEntity entity = new OrderEntity();entity.setOrderSn(secKillOrderTo.getOrderSn());entity.setMemberId(secKillOrderTo.getMemberId());entity.setCreateTime(new Date());entity.setPayAmount(secKillOrderTo.getSeckillPrice());entity.setTotalAmount(secKillOrderTo.getSeckillPrice());entity.setStatus(OrderStatusEnume.CREATE_NEW.getCode());entity.setPayType(1);// TODO 还有挺多的没设置BigDecimal price = secKillOrderTo.getSeckillPrice().multiply(new BigDecimal("" + secKillOrderTo.getNum()));entity.setPayAmount(price);this.save(entity);// 保存订单项信息OrderItemEntity itemEntity = new OrderItemEntity();itemEntity.setOrderSn(secKillOrderTo.getOrderSn());itemEntity.setRealAmount(price);itemEntity.setOrderId(entity.getId());itemEntity.setSkuQuantity(secKillOrderTo.getNum());R info = productFeignService.info(secKillOrderTo.getSkuId());SpuInfoTo spuInfo = info.getData(new TypeReference<SpuInfoTo>() {});itemEntity.setSpuId(spuInfo.getId());itemEntity.setSpuBrand(spuInfo.getBrandId().toString());itemEntity.setSpuName(spuInfo.getSpuName());itemEntity.setCategoryId(spuInfo.getCatalogId());itemEntity.setGiftGrowth(secKillOrderTo.getSeckillPrice().multiply(new BigDecimal(secKillOrderTo.getNum())).intValue());itemEntity.setGiftIntegration(secKillOrderTo.getSeckillPrice().multiply(new BigDecimal(secKillOrderTo.getNum())).intValue());itemEntity.setPromotionAmount(new BigDecimal("0.0"));itemEntity.setCouponAmount(new BigDecimal("0.0"));itemEntity.setIntegrationAmount(new BigDecimal("0.0"));orderItemService.save(itemEntity);}
    3.订单服务保存完成订单跳转到秒杀服务成功页面

    秒杀服务成功页面

    @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);// 1.判断是否登录model.addAttribute("orderSn", orderSn);return "success";}
    
    DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head><meta charset="utf-8"/><title>[[${orderSn != null?'秒杀成功':'秒杀失败'}]]title><script type="text/javascript" src="/static/seckill/js/jquery-3.1.1.min.js">script><script type="text/javascript" src="/static/seckill/bootstrap/js/bootstrap.js">script><script type="text/javascript" src="/static/seckill/js/swiper.min.js">script><script src="/static/seckill/js/swiper.min.js">script><link rel="stylesheet" type="text/css" href="/static/seckill/css/swiper.min.css"/><link rel="stylesheet" type="text/css" href="/static/seckill/bootstrap/css/bootstrap.css"/><link rel="stylesheet" type="text/css" href="/static/seckill/css/success.css"/>head><body>
    
    <div class="alert-info"><div class="hd_wrap_top"><ul class="hd_wrap_left"><li class="hd_home"><i class="glyphicon glyphicon-home">i><a href="http://gulishop.cn">谷粒商城首页a>li>ul><ul class="hd_wrap_right"><li class="forel" th:if="${session.loginUser} ==null"><a href="http://auth.gulishop.cn/login.html" class="link_login">你好,请登录a> <a href="http://auth.gulishop.cn/reg.html" class="link_regist">免费注册a>li><li class="forel" th:if="${session.loginUser} !=null"><a class="link_login" style="width: 100px">[[${session.loginUser.username}]]a> <a href="http://auth.gulishop.cn/oauth2.0/logout" class="link_regist">退出登录a>li><li class="spacer">li><li><a href="">我的订单a>li>ul>div>
    div><div class="nav-tabs-justified"><div class="nav_wrap"><div class="nav_top"><div class="nav_top_one"><a href="http://gulishop.cn"><img src="/static/seckill/img/logo1.jpg" style="height: 60px;width:180px;"/>a>div><div class="nav_top_two"><input type="text"/><button>搜索button>div>div>div>
    div><div class="main"><div class="success-wrap"><div class="w" id="result"><div class="m succeed-box"><div th:if="${orderSn != null}" class="mc success-cont"><div class="success-lcol"><div class="success-top"><b class="succ-icon">b><h3 class="ftx-02">商品秒杀成功 订单号: [[${orderSn}]]h3>div><div class="p-item"><div class="clr">div>div>div><div class="success-btns success-btns-new"><div class="success-ad"><a href="#none">a>div><div class="clr">div><div class="bg_shop"><a class="btn-addtocart" th:href="'http://order.gulishop.cn/payOrder?orderSn=' + ${orderSn}" id="GotoShoppingCart"><b>b>去支付a>div>div>div><div th:if="${orderSn == null}" class="mc success-cont"><div class="success-lcol"><div class="success-top"><b class="succ-icon">b><h3 class="ftx-02">秒杀失败h3>div><div class="p-item"><div class="p-img"><a href="" target="_blank">a>div><div class="p-info"><div class="p-name">div><div class="p-extra"><span class="txt" >span>div>div><div class="clr">div>div>div><div class="success-btns success-btns-new"><div class="success-ad"><a href="#none">a>div><div class="clr">div><div class="bg_shop"><a class="btn-addtocart" href="http://gulishop.cn" id="GotoShoppingCart"><b>b>去购物a>div>div>div>div>div>div>
    div>
    body>
    <script type="text/javascript" src="/static/seckill/js/success.js">script>
    html>

    之后就是调用一般的订单支付逻辑,待完善的部分

    • 开始秒杀的商品需要锁定数据库库存,秒杀结束剩余的redis里的商品再还原

    目前视频进度传送:P326高并发方法论

    二、高并发方法论

    1.SpringCloud Aliababa Sentinel

    1、熔断降级限流
    • 什么是熔断
      A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时
      间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是
      调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影
      响到 A。
    • 什么是降级
      整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和
      页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的
      的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。

    异同:

    • 相同点:
      1、为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我
      2、用户最终都是体验到某个功能不可用
    • 不同点:
      1、熔断是被调用方故障,触发的系统主动规则
      2、降级是基于全局考虑,停止一些正常服务,释放资源
    • 什么是限流
      对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力
    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 扩展接口。您可以通过
        实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

    在这里插入图片描述
    Sentinel 分为两个部分:

    • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时
      对 Dubbo / Spring Cloud 等框架也有较好的支持。
    • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的
      Tomcat 等应用容器。
    3、Sentinel 基本概念
    • 资源(需要受保护的资源、接口)
      资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提
      供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文
      档中,我们都会用资源来描述代码块。
      只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,
      可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
    • 规则(限流、熔断规则)
      围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规
      则。所有规则可以动态实时调整。
    4、使用
    • https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5
    • 什么是熔断降级
      除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关
      系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。

    在这里插入图片描述

    在这里插入图片描述

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

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

    1.熔断降级设计理念

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

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

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

      • 通过并发线程数进行限制
        和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其
        它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个
        资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步
        堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积
        的线程完成任务后才开始继续接收请求。
      • 通过响应时间对资源进行降级
        除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。
        当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的
        时间窗口之后才重新恢复。
    2.整合测试限流

    如何使用:

    • https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/se
      ntinel-example/sentinel-feign-example/readme-zh.md,- https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8

    我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。使用 Sentinel 来进行资源保护,主要分为几个步骤:

    • 定义资源:https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8
    • 定义规则
    • 检验规则是否生效

    在这里插入图片描述

    • 确定好资源就可以使用Sential的控制台配置规则,使用参考https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel

    • 导依赖,直接使用默认适配的方式确定资源(无序操作),或者使用注解等等方式

      
      <!--        Sentinel--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
    • 确定版本,下载Sential客户端1.8.0

    • 启动 java -jar sentinel-dashboard-1.8.0.jar
      在这里插入图片描述
      在这里插入图片描述

    • 配置各个微服务关联控制台的信息

      spring:cloud:sentinel:transport:port: 8719dashboard: localhost:8080
      
    • 访问微服务的接口,查看控制台,编辑规则,测试限流,控制台文档
      https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    3.各个微服务模块导入信息审计

    前面的测试存在问题

    • 在控制台调整限流的参数,都保存在内存中,重启失效
    • 为了保证能够持久的保存限流规则,需要导入信息审计模块
    
    <!--        审计--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
    

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

    4.配置自定义限流返回信息

    WebCallbackManager已经不能使用了

    package henu.soft.xiaosi.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 henu.soft.common.exception.BizCodeEnume;
    import henu.soft.common.utils.R;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;/*** 

    Title: SecKillSentinelConfig

    * Description:配置请求被限制以后的处理器* date:2020/7/10 13:47*/
    @Component public class SecKillSentinelConfig implements BlockExceptionHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {R error = R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());httpServletResponse.setCharacterEncoding("UTF-8");httpServletResponse.setContentType("application/json");httpServletResponse.getWriter().write(JSON.toJSONString(error));}}
    5.整合Feign测试熔断、降级

    远程调用熔断保护机制(远程服务不可达)

    • 调用方开启:

      # 开启远程调用熔断降级  
      feign:sentinel:enabled: true
      
    • 调用方设置失败回调
      在这里插入图片描述

      package henu.soft.xiaosi.product.feign.fallback;import henu.soft.common.exception.BizCodeEnume;
      import henu.soft.common.utils.R;
      import henu.soft.xiaosi.product.feign.SeckillFeignService;
      import org.springframework.stereotype.Component;/*** 

      Title: SecKillFeignServiceFalback

      * Description:* date:2020/7/10 16:03*/
      @Component public class SecKillFeignServiceFalback implements SeckillFeignService {@Overridepublic R getSkuSeckillInfo(Long skuId) {System.out.println("触发熔断");//return R.error();return R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());} }

    远程调用降级保护机制(远程服务负载过重,触发熔断)

    • 调用方开启:
      # 开启远程调用熔断降级  
      feign:sentinel:enabled: true
      
    • 控制台设置降级策略,超过阀值触发熔断方法
      在这里插入图片描述

    提供方也可以做做降级熔断

    • 定义好失败回调方法之后

    • 直接控制台设置即可

    6.自定义受保护的资源
    • 使用try
      在这里插入图片描述

    在这里插入图片描述

    • 使用@SentinelResource注解直接加到方法上value = "xxx",可选参数降级方法 blockHandler = "xxx"
    7.网关流控

    直接在网关层配置,对应请求直接阻断到微服务模块

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

    点开控制台的API管理

    在这里插入图片描述
    在这里插入图片描述
    设置路由

    在这里插入图片描述

    设置各种匹配参数,设置网关流控失败回调

    package henu.soft.xiaosi.gateway.config;import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
    import com.alibaba.fastjson.JSON;import henu.soft.common.exception.BizCodeEnume;
    import henu.soft.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.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());String errJson = JSON.toJSONString(error);Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);return body;});} }

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

    1、为什么用
    • 定位问题微服务,更好的熔断降级

    • 微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务
      单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要
      体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以
      定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,
      参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。

    • 链路追踪组件有 Google 的 Dapper,Twitter 的 Zipkin,以及阿里的 Eagleeye (鹰眼)等,它
      们都是非常优秀的链路追踪开源组件。

    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

    如果服务调用顺序如下

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    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 则会把信息输出到其他可视化的服务上观察
    4、整合 zipkin 可视化观察

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

    在这里插入图片描述

    • 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-servicezipkin:base-url: http://192.168.56.10:9411/ # zipkin 服务器的地址# 关闭服务发现,否则 Spring Cloud 会把 zipkin 的 url 当做服务名称discoveryClientEnabled: falsesender:type: web # 设置使用 http 的方式传输数据sleuth:sampler:probability: 1 # 设置抽样采集率为 100%,默认为 0.1,即 10%
      

    发送远程请求,测试 zipkin

    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

    分布式高级篇至此完结撒花~


    本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

    相关文章

    立即
    投稿

    微信公众账号

    微信扫一扫加关注

    返回
    顶部