谷粒商城—商城业务—秒杀服务(311~324)

目录

一.介绍:

二.后台管理系统:

三.微服务—秒杀所有功能:

四.定时任务:

五.秒杀商品上架:

六.幂等性保证:

七.查询秒杀商品:

八.秒杀(高并发)系统关注的问题:

九.监听队列:

​​​​​​​

一.介绍:

           1)介绍
                  a:秒杀是每一个电商系统里,非常重要的模块,商家会不定期的发布一些低价商品,发布到秒杀系统里边。
                  b:特点就是,等到秒杀时间一到,瞬时流量特别大
                  c:关注:限流、异步、缓存、创建独立的微服务。
                  d:秒杀业务:


                  e:

           2)
           3)
           4)
           5)

二.后台管理系统:

           1)
                  a:查看秒杀场次信息:(增删改查)


                  b:
                  c:
                  d:
                  e:

           2)
           3)
           4)
           5)

三.微服务—秒杀所有功能:

           1)创建微服务:





           2)秒杀流程:
                  a:商品上架:


                  b:定时任务:(详见 四)
                  c:商品上架:(相见 五)
                  d:
                  e:

           3)
                  a:
                  b:
                  c:
                  d:
                  e:

           4)
                  a:
                  b:
                  c:
                  d:
                  e:

           5)



 

四.定时任务:

           1)定时任务介绍:
                  a:cron 表达式:
               
            -1.秒 分 时 日 月 周 年(spring 不支持,可省略)

                            -2.有 Cron 表达式,在线生成器:

                  b:Cron 表达式 示例:



                  c:Spring Boot 整合 定时任务:
               
            -1.语法:

/**
 * 1:定时任务
 *      1:@EnableScheduling
 *      2:@Scheduled(cron = "* /5 * * ? * 1")
 *
 * 2:异步任务:
 *      1:@EnableAsync
 *      2:@Async
 *      
 *  3:使用 定时任务 + 异步任务 ,保证定时任务不阻塞。
 */
@Slf4j
@Component
// 开启异步任务
//@EnableAsync
// 开启定时任务
//@EnableScheduling
public class HelloSchedule {

    /**
     * 在Spring中 只允许6位 [* * * ? * 1]  : 每周一 每秒执行一次
     * [* /5 * * ? * 1] : 每周一 每5秒执行一次
     * 1.定时任务不应阻塞 [默认是阻塞的]
     * 2.定时任务线程池 spring.task.scheduling.pool.size=5
     * 3.让定时任务异步执行
     *      1:SpringBoot + Schedule 线程池。(不太好使))
     *      2:启动异步任务:( CompletableFuture.runAsync(() -> {} )
     *      3:开启异步任务:@Async。
     */
    @Async
    @Scheduled(cron = "*/5 * * ? * 1")
    public void hello() {
        log.info("i love you...");
        CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName());
        });
    }
}

                            -2.Schedule 线程池:(SpringBoot 自带的,不太好使)

spring: 
  task:
    # Async
    execution:
      pool:
        core-size: 5
        max-size: 50
        
#   Schedule 线程池(不太好使)
#   scheduling:
#     pool:
#       size: 5


           2)分布式定时任务:





 

五.秒杀商品上架:

           1)上架最近三天 秒杀的商品:


                  a:定时任务:(防止重复调用,Redisson 分布式锁) 

/**
 * 1:秒杀商品定时上架:
 *      每天凌晨三点,上架最近三天,需要秒杀的商品。
 *      当天: 00:00:00 ~ 23:59:59
 *      明天: 00:00:00 ~ 23:59:59
 *      后天: 00:00:00 ~ 23:59:59
 *
 * [秒杀的定时任务调度]
 */
@Slf4j
@Service
public class SeckillSkuScheduled {

    @Autowired
    private SeckillService seckillService;

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

    }
}

                  b:Service:

@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

	@Autowired
	private CouponFeignService couponFeignService;

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@Autowired
	private ProductFeignService productFeignService;

	@Autowired
	private RedissonClient redissonClient;

	@Autowired
	private RabbitTemplate rabbitTemplate;

	private final String SESSION_CACHE_PREFIX = "seckill:sessions:";

	private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";

	// +商品随机码
	private final String SKUSTOCK_SEMAPHONE = "seckill:stock:";



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

	}

	/**
	 * 2.缓存活动信息
 	 */
	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){
					// 获取所有商品id
					List<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("skuInfo", 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());
					}
				});
			});
		}
	}
}

                  c:被调用:远程的 商品服务,查出来 近三天 秒杀的商品: 

@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {

    @Autowired
    private SeckillSessionService seckillSessionService;

    @GetMapping("/lates3DaySession")
    public R getLate3DaySession(){
		List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLate3DaySession();
		return R.ok().setData(seckillSessionEntities);
	}

}
@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {

	@Autowired
	private SeckillSkuRelationService skuRelationService;

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

}

                  d:
                  e:

           2)
                  a:
                  b:
                  c:
                  d:
                  e:

           3)
                  a:
                  b:
                  c:
                  d:
                  e:

           4)
                  a:
                  b:
                  c:
                  d:
                  e:

           5)
                  a:
                  b:
                  c:
                  d:
                  e:

六.幂等性保证:

           1)
                  a:
                  b:
                  c:
                  d:
                  e:

           2)
           3)
           4)
           5)

七.查询秒杀商品:

           1)接口:

	@ResponseBody
	@GetMapping("/currentSeckillSkus")
	public R getCurrentSeckillSkus(){
		List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
		return R.ok().setData(vos);
	}
public interface SeckillService {

	/**
	 * 获取当前可以参与秒杀的商品信息
	 */
	List<SeckillSkuRedisTo> getCurrentSeckillSkus();

}
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {

        // 1.确定当前时间属于那个秒杀场次
        long time = new Date().getTime();

        // 定义一段受保护的资源
        try (Entry entry = SphU.entry("seckillSkus")) {
            Set<String> keys = stringRedisTemplate.keys(SESSION_CACHE_PREFIX + "*");
            for (String key : keys) {
                // seckill:sessions:1593993600000_1593995400000
                String 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;
                }
            }
        } catch (BlockException e) {
            log.warn("资源被限流:" + e.getMessage());
        }
        return null;
    }


​​​​​​​           2)
           3)
           4)
           5)





 

八.秒杀(高并发)系统关注的问题:

           1)关注点问题:




           2)秒杀步骤:


 

           3)登陆检查:

                  a:配置 Spring Session:

 

/**
 * <p>Title: GlMallSessionConfig</p>
 * Description:设置Session作用域、自定义cookie序列化机制
 * date:2020/7/9 15:56
 */
@Configuration
public class GlMallSessionConfig {

	@Bean
	public CookieSerializer cookieSerializer(){
		DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
		// 明确的指定Cookie的作用域
		cookieSerializer.setDomainName("glmall.com");
		cookieSerializer.setCookieName("FIRESESSION");
		return cookieSerializer;
	}

	/**
	 * 自定义序列化机制
	 * 这里方法名必须是:springSessionDefaultRedisSerializer
	 */
	@Bean
	public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
		return new GenericJackson2JsonRedisSerializer();
	}
}

                  b:拦截器:

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

	public static ThreadLocal<MemberRsepVo> threadLocal = new ThreadLocal<>();

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

		String uri = request.getRequestURI();

		// 秒杀请求,要先进行登陆
		boolean match = new AntPathMatcher().match("/kill", uri);
		if(match){
			HttpSession session = request.getSession();
			MemberRsepVo memberRsepVo = (MemberRsepVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
			if(memberRsepVo != null){
				threadLocal.set(memberRsepVo);
				return true;
			}else{
				// 没登陆就去登录
				session.setAttribute("msg", AuthServerConstant.NOT_LOGIN);
				response.sendRedirect("http://auth.glmall.com/login.html");
				return false;
			}
		}
		return true;
	}
}

                  c:Controller、Service:

	@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";
	}
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String SESSION_CACHE_PREFIX = "seckill:sessions:";

    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";

    // +商品随机码
    private final String SKUSTOCK_SEMAPHONE = "seckill:stock:";


    @Override
    public String kill(String killId, String key, Integer num) {

        MemberRsepVo 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) {
                                // 秒杀成功
                                // 快速下单 发送MQ
                                String 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;
    }


}

                  d:SeckillWebConfig:配置拦截器

@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {

	@Autowired
	private LoginUserInterceptor loginUserInterceptor;

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
	}
}

                  e:
           4)
           5)
 

 

 

 

 

 

 

九.监听队列:

           1)
                  a:
                  b:
                  c:
                  d:
                  e:

           2)
           3)
           4)
           5)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值