商城:秒杀服务
秒杀业务
秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)+ 独立部署。
限流方式:
1. 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
2. nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
3. 网关限流,限流的过滤器
4. 代码中使用分布式信号量
5. rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。
定时任务
cron表达式在线生成:https://cron.qqe2.com/
SpringBoot整合定时任务与异步任务
/**
* 定时任务
* 1、@EnableScheduling 开启定时任务
* 2、@Scheduled开启一个定时任务
*
* 异步任务
* 1、@EnableAsync:开启异步任务
* 2、@Async:给希望异步执行的方法标注
*/
/**
* @author yaoxinjia
*/
@Slf4j
@Component
// @EnableAsync
// @EnableScheduling
public class HelloScheduled {
/**
* 1、在Spring中表达式是6位组成,不允许第七位的年份
* 2、在周几的的位置,1-7代表周一到周日
* 3、定时任务不该阻塞。默认是阻塞的
* 1)、可以让业务以异步的方式,自己提交到线程池
* CompletableFuture.runAsync(() -> {
* },execute);
*
* 2)、支持定时任务线程池;设置 TaskSchedulingProperties
* spring.task.scheduling.pool.size: 5
*
* 3)、让定时任务异步执行
* 异步任务 标注@Async
* spring.task.execution.pool.size: 5
* spring.task.execution.pool.size: 50
*
* 解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
*
*/
@Async
@Scheduled(cron = "*/5 * * ? * 4")
public void hello() {
log.info("hello...");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
时间日期处理
@Override
public List<SeckillSessionEntity> getLate3DaySession() {
//计算最近三天
List<SeckillSessionEntity> start_time = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
return start_time;
}
//开始的时间 如:2022-10-19 00:00:00
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;
}
//结束的时间 如:2022-10-22 23:59:59
private String endTime(){
LocalDate now = LocalDate.now();
LocalDate localDate = now.plusDays(2);
LocalDateTime of = LocalDateTime.of(localDate,LocalTime.MAX);
String format = of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
秒杀商品上架
随机码:防止用户直接访问接口完成抢购
库存作为分布式的信号量 限流:
- 计数信号量是一种锁,他可以让用户限制一项资源最多能够同时被多少个进程访问
计数信号量和其他锁的区别:
- [客户端获取锁失败时],客户端会选择等待
- [获取信号量失败时]通常直接退出,并向用户提示"资源繁忙",由用户决定下一步如何处理
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 uploadSeckillSkuLatest3Days() {
// 1.扫描最近三天要参加秒杀的商品
R r = couponFeignService.getLate3DaySession();
if(r.getCodr() == 0){
List<SeckillSessionWithSkusVo> sessions = r.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {});
// 2.缓存活动信息
saveSeesionInfos(sessions);
// 3.缓存活动的关联的商品信息
saveSessionSkuInfos(sessions);
}
}
//缓存活动信息
public void saveSeesionInfos(List<SeckillSessionWithSkusVo> sessions){
sessions.stream().forEach(session -> {
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
//缓存活动信息
redisTemplate.opsForList().leftPushAll(key,collect);
});
}
//缓存活动的关联商品信息
public void saveSessionSkuInfos(List<SeckillSessionWithSkusVo> sessions){
sessions.stream().forEach(session->{
//准备 hash操作
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//缓存商品
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
//1.sku的基本数据
R r = productFeignService.skuInfo(seckillSkuVo.getSkuId());
if(r.getCodr()==0){
SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
redisTo.setSkuInfo(skuInfo);
}
//2.sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo,redisTo);
//3.设置上当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
//4.随机码 seckill?skuId=1&key=adasdcsd
String token = UUID.randomUUID().toString().replace("-", "");
redisTo.setRandomCode(token);
//使用库存作为分布式的信号量 限流
RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + token);
//商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getSkuId().toString(),jsonString);
});
});
}
幂等性保护
方式一个秒杀场次的商品多次上架到Redis中,加锁处理
com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl
查询秒杀商品
前端Ajax请求http://seckill.mall.com/currentSeckillSkus 查询出所有参与秒杀的商品
com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl#getCurrentSeckillSkus
前端 详细页传入商品ID 查询出是否参与秒杀 与 其相关信息
com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl#getSkuSeckillInfo
秒杀系统设计
秒杀(高并发)关注的问题
- 服务独立部署
- 秒杀链接加密
- 库存预热 快速扣减
- 动静分类
- 恶意请求
- 流量错峰
- 限流&熔断&降级
- 队列削峰
登录检查
前端js判断 商品是否在秒杀的商品中,在显示"立即抢购",不在显示"立即购买"
js实现点击立即抢购判断是否为登录用户,跳转:http://seckill.mall.com/kill?killId= &key= &num= ;
秒杀流程
主要步骤:去Redis中获取信号量,获取成功就生成订单信息
代码:com.tinstu.gulimall.seckill.service.Impl.SeckillServiceImpl#kill
秒杀效果
方法的实现
@Override
public String kill(String killId, String key, Integer num) {
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
//获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String json = hashOps.get(killId);
if(StringUtils.isEmpty(json)){
return null;
}else {
SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
//校验合法性
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long time = new Date().getTime();
long l = endTime - time;
//校验时间的合法性
if(time>=startTime && time<=endTime){
//校验随机码 和 商品id
String randomCode = redisTo.getRandomCode();
String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
if(randomCode.equals(key)&&killId.equals(skuId)){
//3.验证购物数量是否合理
if(num<=redisTo.getSeckillLimit()){
//4.验证这个人是否已经买过,幂等性 如果秒杀成功就去站位
//SETNX
String s = respVo.getId() + "_" + skuId;
//自动过期
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(s, num.toString(), l, TimeUnit.MILLISECONDS);
if(aBoolean){
//占位成功说明从来没有买过
RSemaphore semaphore = redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE);
semaphore.tryAcquire(num);
//秒杀成功
// 快速下单 发送MQ消息
String orderSn = IdWorker.getTimeId();
SecKillOrderTo orderTo = new SecKillOrderTo();
orderTo.setOrderSn(orderSn);
orderTo.setMemberId(respVo.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;
}
}
return null;
}
前端页面请求
http://seckill.mall.com/kill?killId=&key=&num=
还要添加一个拦截器 /kill 是否登录,登录了就跳转success页面