目录
一、秒杀业务的介绍
秒杀业务:秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步+ 缓存 (页面静态化)+ 独立部署。
限流方式:
- 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
- nginx 限流,直接负载部分请求到错误的静态页面:令牌算法、漏斗算法
- 网关限流,限流的过滤器
- 代码中使用分布式信号量
- rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能
秒杀设计
秒杀流程
二、搭建秒杀服务环境
1、秒杀服务后台管理系统
1.网关配置
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
2.新增场次,关联商品
SeckillSkuRelationServiceImpl.java
package com.atguigu.gulimall.coupon.service.impl;
@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
String promotionSessionId = (String) params.get("promotionSessionId");
// 场次id不是null
if (StringUtils.isEmpty(promotionSessionId)) {
queryWrapper.eq("promotion_session_id",promotionSessionId);
}
IPage<SeckillSkuRelationEntity> page = this.page(
new Query<SeckillSkuRelationEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
2、搭建秒杀服务环境
1.创建微服务模块
2.导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</exclusion>
</exclusions>
</dependency>
3.添加配置
application.properties
spring.application.name=gulimall-seckill
server.port=20000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.88.130
4.主启动类添加注解
package com.atguigu.gulimall.seckill;
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
二、定时任务
1、cron 表达式
1.cron表达式语法
秒 分 时 日 月 周 年(Spring不支持)
2.cron 表达式特殊字符
,:枚举;
(cron="7,9,23****?"):任意时刻的7,9,23秒启动这个任务;
-:范围:
(cron="7-20****?""):任意时刻的7-20秒之间,每秒启动一次
*:任意;
指定位置的任意时刻都可以
/:步长;
(cron="7/5****?"):第7秒启动,每5秒一次;
(cron="*/5****?"):任意秒启动,每5秒一次;
? :(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使用?
(cron="***1*?"):每月的1号,而且必须是周二然后启动这个任务;
L:(出现在日和周的位置)”,
last:最后一个
(cron="***?*3L"):每月的最后一个周二
W:Work Day:工作日
(cron="***W*?"):每个月的工作日触发
(cron="***LW*?"):每个月的最后一个工作日触发
#:第几个
(cron="***?*5#2"):每个月的 第2个周4
3.cron表达式案例
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0 12 ? * WED 表示每个星期三中午12点
0 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 0 23 L * ? 每月最后一天23点执行一次
0 15 10 L * ? 每月最后一日的上午10:15触发
0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
0 15 10 * * ? 2005 2005年的每天上午10:15触发
0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
"30 * * * * ?" 每半分钟触发任务
"30 10 * * * ?" 每小时的10分30秒触发任务
"30 10 1 * * ?" 每天1点10分30秒触发任务
"30 10 1 20 * ?" 每月20号1点10分30秒触发任务
"30 10 1 20 10 ? *" 每年10月20号1点10分30秒触发任务
"30 10 1 20 10 ? 2011" 2011年10月20号1点10分30秒触发任务
"30 10 1 ? 10 * 2011" 2011年10月每天1点10分30秒触发任务
"30 10 1 ? 10 SUN 2011" 2011年10月每周日1点10分30秒触发任务
"15,30,45 * * * * ?" 每15秒,30秒,45秒时触发任务
"15-45 * * * * ?" 15到45秒内,每秒都触发任务
"15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?" 每小时的第0分0秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI" 星期一到星期五的10点15分0秒触发任务
"0 15 10 L * ?" 每个月最后一天的10点15分0秒触发任务
"0 15 10 LW * ?" 每个月最后一个工作日的10点15分0秒触发任务
"0 15 10 ? * 5L" 每个月最后一个星期四的10点15分0秒触发任务
"0 15 10 ? * 5#3" 每个月第三周的星期四的10点15分0秒触发任务
2、测试
问题:定时任务默认是阻塞的。如何让它不阻塞?
解决:使用异步+定时任务来完成定时任务不阻塞的功能
定时任务:
- @EnableScheduling 开启定时任务
- @Scheduled 开启一个定时任务
- 自动配置类 TaskSchedulingAutoConfiguration
异步任务:
- @EnableAsync 开启异步任务功能
- @Async :给我希望异步执行的方法上标注
- 自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
package com.atguigu.gulimall.seckill.scheduled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* Description: 定时调度测试
* 定时任务:
* 1、@EnableScheduling 开启定时任务
* 2、@Scheduled 开启一个定时任务
* 3、自动配置类 TaskSchedulingAutoConfiguration
* 异步任务:
* 1、@EnableAsync 开启异步任务功能
* 2、@Async :给我希望异步执行的方法上标注
* 3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
*/
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
* 1、spring中corn 表达式由6为组成,不允许第7位的年 Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
* 2、在周几的位置,1-7分别代表:周一到周日(MON-SUN)
* 3、定时任务默认是阻塞的。如何让它不阻塞?
* 1)、可以让业务运行以异步的方式,自己提交到线程池
* 2)、Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
* spring.task.scheduling.pool.size=5
* 3)、让定时任务异步执行
* 异步任务
* 解决:使用异步+定时任务来完成定时任务不阻塞的功能
*/
@Async
@Scheduled(cron = "* * * * * 6")
public void hello() throws InterruptedException {
log.info("hello.....");
Thread.sleep(3000);
}
}
配置异步任务线程池:
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=50
定时任务开启后其实也是有线程池的,通过更改配置修改线程池大小,这样也可以解决阻塞问题
#默认为1,就会阻塞
spring.task.scheduling.pool.size: 2
三、商品上架
1、远程查询秒杀的活动以及关联的商品信息
远程查询最近 3 天内秒杀的活动 以及 秒杀活动的关联的商品信息
1.秒杀服务中编写优惠服务的远程调用接口
CouponFeignService.java
package com.atguigu.gulimall.seckill.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Description: 远程调用优惠服务接口
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@GetMapping("/coupon/seckillsession/lates3DaySession")
R getLates3DaySession();
}
2.秒杀服务中编写优惠服务获取的数据的Vo
SeckillSessionsWithSkus.java
package com.atguigu.gulimall.seckill.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class SeckillSessionsWithSkus {
/**
* id
*/
private Long id;
/**
* 场次名称
*/
private String name;
/**
* 每日开始时间
*/
private Date startTime;
/**
* 每日结束时间
*/
private Date endTime;
/**
* 启用状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
private List<SeckillSkuVo> relationSkus;
}
SeckillSkuVo.java
package com.atguigu.gulimall.seckill.vo;
import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;
@Data
public class SeckillSkuVo {
/**
* id
*/
private Long id;
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private BigDecimal seckillCount;
/**
* 每人限购数量
*/
private BigDecimal seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
}
优惠服务编写扫描数据库最近3天需要上架的秒杀活动以及秒杀活动需要的商品
1.Controller 层接口编写
package com.atguigu.gulimall.coupon.controller;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.atguigu.gulimall.coupon.entity.SeckillSessionEntity;
import com.atguigu.gulimall.coupon.service.SeckillSessionService;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.R;
/**
* 秒杀活动场次
*
*/
@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {
@Autowired
private SeckillSessionService seckillSessionService;
/**
* 查询三天内需要上架的服务
* @return
*/
@GetMapping("/lates3DaySession")
public R getLates3DaySession(){
List<SeckillSessionEntity> sessions = seckillSessionService.getLates3DaySession();
return R.ok().setData(sessions);
}
2.Service 层实现类编写
package com.atguigu.gulimall.coupon.service.impl;
@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {
@Autowired
SeckillSkuRelationService seckillSkuRelationService;
@Override
public List<SeckillSessionEntity> getLates3DaySession() {
// 计算最近3天
List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
if (list!=null && list.size()>0) {
List<SeckillSessionEntity> collect = list.stream().map(session -> {
Long id = session.getId();
List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
session.setRelationSkus(relationEntities);
return session;
}).collect(Collectors.toList());
return collect;
}
return null;
}
2、在Redis中保存秒杀场次信息
SeckillServiceImpl.java
package com.atguigu.gulimall.seckill.service.impl;
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
StringRedisTemplate redisTemplate;
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
/**
* 缓存活动信息
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
sessions.stream().forEach(session ->{
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
System.out.println(key);
List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
// 缓存活动信息
redisTemplate.opsForList().leftPushAll(key,collect);
});
}
3、在Redis中保存秒杀活动关联的商品信息
saveSessionSkuInfo.java
/**
* 缓存活动的关联商品信息
* @param sessions
*/
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> 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 skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (skuInfo.getCode() == 0) {
SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
redisTo.setSkuInfo(info);
}
// 2、Sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo, redisTo);
// 3、设置上当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
// 4、商品的随机码
String token = UUID.randomUUID().toString().replace("_", "");
redisTo.setRandomCode(token);
// 5、引入分布式的信号量 限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getSkuId().toString(),jsonString);
});
});
}
封装秒杀商品的详细信息To
SecKillSkuRedisTo.java
package com.atguigu.gulimall.seckill.to;
import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;
import java.math.BigDecimal;
/**
* Description: 秒杀商品的详细信息
*/
@Data
public class SecKillSkuRedisTo {
/**
* 活动id
*/
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 skuInfo;
/**
* 当前商品秒杀活动的开始时间
*/
private Long startTime;
/**
* 当前商品秒杀活动的结束时间
*/
private Long endTime;
}
SkuInfoVo.java
package com.atguigu.gulimall.seckill.vo;
@Data
public class SkuInfoVo {
/**
* skuId
*/
private Long skuId;
/**
* spuId
*/
private Long spuId;
/**
* sku名称
*/
private String skuName;
/**
* sku介绍描述
*/
private String skuDesc;
/**
* 所属分类id
*/
private Long catalogId;
/**
* 品牌id
*/
private Long brandId;
/**
* 默认图片
*/
private String skuDefaultImg;
/**
* 标题
*/
private String skuTitle;
/**
* 副标题
*/
private String skuSubtitle;
/**
* 价格
*/
private BigDecimal price;
/**
* 销量
*/
private Long saleCount;
}
编写远程查询Sku基本信息的接口
1.在秒杀服务中编写远程调用产品服务中的 查询sku基本信息的方法
package com.atguigu.gulimall.seckill.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);
}
4、幂等性保证
加上分布式锁
- 保证在分布式的情况下,锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态
代码逻辑编写
- 当查询Redis中已经上架的秒杀场次和秒杀关联的商品,则不进行上架
加锁
package com.atguigu.gulimall.seckill.scheduled;
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
// TODO 幂等性处理
@Scheduled(cron = "* * 3 * * ?")
public void uploadSeckillSkuLatest3Days() {
// 1、重复上架无需处理
log.info("上架秒杀商品的信息");
// 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try {
seckillService.uploadSeckillSkuLatest3Days();
} finally {
lock.unlock();
}
}
}
判断Redis中是否已上架
package com.atguigu.gulimall.seckill.service.impl;
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
ProductFeignService productFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
RedissonClient redissonClient;
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // + 商品随机码
/**
* 远程查询最近 3 天内秒杀的活动 以及 秒杀活动的关联的商品信息
*/
@Override
public void uploadSeckillSkuLatest3Days() {
// 1、扫描最近三天数据库需要参与秒杀的活动
R session = couponFeignService.getLates3DaySession();
if (session.getCode() == 0) {
// 上架商品
List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
});
// 缓存到Redis
// 1)、缓存活动信息
saveSessionInfos(sessionData);
// 2)、缓存活动的关联商品信息
saveSessionSkuInfo(sessionData);
}
}
/**
* 缓存活动信息
*
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey) {
// 缓存活动信息
List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId().toString()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key, collect);
}
});
}
/**
* 缓存活动的关联商品信息
*
* @param sessions
*/
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions) {
sessions.stream().forEach(session -> {
// 准备Hash操作
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
// 生成随机码
String token = UUID.randomUUID().toString().replace("_", "");
// 1)、缓存商品
if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString())) {
SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
// 1、Sku的基本数据
R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (skuInfo.getCode() == 0) {
SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
redisTo.setSkuInfo(info);
}
// 2、Sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo, redisTo);
// 3、设置上当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
// 4、商品的随机码
redisTo.setRandomCode(token);
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(), jsonString);
// 如果当前这个场次的商品的库存信息已经上架就不需要上架
// 5、引入分布式的信号量 限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
}
});
});
}
}
四、获取当前的秒杀商品并展示
1、获取当前的秒杀商品
1.Controller层接口
package com.atguigu.gulimall.seckill.controller;
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 返回当前时间可以参与秒杀的商品信息
* @return
*/
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
List<SecKillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
}
2.Service 层实现类方法编写
getCurrentSeckillSkus.java
/**
* 获取当前参与秒杀的商品
* @return
*/
@Override
public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
// 1、确定当前时间属于哪个秒杀场次
long time = new Date().getTime();
Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
for (String key : keys) {
// seckill:sessions:1650153600000_1650160800000
String replace = key.replace(SESSION_CACHE_PREFIX, "");
String[] s = replace.split("_");
long start = Long.parseLong(s[0]);
long end = Long.parseLong(s[1]);
if (time>= start && time<=end) {
// 2、获取指定秒杀场次需要的所有商品信息
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<String> list = hashOps.multiGet(range);
if (list!=null) {
List<SecKillSkuRedisTo> collect = list.stream().map(item -> {
SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class);
redis.setRandomCode(null); // 当前秒杀开始了需要随机码
return redis;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
return null;
}
2、首页获取并拼装数据
1、环境配置
1.配置网关
- id: gulimall_seckill_route
uri: lb://gulimall-seckill
predicates:
- Host=seckill.gulimall.cn
2.配置域名 vim /etc/hosts
127.0.0.1 seckill.gulimall.cn
2、页面修改
修改 gulimall-product 服务的 index.html
<div class="section_second_list">
<div class="swiper-container swiper_section_second_list_left">
<div class="swiper-wrapper">
<div class="swiper-slide">
<ul id="seckillSkuContent"></ul>
function to_href(skuId) {
location.href = "http://item.gulimall.cn/"+skuId+".html";
}
$.get("http://seckill.gulimall.cn/currentSeckillSkus",function (resp) {
if (resp.data.length > 0) {
resp.data.forEach(function (item) {
$("<li οnclick='to_href("+ item.skuId +")'></li>")
.append($("<img style='width: 130px; height: 130px;' src='"+ item.skuInfo.skuDefaultImg+"'/>"))
.append($("<p>"+ item.skuInfo.skuTitle +"</p>"))
.append($("<span>"+ item.seckillPrice +"</span>"))
.append($("<s>"+ item.skuInfo.price +"</s>"))
.appendTo("#seckillSkuContent");
});
}
五、获取处于秒杀的商品信息
1、编写获取某个商品的秒杀预告信息
修改商品服务的SkuInfoServiceImpl类的 item 方法
SkuInfoServiceImpl.java
@Override
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
// 1、sku基本信息 pms_sku_info
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
// 2、获取 spu 的销售属性组合
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync(res -> {
List<SkuItemSaleAttrsVo> saleAttrVos = saleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
// 3、获取 spu 的介绍 pms_spu_info_desc
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
// 4、获取 spu 的规格参数信息 pms_spu_info_desc
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(res -> {
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
// 5、sku的图片信息 pms_sku_images
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
// 6、查询当前sku是否参与秒杀优惠
CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
R seckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
if (seckillInfo.getCode() == 0) {
SeckillInfoVo seckillInfoVo = seckillInfo.getData(new TypeReference<SeckillInfoVo>() {
});
skuItemVo.setSeckillInfo(seckillInfoVo);
}
}, executor);
// 等待所有任务都完成
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,secKillFuture).join();
return skuItemVo;
}
1.在商品服务中编写远程调用秒杀服务的feign接口
package com.atguigu.gulimall.product.feign;
@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
封装接收VO
SkuItemVo.java
package com.atguigu.gulimall.product.vo;
/**
* Description: 商品详情
*/
@Data
public class SkuItemVo {
// 1、sku基本信息 pms_sku_info
SkuInfoEntity info;
// 是否有货
boolean hasStock = true;
// 2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images;
// 3、获取 spu 的销售属性组合
List<SkuItemSaleAttrsVo> saleAttr;
// 4、获取 spu 的介绍 pms_spu_info_desc
SpuInfoDescEntity desp;
// 5、获取 spu 的规格参数信息
List<SpuItemAttrGroupVo> groupAttrs;
// 6、当前商品的秒杀优惠信息
SeckillInfoVo seckillInfo;
}
SeckillInfoVo.java
package com.atguigu.gulimall.product.vo;
@Data
public class SeckillInfoVo {
/**
* 活动id
*/
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;
/**
* 当前商品秒杀活动的开始时间
*/
private Long startTime;
/**
* 当前商品秒杀活动的结束时间
*/
private Long endTime;
}
2.在秒杀服务中编写获取某个商品的秒杀预告信息接口
SeckillController.java
package com.atguigu.gulimall.seckill.controller;
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 获取某个商品的秒杀预告信息
* @param skuId
* @return
*/
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
SecKillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}
}
SeckillServiceImpl.java
/**
* 获取某个商品的秒杀预告信息
* @param skuId
* @return
*/
@Override
public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
// 1、找到所有需要参与秒杀的key
BoundHashOperations<String, String, String> hashOps = redisTemplate.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 skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
long current = new Date().getTime();
Long startTime = skuRedisTo.getStartTime();
Long endTime = skuRedisTo.getEndTime();
if (current>=startTime && current<=endTime){
// 在秒杀活动时
} else {
// 不在秒杀活动时不应该传递随机码
skuRedisTo.setRandomCode("");
}
return skuRedisTo;
}
}
}
return null;
}
2、商品详情页前端渲染
修改 item.html 页面
<div class="box-summary clear">
<ul>
<li>京东价</li>
<li>
<span>¥</span>
<span th:text="${#numbers.formatDecimal(item.info.price,0,2)}">4499.00</span>
</li>
<li style="color: red" th:if="${item.seckillInfo!=null}">
<span th:if="${#dates.createNow().getTime() < item.seckillInfo.startTime}">
商品将会在 [[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]] 进行秒杀
</span>
<span th:if="${#dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
</span>
</li>
<li>
<a href="/static/item/">
预约说明
</a>
</li>
</ul>
</div>
六、登录检查
1、商品详情页修改
- 在秒杀活动时,商品显示:立刻抢购
- 登录才跳转至 秒杀服务
- 未登录不跳转
- 在秒杀活动外,商品显示:加入购物车
1.修改 item.html 页面
<div class="box-btns-two" th:if="${item.seckillInfo != null && (item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime)}">
<a href="#" id="seckillA" th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfo.promotionSessionId},code=${item.seckillInfo.randomCode}">
立即抢购
</a>
</div>
<div class="box-btns-two" th:if="${item.seckillInfo == null || (item.seckillInfo.startTime > #dates.createNow().getTime() || #dates.createNow().getTime() > item.seckillInfo.endTime)}">
<a href="#" id="addToCart" th:attr="skuId=${item.info.skuId}">
加入购物车
</a>
</div>
- 前端要考虑秒杀系统设计的限流思想
- 在进行立即抢购之前,前端先进行判断是否登录
$("#secKillA").click(function () {
var islogin = [[${session.loginUser!=null}]];
if (islogin) {
var killId = $(this).attr("sessionid")+"_"+$(this).attr("skuid");
var key = $(this).attr("code");
var num = $("#numInput").val();
location.href = "http://seckill.gulimall.cn/kill?killId="+killId+"&key="+key+"&num="+num;
} else {
alert("秒杀请先登录!");
}
return false;
});
2、秒杀服务登录检查
1.引入SpringSession依赖的Redis
<!-- 整合SpringSession完成Session共享问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2.在配置文件中添加SpringSession的保存方式
#SpringSession的保存方式
spring.session.store-type=redis
3.主启动类开启RedisHttpSession这个功能
package com.atguigu.gulimall.seckill;
@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
4.编写SpringSession的配置
package com.atguigu.gulimall.seckill.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* Description: 自定义Session 配置
*/
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.cn");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
5.编写用户登录拦截器并配置到Spring容器中
package com.atguigu.gulimall.seckill.interceptoe;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match("/kill", uri);
if (match){
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
} else {
// 没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.cn/login.html");
return false;
}
}
return true;
}
}
注意:把拦截器配置到spring中,否则拦截器不生效。添加addInterceptors表示当前项目的所有请求都要经过这个拦截请求。
添加SeckillWebConfig
package com.atguigu.gulimall.seckill.config;
@Configuration
public class SeckillWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
七、秒杀
1、模式一
加入购物车秒杀-----不推荐用
优点: 加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好
缺点: 秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单
2、模式二
独立秒杀业务来处理----我们使用此秒杀模式
优点: 从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息
缺点: 如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款
解决方案: 不使用订单服务处理秒杀消息,需要一套独立的业务来处理
1.秒杀请求的处理
Controller层接口的编写
package com.atguigu.gulimall.seckill.controller;
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 秒杀请求
* @return
*/
@GetMapping("/kill")
public R secKill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num) {
String orderSn = seckillService.kill(killId,key,num);
return R.ok().setData(orderSn);
}
}
2.引入rabbitMQ
①引入依赖
<!--RabbitMq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
②编写配置
#RabbitMq的配置
spring.rabbitmq.host=192.168.88.130
spring.rabbitmq.virtual-host=/
③编写配置类
MyRabbitConfig.java
package com.atguigu.gulimall.seckill.config;
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
④编写创建消息队列、以及消息队列和交换器的绑定
/**
* 商品秒杀队列
* 作用:削峰,创建订单
*/
@Bean
public Queue orderSecKillOrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
3.创建订单
秒杀代码SeckillServiceImpl.java
/**
* 秒杀处理,发送消息给MQ
* @param killId 存放的key
* @param key 随机码
* @param num 购买数量
* @return 生成的订单号
*/
@Override
public String kill(String killId, String key, Integer num) {
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
// 1、获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String json = hashOps.get(killId);
if (StringUtils.isEmpty(json)) {
return null;
} else {
SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
// 2、校验合法性
long time = new Date().getTime();
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
long ttl = endTime - time;
// 2.1、校验时间的合法性
if (time >= startTime && time <= endTime) {
// 2.2、校验随机码 和 商品id 是否正确
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
// 2.3、验证购物车数量是否合理
if (num <= redis.getSeckillLimit().intValue()) {
// 2.4、验证这个人是否购买过。幂等性:如果只要秒杀成功,就去占位。 userId_SessionId_skuId
String redisKey = respVo.getId() + "_" + skuId;
// 自动过期
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
// 占位成功说明从来没有买过
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
try {
boolean tryAcquire = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
// 秒杀成功
// 3、快速下单,给MQ发送消息
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(respVo.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redis.getPromotionSessionId());
orderTo.setSkuId(redis.getSkuId());
orderTo.setSeckillPrice(redis.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", orderTo);
return timeId;
} catch (InterruptedException e) {
return null;
}
} else {
// 说明已经买过了
return null;
}
}
} else {
return null;
}
} else {
return null;
}
}
return null;
}
封装消息传递的TO
package com.atguigu.common.to.mq;
import lombok.Data;
import java.math.BigDecimal;
/**
* Description: 秒杀订单
*/
@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;
}
4.监听队列,秒杀消息消费
package com.atguigu.gulimall.order.listener;
@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
try {
log.info("准备创建秒杀单的详细信息:"+seckillOrder);
orderService.createSeckillOrder(seckillOrder);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
创建秒杀订单createSeckillOrder.java
/**
* 创建秒杀单
* @param orderTo
*/
@Override
public void createSeckillOrder(SeckillOrderTo orderTo) {
//TODO 保存订单信息
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderTo.getOrderSn());
orderEntity.setMemberId(orderTo.getMemberId());
orderEntity.setCreateTime(new Date());
BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
orderEntity.setPayAmount(totalPrice);
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
//保存订单
this.save(orderEntity);
//保存订单项信息
OrderItemEntity orderItem = new OrderItemEntity();
orderItem.setOrderSn(orderTo.getOrderSn());
orderItem.setRealAmount(totalPrice);
orderItem.setSkuQuantity(orderTo.getNum());
//保存商品的spu信息
R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());
SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
});
orderItem.setSpuId(spuInfoData.getId());
orderItem.setSpuName(spuInfoData.getSpuName());
orderItem.setSpuBrand(spuInfoData.getBrandName());
orderItem.setCategoryId(spuInfoData.getCatalogId());
//保存订单项数据
orderItemService.save(orderItem);
}
5.秒杀页面
①导入thymeleaf依赖
<!--模板引擎 thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
②关闭thymeleaf缓存
#关闭缓存
spring.thymeleaf.cache=false
③修改Controller层代码进行页面跳转
package com.atguigu.gulimall.seckill.controller;
@Controller
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 返回当前时间可以参与秒杀的商品信息
* @return
*/
@ResponseBody
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
List<SecKillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
/**
* 获取某个商品的秒杀预告信息
* @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);
}
/**
* 秒杀请求
* @return
*/
@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);
model.addAttribute("orderSn",orderSn);
return "success";
}
}
④前端页面修改
<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">
<h1>恭喜,秒杀成功!订单号: [[${orderSn}]]</h1>
<h2>正在准备订单数据,10s以后自动跳转支付 <a style="color: red" th:href="${'http://order.gulimall.cn/payOrder?orderSn='+orderSn}">去支付</a></h2>
</div>
</div>
<div th:if="${orderSn==null}">
<h1>手气不好,秒杀失败!</h1>
</div>
</div>
</div>
</div>
八、秒杀设计问题的解决方法
服务单一职责+独立部署:新增秒杀服务
秒杀链接加密:请求需要随机码,在秒杀开始时随机码才会放在商品信息中
库存预热+快速扣减:库存放入redis中,使用分布式信号量扣减+限流
动静分离:Nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
恶意请求拦截:使用网关拦截,一些不带令牌的请求循环发送,本系统做了登录拦截器
流量错峰:
- 1、输入验证码需要时间,将流量错开了【速度有快有慢】
- 2、加入购物车,然后再结算【速度有快有慢】--当前使用
限流&熔断&降级:spring alibaba sentinel
队列削峰:秒杀服务将创建订单的请求存入mq,订单服务监听mq。
最后提一句:高并发有三宝,缓存异步队排好
结束!