Description or Example
# 知识点
## 秒杀八大问题


### 1. 为什么秒杀需要独立部署
> 独立部署: 所有秒杀的业务独立到一个微服务中
> 独立部署的原因是, 如果秒杀中有其他功能, 按照秒杀的特性, 通常并发量非常的高, 服务器崩溃的概率极高, 如果耦合了其他服务, 那么其他服务也会因秒杀而不可用, 因此, 秒杀需要独立部署
### 2. 秒杀的链接加密
> 链接加密: 当前项目采取的策略是随机码监测, 参数必须有随机码才能参与秒杀
> 链接加密的目的主要是为了防止内部人员或恶意分子获取到秒杀请求后, 提前秒杀, 我们必须获取到随机码后, 才能参与秒杀
#### 额外知识
> 随机码只能防止内部人员提前秒杀, 并不能防止恶意刷请求, 恶意刷请求利用验证登录状态即可
### 3. 库存预热+快速扣减
> 库存预热+快速扣减: 可以将秒杀商品上传到Redis中, 直接在Redis扣减信号量即可
> ~~我们在上架的时候可以预先将秒杀的库存给锁定了, 当秒杀开始的时候, 只需要访问Redis而不需要关注数据库, 数据库的压力就会小很多, Redis读写能力都很强, 整体的吞吐量和稳定性更好~~
### 4. 动静分离
> 动静分离: 动态资源和静态资源分离, 即将CSS, JS等静态资源存储到NGINX
> 目的是为了让后端的并发请求数更少, 吞吐量更大(少了大量访问静态资源的二级请求)
### 5. 恶意请求拦截
> 恶意请求拦截在本项目中没有使用, 其实, 用登录拦截就可以拦截到大多数恶意请求, 如果我们设置了商品秒杀量为1, 再怎么恶意都没用
### 6. 流量错峰
> 流量错峰, 就是想让同一个时刻的秒杀请求分摊到一个时间段内
#### 常规做法
1. 在秒杀的时候设置验证码(每个人的手速都不同, 很好的将同一个时刻的秒杀请求分摊到一个时间段内)
2. 使用购物车逻辑, 即普通的购物逻辑, 这样的话, 流程更多, 每个人的网速都不同, 很好的将秒杀分摊到一个时间段内
(**不要使用购物车的逻辑, 这样会使购物车等其他微服务的压力变大, 耦合其他微服务, 系统变得脆弱, 和独立部署的理念相悖**)
### 7. 限流, 熔断降级
> 熔断的目的是, 防止微服务多次调用其他微服务, 如果多次调用, 整体的执行时间变长, 吞吐量变低, 但是秒杀要求的吞吐量很高, 这就会引起服务的崩溃, 因此需要熔断
> 降级的目的是, 把某些冷门服务下线, 把资源转让给秒杀服务, 让其具有更高的吞吐能力
> 限流, 为了避免秒杀的无限度高并发, 限制并发程度, 保护应用
### 8. 队列削峰
> 队列削峰, 就是将秒杀的商品处理放在队列中
> 这样做的目的是为了避免秒杀服务直接耦合调用其他微服务, 如果直接调用其他微服务, 不仅秒杀面临着高并发的风险, 其他微服务也会被波及, 避免波及, 可以放在队列中, 让别人慢慢执行
> 信号量是解决并发问题的大招,因为并发量完全由信号量决定
### 扩展
#### CDN有什么用?
> CDN可以存储我们的静态资源, 然后将静态资源部署到不同的地方, 当用户访问的时候, 自动访问距离最近的CDN获取静态资源, 这样可以减少延迟, 系统的整体吞吐量更高(相较于动静分离而言)
## 秒杀的方案
### 方案1: 耦合之前的逻辑
> 即如果我们点击秒杀, 会走之前的方案, 先加入购物车...., 最后付款
#### 优点
1. 该方案直接耦合了之前的方案, 不需要额外的编码, 做一下适配即可, 非常的方便
2. 该方案还可以流量错峰, 增加系统的健壮性
#### 缺点
1. 会直接耦合其他微服务, 给其他微服务带来秒杀的压力, 其他微服务可能因此而崩溃
### 方案2

#### 设计思路
> <font color="red">**整体的设计思路是, 秒杀服务只负责判断当前用户秒杀是否合法, 至于订单和库存相关的操作, 秒杀不做, 交予消息队列慢慢处理, 使吞吐量最大化**</font>
#### 优点
1. 与其他微服务解耦, 只会影响自己
2. 能通过队列流量削峰
#### 缺点
1. 如果并发量很大, 会导致消息的积压, 导致用户成功秒杀后很久都无法付款
### 如何避免重复秒杀
> **我们可以在Redis中setnx, 如果不存在才设置, 如果设置成功, 则说明没秒杀过, 否则说明秒杀过了**
> 通过这样来彻底解决重复秒杀的问题
> 注意: 一定要有场次的ID, 否则场次A秒杀了商品A, 场次B就秒杀不了商品B了
### 为什么需要判断随机码
> 因为有可能用了其他商品的随机码来, 这样会导致把别人的信号量扣减了, 非常的不好
### 为什么不需要判断商品ID?
> 如果商品ID是错的, 那么有两种情况, 找不出来, 找不出来我这里直接返回了false, 直接判断失败, 如果找出来了, 随机码也不可能匹配的上, 也返回false, 也判断失败, 因此, 不需要判断商品ID
## 为什么设置用户秒杀的过期时间
> 其实这里不设置也是可以的, 只是浪费磁盘空间而言
## 消息队列为什么可以削峰?
> 因为放到消息队列后, 处理业务可以慢慢处理, 没必要瞬间完成, 因此可以削峰
## 同源策略问题!
> 这个成功页的JS用的是cart微服务里面的, 因为动静分离了, 所以, 我们需要在NGINX配置跨域, 否则访问不到对应的静态资源

### 多说一嘴
> 这里整体的逻辑是, 如果秒杀成功, 跳转秒杀成功页, 等待10s自动跳转支付页面, 失败的话有对应的提示信息
# 核心代码
```java
/**
* 秒杀
* @param randomCode 随机码
* @param killId 秒杀的商品Id
* @param num 秒杀个数
* @return
*/
@GetMapping("/kill")
public ModelAndView secKill(@RequestParam("key") String randomCode,
@RequestParam String killId,
@RequestParam String num,
ModelAndView mv) {
mv.setViewName("success");
try {
// 0. 获取用户信息
MemberVO memberVO = UserLoginInterceptor.USER_STATE.get();
Long memberId = memberVO.getId();
// 1. 校验秒杀的合法性
SeckillSkuTO seckillSkuTO = secKillService.judgeIfLegal(randomCode, killId, num, memberId);
// 2. 秒杀的逻辑
String orderSn = secKillService.kill(num, seckillSkuTO, memberId);
mv.addObject("seckillSkuTO", seckillSkuTO);
mv.addObject("orderSn", orderSn);
long endTimes = System.currentTimeMillis();
return mv;
} catch (Exception e) {
mv.addObject("msg", e.getMessage());
return mv;
}
}
```
```java
@Override
public SeckillSkuTO judgeIfLegal(String randomCode, String killId, String num, Long memberId) {
// 获取hash操作
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SecKillConstant.SEC_KILL_SKU_MAP_SKU_INFO);
// 获取秒杀商品的详情信息
String secKillSKuTOJson = operations.get(killId);
SeckillSkuTO seckillSkuTO = JSON.parseObject(secKillSKuTOJson, SeckillSkuTO.class);
// 秒杀商品存在时
if (seckillSkuTO != null) {
Long startTime = seckillSkuTO.getStartTime();
Long endTime = seckillSkuTO.getEndTime();
long nowTIme = new Date().getTime();
// 1. 判断是否在合理的时间内秒杀
if (nowTIme < startTime || nowTIme > endTime) { // 时间范围不合理
throw new RuntimeException("时间范围不合理");
}
// 2. 判断随机码是否正确
if (!randomCode.equals(seckillSkuTO.getRandomToken())) { // 随机码不正确
throw new RuntimeException("随机码不正确");
}
// 3. 判断是否超过秒杀数
if (Integer.parseInt(num) > seckillSkuTO.getSeckillCount().intValue()) {
throw new RuntimeException("超买");
}
// 4. 判断是否重复秒杀
if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(memberId + "_" + killId, "", Duration.ofMillis(endTime - startTime)))) {
// 如果设置失败, 已经秒杀过了, 返回false
throw new RuntimeException("重复秒杀");
}
// 5. 所有都没问题, 尝试获取秒杀的信号量
RSemaphore semaphore = redissonClient.getSemaphore(SecKillConstant.SEC_KILL_RANDOM_CODE_SEMAPHORE + randomCode);
// 尝试获取这么多的信号量(秒杀数), 获取成功了, 真的可以秒杀了, 否则还是秒杀不了
if (semaphore.tryAcquire(Integer.parseInt(num))) {
return seckillSkuTO;
}
}
// 默认都判断失败
throw new RuntimeException("商品被秒杀完或你恶意攻击");
}
@Override
public String kill(String num, SeckillSkuTO seckillSkuTO, Long memberId) {
// 开始秒杀
// 1. 封装信息
SecKillOrderTO secKillOrderTO = new SecKillOrderTO();
String orderSn = UUID.randomUUID().toString().replace("-", "");
secKillOrderTO.setOrderSn(orderSn) // 设置订单号
.setSecKillCount(Integer.parseInt(num)) // 设置秒杀数
.setSecKillPrice(seckillSkuTO.getSeckillPrice()) // 设置秒杀价格
.setMemberId(memberId)
.setPromotionSessionId(seckillSkuTO.getPromotionSessionId()) // 设置场次Id
.setSkuId(seckillSkuTO.getSkuId()) // 设置商品Id
.setRandomToken(seckillSkuTO.getRandomToken()); // 设置随机码
// 2. 发送消息
R info = mqService.sendMessageWithSecKill(secKillOrderTO);
if (info.getCode() != 0) {
// 有问题, 被熔断了, 需要返回一个错误
throw new RuntimeException(info.get("msg").toString());
}
return orderSn;
}
```
```java
@FeignClient("bitmall-mq")
public interface MQService {
@PostMapping("/mq/seckill/send")
R sendMessageWithSecKill(@RequestBody SecKillOrderTO secKillOrderTO);
}
```
```java
@PostMapping("/seckill/send")
public R sendMessageWithSecKill(@RequestBody SecKillOrderTO secKillOrderTO) {
try {
orderSender.sendSeckillOrder(secKillOrderTO);
} catch (Exception e) {
log.error("发送消息失败");
}
return R.ok();
}
```