本笔记内容为尚硅谷谷粒商城秒杀服务部分
目录
一、秒杀业务的介绍
秒杀设计
秒杀流程
二、搭建秒杀服务环境
1、秒杀服务后台管理系统
2、搭建秒杀服务环境
二、定时任务
1、cron 表达式
2、测试
三、商品上架
1、远程查询秒杀的活动以及关联的商品信息
2、在Redis中保存秒杀场次信息
3、在Redis中保存秒杀活动关联的商品信息
4、幂等性保证
加锁
判断Redis中是否已上架
四、获取当前的秒杀商品并展示
1、获取当前的秒杀商品
2、首页获取并拼装数据
五、获取处于秒杀的商品信息
1、编写获取某个商品的秒杀预告信息
2、商品详情页前端渲染
六、登录检查
1、商品详情页修改
2、秒杀服务登录检查
七、秒杀
1、模式一
2、模式二
1.秒杀请求的处理
2.引入rabbitMQ
3.创建订单
4.监听队列,秒杀消息消费
5.秒杀页面
八、秒杀设计问题的解决方法
一、秒杀业务的介绍
秒杀业务:秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步+ 缓存 (页面静态化)+ 独立部署。
限流方式:
- 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
- nginx 限流,直接负载部分请求到错误的静态页面:令牌算法、漏斗算法
- 网关限流,限流的过滤器
- 代码中使用分布式信号量
- rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能
秒杀设计
秒杀流程
二、搭建秒杀服务环境
1、秒杀服务后台管理系统
1.网关配置
- id: coupon_routeuri: lb://gulimall-couponpredicates:- 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 {@Overridepublic PageUtils queryPage(Map<String, Object> params) {QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();String promotionSessionId = (String) params.get("promotionSessionId");// 场次id不是nullif (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 表达式
在线Cron表达式生成器 (qqe2.com)
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 {@Autowiredprivate 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 {@AutowiredSeckillSkuRelationService seckillSkuRelationService;@Overridepublic 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 {@AutowiredCouponFeignService couponFeignService;@AutowiredStringRedisTemplate 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 {@AutowiredSeckillService seckillService;@AutowiredRedissonClient 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 {@AutowiredCouponFeignService couponFeignService;@AutowiredProductFeignService productFeignService;@AutowiredStringRedisTemplate redisTemplate;@AutowiredRedissonClient 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 天内秒杀的活动 以及 秒杀活动的关联的商品信息*/@Overridepublic 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 {@AutowiredSeckillService 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_1650160800000String 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_routeuri: lb://gulimall-seckillpredicates:- 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_infoCompletableFuture<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_descCompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());skuItemVo.setDesp(spuInfoDescEntity);}, executor);// 4、获取 spu 的规格参数信息 pms_spu_info_descCompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(res -> {List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());skuItemVo.setGroupAttrs(attrGroupVos);}, executor);// 5、sku的图片信息 pms_sku_imagesCompletableFuture<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_infoSkuInfoEntity info;// 是否有货boolean hasStock = true;// 2、sku的图片信息 pms_sku_imagesList<SkuImagesEntity> images;// 3、获取 spu 的销售属性组合List<SkuItemSaleAttrsVo> saleAttr;// 4、获取 spu 的介绍 pms_spu_info_descSpuInfoDescEntity 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 {@AutowiredSeckillService 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、找到所有需要参与秒杀的keyBoundHashOperations<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 {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.cn");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic 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<>();@Overridepublic 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 {@AutowiredLoginUserInterceptor interceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(interceptor).addPathPatterns("/**");}
}
七、秒杀
1、模式一
加入购物车秒杀-----不推荐用
优点: 加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好
缺点: 秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单
2、模式二
独立秒杀业务来处理----我们使用此秒杀模式
优点: 从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息
缺点: 如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款
解决方案: 不使用订单服务处理秒杀消息,需要一套独立的业务来处理
1.秒杀请求的处理
Controller层接口的编写
package com.atguigu.gulimall.seckill.controller;@RestController
public class SeckillController {@AutowiredSeckillService 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 {@AutowiredRabbitTemplate rabbitTemplate;/*** 使用JSON序列化机制,进行消息转换* @return*/@Beanpublic 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> argumentsBinding 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_skuIdString 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 {@AutowiredOrderService orderService;@RabbitHandlerpublic 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 {@AutowiredSeckillService 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。
最后提一句:高并发有三宝,缓存异步队排好
结束!