目录
一.介绍:
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)