去到我们的后台管理系统查询上架秒杀商品的管理页面:
然后查看它所发送的请求,我们就知道应该编写哪个服务的数据
配置网关路由:
注意:url 和id 一定要写在同一列,因为他们是同级别的。gateway 也会识别出这块的格式。
点击新增,就能增加秒杀活动的场次
添加了两个场次:
每个场次都能关联对应的商品:
根据点击这个"关联商品" 出现的request 请求,我们找到后台的Controller。然后修改里面的参数。我们要达到的效果是:点击不同场次,显示出不同场次中包含的商品,所以查询时应该带上这个场次的id
seckill_session: 秒杀场次。 seckill_sku_relation: 场次关联的商品
秒杀服务要单独成一个服务,因为就算这里因为高并发崩溃了,也不会影响其他服务。
秒杀流程:
定时任务:
定时任务的框架:Quartz
网上有很多生成cron 表达式的网站。
在如果日,月是通配,周不是,那么日,月其中一个得是 ?
如果日,月,周都是通配,则他们其中一个得是 ?
SpringBoot 整合cron 表达式定时任务与异步任务:
在原生cron 表达式中:1 = 星期天,6 = 星期5。
在Spring 的cron 表达式中:1 = 星期一,7 = 星期日
/**
* 定时任务:
* 1. @EnableScheduling开启定时任务
* 2. @Component 放入容器中
* 3. @Scheduled 开启定时任务
* 4、自动配置类 TaskSchedulingAutoConfiguration
*
* 异步任务:
* 1. @EnableAsync 开启异步任务功能
* 2. @Async 给希望异步执行的方法上标注
* 3. 自动配置类 TaskExecutionAutoConfiguration
*/
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
* 1. 在Spring 只允许6 位组成,不允许第7 位的年
* 2. 在周几的位置,1-7 代表: 星期一到日: MON-SUN
* 3. 定时任务不应该阻塞。默认是阻塞的(当前定时任务阻塞住,下一个定时任务也无法执行)
* 1). 可以让业务运行以异步的方式,自己提交到到线程池(要自己配置线程池,麻烦)
* 2). 支持定时任务线程池:设置 TaskSchedulingProperties (有些版本的spring 不能使用该功能,不用)
* spring.task.scheduling.pool.size=5
* 3). 让定时任务异步执行(简单方便,使用)
* 异步任务
* 解决:使用异步+定时任务来完成定时任务不阻塞的功能
*/
@Async
@Scheduled(cron = "*/5 * * ? * 7")
public void hello(){
log.info("hello...");
}
}
application.properties
查询最近3 天的秒杀活动场次:
是在coupon 服务中进行查询
数据库查询语句:
SeckillSessionServiceImpl:
@Override
public List<SeckillSessionEntity> getLatest3DaySession() {
List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
return list;
}
/**
* 得出当前日期的最小时间:2020/11/22 00:00:00
* @return
*/
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;
}
/**
* 得到最近三天的场次
* @return
*/
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;
}
关于java8 中时间类的简单使用:
查出活动以后,从活动中查询其中包含的商品
为了方便,在秒杀活动的实体类中添加一个秒杀活动的商品的集合字段
将秒杀场次和秒杀商品都放到Redis 中的处理类的相关字段.
从coupon 服务中获取到秒杀场次和秒杀商品的数据并封装成属于coupon 服务的vo 数据:
SekillSessionsWithSkus(秒杀场次对象):
SeckillSkuVo(秒杀场次所包含的秒杀商品):
@Data
public class SeckillSkuVo {
private Long id;
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private Integer seckillCount;
/**
* 每人限购数量
*/
private Integer seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
}
因为缓存的商品对象需要用到商品的详细信息,所以又构建了一个to 对象(秒杀商品信息+商品详细信息+秒杀开始/结束信息)
SecKillSkuRedisTo:
@Data
public class SecKillSkuRedisTo {
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private Integer seckillCount;
/**
* 每人限购数量
*/
private Integer seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
//sku 的详细信息
private SkuInfoVo skuInfo;
//当前商品秒杀的开始时间
private Long startTime;
//当前商品秒杀的结束时间
private Long endTime;
//当前商品的秒杀随机码
private String randomCode;
}
将秒杀场次对象缓存到redis 中
将秒杀商品也缓存到redis 中
private void saveSessionSkuInfos(List<SekillSessionsWithSkus> 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.设置redis 信息号量
//随机码和信号量产生关联,只有知道随机码才能进行信号量的减少,进而才能抢到秒杀商品
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
//使用库存作为信号量的值
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getId(),jsonString);
});
});
}
因为这里用到了redis 的操作,所以就要引入redis
幂等性处理:
上架过的就不要再上传了。
解决:要为定时任务加一把分布式锁。这里就解决了定时任务的幂等性。
让优先获取到锁的机器先执行定时任务
解决业务代码的幂等性:
为了方便对比数据,对Redis 的存储的数据修改一下结构。
当优先进来的机器进来执行完业务代码后,后面的机器进来看到redis 中已经存在了这些数据了,就不用重复上架相同的秒杀场次和秒杀商品了。
给定时任务加上分布式锁:
业务代码:
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ProductFeignService productFeignService;
@Autowired
RedissonClient redissonClient;
private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+ 商品随机码
@Override
public void uploadSeckillSkuLatest3Days() {
//1. 扫描最近3天需要参与秒杀的活动
R session = couponFeignService.getLatest3DaySession();
if (session.getCode() == 0){
//上架商品
List<SekillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SekillSessionsWithSkus>>() {
});
//缓存到redis 中
//1. 缓存活动信息
saveSessionInfos(sessionData);
//2. 缓存活动的关联商品信息
saveSessionSkuInfos(sessionData);
}
}
private void saveSessionInfos(List<SekillSessionsWithSkus> sessions){
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
//存到redis 的key 值
String key = SESSIONS_CACHE_PREFIX + startTime+ "_"+endTime;
//如果当前Redis 中已经有了这个key,那就不需要再往里面加这个数据了
Boolean hasKey = redisTemplate.hasKey(key);
//获取当前活动场次的所有商品id
if (!hasKey){
List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
//缓存活动信息
redisTemplate.opsForList().leftPushAll(key,collect);
}
});
}
private void saveSessionSkuInfos(List<SekillSessionsWithSkus> sessions){
sessions.stream().forEach(session -> {
//准备hash 操作
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//4. 商品的随机码
//这个随机码的作用:当秒杀商品的请求暴露以后,有人可能会恶意多次发送这个请求来强商品
//或者后台人员都知道发送这个请求来抢这个商品的话,客户就会很亏,所以抢商品时就要携带一个随机码
String token = UUID.randomUUID().toString().replace("-", "");
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);
//将数据以JSON 字符串形式序列化,方便在Redis 中阅读
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(),jsonString);
//如果当前这个场次的商品的库存信息已经上架就不需要上架
//5.设置redis 信息号量
//随机码和信号量产生关联,只有知道随机码才能进行信号量的减少,进而才能抢到秒杀商品
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
//使用库存作为信号量的值
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
});
});
}
}
完成功能:
1.将参与秒杀的商品放到秒杀模块部分显示
2.还有就是在商品详情页中显示当前当前商品如果参与秒杀活动的话,就会有显示。
功能1:当前页面得判断当前的时间,将当前时间具有的秒杀商品显示出来
SeckillServiceImpl:
@Override
public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
//1. 确定当前时间属于哪个秒杀场次
//1970 - 现在的差值数据
long time = new Date().getTime();
//获取到redis 中key 中带有SESSIONS_CACHE_PREFIX 的所有key.
Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
for (String key : keys) {
//"seckill:sessions:1606003200000_1606010400000"
String replace = key.replace(SESSIONS_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);
//因为我们存入秒杀商品时的数据结构就是Hash,所以取出来也要绑定Hash 结构
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);
return redis;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
return null;
}
在商城首页:一刷新商品页面就立刻发送ajax 请求,请求秒杀商品
以上就完成了首页的秒杀商品信息。
完成功能2:
在商品详情页面有当前时间的秒杀价格信息,并且在页面的秒杀活动中能跳转到对应秒杀的商品详情页
因为涉及到商品详情页的数据,所以就去到商品详情页的Controller 中获取数据就行了
itemController:
SkuInfoServiceImpl:
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1. sku 基本信息获取 pms_sku_info
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3. 获取spu 的销售属性组合
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
},executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
//4. 获取spu 的介绍
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//5. 获取spu 的规格参数信息
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySkuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
CompletableFuture<Void> imagesFuture = CompletableFuture.runAsync(() -> {
//2. sku 图片信息获取 pms_sku_images
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);
//要等待所有的异步线程完成以后,才能return skuItemVo;
CompletableFuture.allOf(saleAttrFuture,descFuture,imagesFuture,secKillFuture).get();
return skuItemVo;
}
从秒杀服务中,利用当前skuId 查询到当前商品有没有秒杀活动
而且为了接受从秒杀活动查询得到的数据,也创建了对应的vo 对象
@Data
public class SeckillInfoVo {
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private Integer seckillCount;
/**
* 每人限购数量
*/
private Integer seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
//当前商品秒杀的开始时间
private Long startTime;
//当前商品秒杀的结束时间
private Long endTime;
//当前商品的秒杀随机码
private String randomCode;
}
远程查询的秒杀服务:
SeckillController:
SeckillServiceImpl:
@Override
public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
//1. 找到所有需要参与秒杀的商品的key
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SESSIONS_CACHE_PREFIX);
Set<String> keys = hashOps.keys();
if (keys != null && keys.size() > 0){
String regx = "\\d_"+skuId;
for (String key : keys) {
//6_4
if(Pattern.matches(regx,key)){
String json = hashOps.get(key);
SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class);
//随机码:如果当前时间刚好是这件商品的秒杀时间,那么就返回它的随机码,不然就不返回
long current = new Date().getTime();
if(current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()){
}else{
skuRedisTo.setRandomCode(null);
}
return skuRedisTo;
}
}
}
return null;
}
以上就是后端查询得到的商品秒杀数据,把这些数据封装打包给前端:
商品详情页(item.html)
效果:查询当前时间,如果时间在还没到秒杀活动,则提示秒杀活动的时间。如果时间在秒杀活动期间,则提示商品的秒杀价格。
从商品首页跳转到秒杀商品详情页:
秒杀系统的设计:
秒杀与不秒杀的商品都是在点击购买以后进入到购物车的。所以秒杀业务不是重点,而是考虑如何设计一个高并发的系统,能承载住大量的峰值流量。
07:限流& 熔断 & 降级,这个在高并发系统中一定要考虑。
01:服务单一职责,独立部署:我们项目已经将秒杀服务独立成一个项目服务
02:秒杀链接加密:为每一个秒杀商品都加上一个随机码,用于加密。只有拿着随机码才能进行秒杀。项目中有了
03:库存预热,快速扣减:使用redis 的信号量来对秒杀数据请求进行控制。项目中有了。
04:动静分离:将静态资源直接部署到nginx上,我们项目已经有了
06:流量错峰:我们项目使用了购物车逻辑,这使得流量得以错峰。
我们开始关注05,07,08 问题。
引入spring-session,用于进行登录校验:
另外,因为redis 默认的客户端总是有问题,所以要更换redis 客户端
在配置文件中声明session 的存储方式:redis
配置Cookie 的作用域以及session 的序列化机制
开启Redis 中的session 功能:
以上配置好spring-session 以后才能写上拦截器,它的作用就是每次有请求进入秒杀服务后,都得先通过这个拦截器判断有没有登录,再按照拦截器的指示进行操作:
在秒杀服务中,只有/kill 请求需要判断有没有登录,其他都不需要判断登录状态,所以放行其他请求
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
//使用ThreadLocal 来进行数据共享
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
/**
* 在目标请求到达之前,执行该方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match =antPathMatcher.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.store/login.html");
return false;
}
}
return true;
}
}
要想这个拦截器生效,就得注入到Spring 的web 容器中:
后端之所以要拦截/kill 请求(抢购秒杀商品请求),是因为这也是一个错峰限流的操作。包括前端的检查登录状态,拦截抢购商品操作也是:
抢购秒杀商品第一套流程:秒杀商品其实就是优惠商品,也是直接从购物车到支付。
优点:因为有加入购物车环节,所以流量就能分散开来。它会把流量带到购物车服务中,
第二套流程:这套能应对超高并发的流量。
优点:没有操作数据库,没有做过远程调用,这是非常快的流程,非常快的创建好订单号等订单服务消费即可。
缺点:如果订单服务炸了,那么订单消息没有订单服务消费,所以秒杀商品就一直支付不成功。
完成合法校验+快速创建订单号 ,然后返回订单号流程:
接收请求:
SeckillServiceImpl
@Override
public String kill(String killId, String key, Integer num) {
//从拦截器中得到当前用户的id
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
//1. 获取当前秒杀商品的详细信息
//从Redis 中获取,因为商品详细信息在redis 中是以Hash 形式保存的,所以就要绑定一个hash 操作
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);
//校验合法性
//1. 秒杀时间是否已经过了(检验时间合法性)
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
//当前时间
long time = new Date().getTime();
//当前秒杀场次剩下的时间
long ttl = endTime - time;
if (time >= startTime && time <= endTime){
//能进入这里,证明时间合法
//2. 校验随机码和商品id 是否正确
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() +"_"+redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)){
//3. 验证购物数量是否合理
if(num <= redis.getSeckillLimit()){
//4. 验证这个人是否已经购买过。
//幂等性:如果秒杀成功就去Redis 中占位,userId_SessionId_skuId
String redisKey = respVo.getId()+"_"+skuId;
//设置自动过期时间: MICROSECONDS: 微秒; MILLISECONDS : 毫秒
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean){
//如果占位成功,说明这个人从来没买过这个秒杀商品
//购买商品
//1. 获取特定的分布式信号量(对应秒杀商品的数量)
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//MILLISECONDS : 毫秒
try {
//不用acquire() 原因: 它是阻塞方法,必须等到有信号量才会去减信号量
//tryAcquire() : 有则减,没有则过
boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
//秒杀成功
//快速下单: 发送MQ 消息, 耗费10ms
//利用mybatis-plus 的工具类:IdWorker 来生成订单号
String timeId = IdWorker.getTimeId();
return timeId;
} catch (InterruptedException e) {
return null;
}
}else{
//如果失败,说明已经买过了
return null;
}
}
}else{
return null;
}
}else{
return null;
}
}
return null;
}
引入RabbitMQ:
配置RabbitMQ 的信息:
配置发送给RabbitMQ 的数据序列化:
可以不用在配置类中写@EnableRabbit。因为这个配置是用来监听RabbitMQ 的消息用的。我们现在是发消息给RabbitMQ
SeckillServiceImpl:
@Override
public String kill(String killId, String key, Integer num) {
//从拦截器中得到当前用户的id
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
//1. 获取当前秒杀商品的详细信息
//从Redis 中获取,因为商品详细信息在redis 中是以Hash 形式保存的,所以就要绑定一个hash 操作
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);
//校验合法性
//1. 秒杀时间是否已经过了(检验时间合法性)
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
//当前时间
long time = new Date().getTime();
//当前秒杀场次剩下的时间
long ttl = endTime - time;
if (time >= startTime && time <= endTime){
//能进入这里,证明时间合法
//2. 校验随机码和商品id 是否正确
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() +"_"+redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)){
//3. 验证购物数量是否合理
if(num <= redis.getSeckillLimit()){
//4. 验证这个人是否已经购买过。
//幂等性:如果秒杀成功就去Redis 中占位,userId_SessionId_skuId
String redisKey = respVo.getId()+"_"+skuId;
//设置自动过期时间: MICROSECONDS: 微秒; MILLISECONDS : 毫秒
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean){
//如果占位成功,说明这个人从来没买过这个秒杀商品
//购买商品
//1. 获取特定的分布式信号量(对应秒杀商品的数量)
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//MILLISECONDS : 毫秒
//不用acquire() 原因: 它是阻塞方法,必须等到有信号量才会去减信号量
//tryAcquire() : 有则减,没有则过
boolean b = semaphore.tryAcquire(num);
//只有当信号量减成功了,才能说明抢到了秒杀商品,此时才能创建订单
if (b){
//秒杀成功
//快速下单: 发送MQ 消息, 耗费10ms
//利用mybatis-plus 的工具类:IdWorker 来生成订单号
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;
}
return null;
}else{
//如果失败,说明已经买过了
return null;
}
}
}else{
return null;
}
}else{
return null;
}
}
return null;
}
MQ 消息队列模型:
按照MQ 模型来创建消息队列:
然后通过order 服务中,监听RabbitMQ 的队列: order.seckill.order.queue 队列,来触发保存订单和订单项数据业务:
OrderServiceImpl:
引入RabbitMQ 和Redis 就能做到削峰操作,因为Redis 的信号量(秒杀商品总数量)是有限的,只要Redis 的信号量减完了,那么之后发过来的请求都不会进行接收,然后所有秒杀成功的请求都进入消息队列,再发消息给order 服务慢慢创建订单,这就不会造成有一大批的流量一时间涌进Order 服务导致其崩溃了。
秒杀页面的完成:
前端的商品详情页:加入购物车和立即抢购按钮的逻辑判断
要实现的效果:
当成功抢到商品(点击立即抢购成功),来到购物车页面,提示:“恭喜你抢购成功”.
所以这是给秒杀服务单独写一个购物车页面,就把cart 服务的seccuess.html 页面拿过来就行
引入thymeleaf 依赖:
关闭thymeleaf 缓存:
因为success.html 的静态资源都是从cart 服务中的,这些资源都在Nginx 中,所以就要给所有的src, href 连接都加上cart 服务的nginx 入口路径
效果图:
抢购成功:
支付: