电商项目实战之商品秒杀
定时任务
corn表达式
-
定时查询秒杀活动
https://cron.qqe2.com/
实现方式
基于注解
-
内容介绍
基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
-
cron表达式参数
Cron表达式参数分别表示: 秒(0~59) 例如0/5表示每5秒 分(0~59) 时(0~23) 日(0~31)的某天,需计算 月(0~11) 周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT) @Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。 // Cron表达式范例: 每隔5秒执行一次:*/5 * * * * ? 每隔1分钟执行一次:0 */1 * * * ? 每天23点执行一次:0 0 23 * * ? 每天凌晨1点执行一次:0 0 1 * * ? 每月1号凌晨1点执行一次:0 0 1 1 * ? 每月最后一天23点执行一次:0 0 23 L * ? 每周星期天凌晨1点实行一次:0 0 1 ? * L 在26分、29分、33分执行一次:0 26,29,33 * * * ? 每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
-
实现源码
@Configuration //1.主要用于标记配置类,兼备Component的效果。 @EnableScheduling // 2.开启定时任务 public class SaticScheduleTask { //3.添加定时任务 @Scheduled(cron = "0/5 * * * * ?") //或直接指定时间间隔,例如:5秒 //@Scheduled(fixedRate=5000) private void configureTasks() { System.err.println("执行静态定时任务时间: " + LocalDateTime.now()); } }
-
方案分析
显然,使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到实时生效的效果,可以使用接口来完成定时任务。
基于接口
-
引入依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.0.4.RELEASE</version> </parent> <dependencies> <dependency><!--添加Web依赖 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency><!--添加MySql依赖 --> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency><!--添加Mybatis依赖 配置mybatis的一些初始化的东西--> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency><!-- 添加mybatis依赖 --> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.5</version> <scope>compile</scope> </dependency> </dependencies>
-
表结构创建
DROP DATABASE IF EXISTS `socks`; CREATE DATABASE `socks`; USE `SOCKS`; DROP TABLE IF EXISTS `cron`; CREATE TABLE `cron` ( `cron_id` varchar(30) NOT NULL PRIMARY KEY, `cron` varchar(30) NOT NULL ); INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');
-
开启定时任务
@Configuration //1.主要用于标记配置类,兼备Component的效果。 @EnableScheduling // 2.开启定时任务 public class DynamicScheduleTask implements SchedulingConfigurer { @Mapper public interface CronMapper { @Select("select cron from cron limit 1") public String getCron(); } @Autowired //注入mapper @SuppressWarnings("all") CronMapper cronMapper; /** * 执行定时任务. */ @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addTriggerTask( //1.添加任务内容(Runnable) () -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()), //2.设置执行周期(Trigger) triggerContext -> { //2.1 从数据库获取执行周期 String cron = cronMapper.getCron(); //2.2 合法性校验. if (StringUtils.isEmpty(cron)) { // Omitted Code .. } //2.3 返回执行周期(Date) return new CronTrigger(cron).nextExecutionTime(triggerContext); } ); } }
-
多线程定时任务
//@Component注解用于对那些比较中立的类进行注释; //相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释 @Component @EnableScheduling // 1.开启定时任务 @EnableAsync // 2.开启多线程 public class MultithreadScheduleTask { @Async @Scheduled(fixedDelay = 1000) //间隔1秒 public void first() throws InterruptedException { System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName()); System.out.println(); Thread.sleep(1000 * 10); } @Async @Scheduled(fixedDelay = 2000) public void second() { System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName()); System.out.println(); } }
实战
-
Spring定时任务
/** * 定时任务 * 1、@EnableScheduling 开启定时任务(注解类上) * 2、@Scheduled开启一个定时任务(注解方法) * * 异步任务 * 1、@EnableAsync:开启异步任务(注解类上) * 2、@Async:给希望异步执行的方法标注(注解方法) */ @Slf4j // 开启异步任务 //@EnableAsync //@EnableScheduling //@Component public class HelloSchedule { /** * 1、在Spring中表达式是6位组成,不允许第七位的年份 * 2、在周几的的位置,1-7代表周一到周日 * 3、定时任务不该阻塞。默认是阻塞的 * 1)、可以让业务以异步的方式,自己提交到线程池 * CompletableFuture.runAsync(() -> { * },execute); * * 2)、支持定时任务线程池;设置 TaskSchedulingProperties * spring.task.scheduling.pool.size: 5 * * 3)、让定时任务异步执行 * 异步任务自动配置类 TaskExecutionAutoConfiguration * * 解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能 * */ @Async @Scheduled(cron = "*/5 * * ? * 1") public void hello(){ log.info("定时任务..."); try { Thread.sleep(3000); } catch (InterruptedException e) { } } }
秒杀系统
秒杀系统关注问题
-
服务单一职责+独立部署
秒杀服务即使自己扛不住压力,挂掉,也不要影响别人
-
秒杀链接加密
防止恶意攻击,模拟秒杀请求,1000次/s攻击
防止链接暴露,自己工作人员,提前秒杀商品。
-
库存预热+快速扣减
秒杀读多写少,无需每次实时校验库存。我们库存预热,放到redis中,通过信号量控制进来秒杀的请求
-
动静分离
nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。使用CDN网络,分担本集群压力
-
恶意请求拦截
识别非法攻击请求并进行拦截,网关层
-
流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车
-
限流&熔断&降级
前端限流+后端限流
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
-
队列削峰
1万个商品,每个1000件秒杀。双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可。
秒杀架构设计
-
架构流程
nginx -> gateway -> redis分布式信号量 -> 秒杀服务
-
独立部署:独立部署秒杀模块gulimall-seckill;
-
定时任务:每天三点上架最新秒杀商品,削减高峰期压力;
-
秒杀链接加密:为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口;
-
库存预热:先从数据库中扣除一部分库存以 redisson 信号量的形式存储在redis中;
-
队列削峰:秒杀成功后立即返回,然后以发送消息的形式创建订单
-
-
redis数据存储设计
秒杀活动:存储于scekill:sesssions这个redis-key里,value为 skuIds[]
秒杀活动里具体商品项:是一个map,redis-key是seckill:skus,map-key是skuId+商品随机码
-
redis存储模型设计
-
秒杀场次存储的List可以当做hash key在SECKILL_CHARE_PREFIX中获得对应的商品数据;
-
随机码防止人在秒杀一开放就秒杀完,必须携带上随机码才能秒杀;
-
结束时间;
-
设置分布式信号量,这样就不会每个请求都访问数据库。seckill:stock:#(商品随机码);
-
session里存了session-sku[]的列表,而seckill:skus的key也是session-sku,不要只存储sku,不能区分场次。
-
-
案例代码设计
Redis中存放的skuInfo的信息
@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中存储Key-Value值
// 存储的秒杀场次对应数据 // K: SESSION_CACHE_PREFIX + startTime + "_" + endTime; // V: sessionId(活动场次id)+"-"+skuId(商品id)的List private final String SESSION_CACHE_PREFIX = "seckill:sessions:"; // 存储的秒杀商品数据 // K: 固定值SECKILL_CHARE_PREFIX // V: hash,k为sessionId(活动场次id)+"-"+skuId(商品id),v为对应的商品信息SeckillSkuRedisTo private final String SECKILL_CHARE_PREFIX = "seckill:skus"; // K: SKU_STOCK_SEMAPHORE+商品随机码 // V: 秒杀的库存件数 private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // 使用库存作为分布式信号量 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); // 商品可以秒杀的数量作为信号量 semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
商品上架
-
定时上架
-
开启对定时任务支持
@EnableAsync //开启对异步的支持,防止定时任务之间相互阻塞 @EnableScheduling //开启对定时任务的支持 @Configuration public class ScheduledConfig { }
-
每天凌晨三点远程调用coupon(优惠券)服务上架最近三天的秒杀商品;
-
由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法;
-
上架后无需再次上架,用分布式锁做好幂等性
/** * 秒杀商品定时上架 * 每天晚上3点,上架最近三天需要三天秒杀的商品 * 当天00:00:00 - 23:59:59 * 明天00:00:00 - 23:59:59 * 后天00:00:00 - 23:59:59 */ @Slf4j @Service public class SeckillScheduled { @Autowired private SeckillService seckillService; @Autowired private RedissonClient redissonClient; //秒杀商品上架功能的锁 private final String upload_lock = "seckill:upload:lock"; //TODO 保证幂等性问题 // @Scheduled(cron = "*/5 * * * * ? ") @Scheduled(cron = "0 0 1/1 * * ? ") public void uploadSeckillSkuLatest3Days() { //1、重复上架无需处理 log.info("上架秒杀的商品..."); //分布式锁 RLock lock = redissonClient.getLock(upload_lock); try { //加锁 lock.lock(10, TimeUnit.SECONDS); seckillService.uploadSeckillSkuLatest3Days(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } /** * 秒杀服务接口实现类 */ @Slf4j @Service public class SeckillServiceImpl implements SeckillService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private CouponFeignService couponFeignService; @Autowired private ProductFeignService productFeignService; @Autowired private RedissonClient redissonClient; @Autowired private RabbitTemplate rabbitTemplate; private final String SESSION_CACHE_PREFIX = "seckill:sessions:"; private final String SECKILL_CHARE_PREFIX = "seckill:skus"; private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码 @Override public void uploadSeckillSkuLatest3Days() { //1、扫描最近三天的商品需要参加秒杀的活动 R lates3DaySession = couponFeignService.getLates3DaySession(); if (lates3DaySession.getCode() == 0) { //上架商品 List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() { }); //缓存到Redis //1、缓存活动信息 saveSessionInfos(sessionData); //2、缓存活动的关联商品信息 saveSessionSkuInfo(sessionData); } } }
-
-
获取最近三天的秒杀信息
-
获取最近三天的秒杀场次信息,再通过秒杀场次id查询对应的商品信息;
-
防止集群多次上架
@Override public List<SeckillSessionEntity> getLates3DaySession() { //计算最近三天 //查出这三天参与秒杀活动的商品 List<SeckillSessionEntity> list = this.baseMapper.selectList(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(); //查出sms_seckill_sku_relation表中关联的skuId List<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>() .eq("promotion_session_id", id)); session.setRelationSkus(relationSkus); return session; }).collect(Collectors.toList()); return collect; } return null; } /** * 当前时间 * @return */ private String startTime() { LocalDate now = LocalDate.now(); LocalTime min = LocalTime.MIN; LocalDateTime start = LocalDateTime.of(now, min); //格式化时间 String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return startFormat; } /** * 结束时间 * @return */ private String endTime() { LocalDate now = LocalDate.now(); LocalDate plus = now.plusDays(2); LocalTime max = LocalTime.MAX; LocalDateTime end = LocalDateTime.of(plus, max); //格式化时间 String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return endFormat; }
-
-
Redis保存秒杀活动场次信息
/** * 缓存秒杀活动信息 * @param sessions */ private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) { sessions.stream().forEach(session -> { //获取当前活动的开始和结束时间的时间戳 long startTime = session.getStartTime().getTime(); long endTime = session.getEndTime().getTime(); //存入到Redis中的key String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime; //判断Redis中是否有该信息,如果没有才进行添加 Boolean hasKey = redisTemplate.hasKey(key); //缓存活动信息 if (!hasKey) { //获取到活动中所有商品的skuId List<String> skuIds = session.getRelationSkus().stream() .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList()); redisTemplate.opsForList().leftPushAll(key,skuIds); } }); }
-
Redis保存秒杀商品信息
/** * 缓存秒杀活动所关联的商品信息 * @param sessions */ private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) { sessions.stream().forEach(session -> { //准备hash操作,绑定hash BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); session.getRelationSkus().stream().forEach(seckillSkuVo -> { //生成随机码 String token = UUID.randomUUID().toString().replace("-", ""); String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(); // 缓存中没有再添加 if (!operations.hasKey(redisKey)) { //缓存我们商品信息 SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo(); Long skuId = seckillSkuVo.getSkuId(); //1、先查询sku的基本信息,调用远程服务 R info = productFeignService.getSkuInfo(skuId); if (info.getCode() == 0) { SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){}); redisTo.setSkuInfo(skuInfo); } //2、sku的秒杀信息 BeanUtils.copyProperties(seckillSkuVo,redisTo); //3、设置当前商品的秒杀时间信息 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); //4、设置商品的随机码(防止恶意攻击) redisTo.setRandomCode(token); //活动id-skuID 秒杀sku信息 序列化json格式存入Redis中 String seckillValue = JSON.toJSONString(redisTo); operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue); //如果当前这个场次的商品库存信息已经上架就不需要上架 //5、使用库存作为分布式Redisson信号量(限流) // 使用库存作为分布式信号量 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); // 商品可以秒杀的数量作为信号量 semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } }); }); }
获取当前秒杀商品
-
根据redis中缓存秒杀活动的各种信息,获取缓存中当前时间段在秒杀的sku
/** * 获取到当前可以参加秒杀商品的信息 * @return */ @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler") @Override public List<SeckillSkuRedisTo> getCurrentSeckillSkus() { try (Entry entry = SphU.entry("seckillSkus")) { //1、确定当前属于哪个秒杀场次 long currentTime = System.currentTimeMillis(); //从Redis中查询到所有key以seckill:sessions开头的所有数据 Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*"); for (String key : keys) { //seckill:sessions:1594396764000_1594453242000 String replace = key.replace(SESSION_CACHE_PREFIX, ""); String[] s = replace.split("_"); //获取存入Redis商品的开始时间 long startTime = Long.parseLong(s[0]); //获取存入Redis商品的结束时间 long endTime = Long.parseLong(s[1]); //判断是否是当前秒杀场次 if (currentTime >= startTime && currentTime <= endTime) { //2、获取这个秒杀场次需要的所有商品信息 List<String> range = redisTemplate.opsForList().range(key, -100, 100); BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); assert range != null; List<String> listValue = hasOps.multiGet(range); if (listValue != null && listValue.size() >= 0) { List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> { String items = (String) item; SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class); // redisTo.setRandomCode(null);当前秒杀开始需要随机码 return redisTo; }).collect(Collectors.toList()); return collect; } break; } } } catch (BlockException e) { log.error("资源被限流{}",e.getMessage()); } return null; }
获取当前商品的秒杀信息
-
点击秒杀商品
用户点击秒杀商品,如果时间段正确,返回随机码,购买时带着。
注意:不要redis-map中的key。
/** * 根据skuId查询商品是否参加秒杀活动 * @param skuId * @return */ @Override public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) { //1、找到所有需要秒杀的商品的key信息---seckill:skus BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); //拿到所有的key Set<String> keys = hashOps.keys(); if (keys != null && keys.size() > 0) { //4-45 正则表达式进行匹配 String reg = "\\d-" + skuId; for (String key : keys) { //如果匹配上了 if (Pattern.matches(reg,key)) { //从Redis中取出数据来 String redisValue = hashOps.get(key); //进行序列化 SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class); //随机码 Long currentTime = System.currentTimeMillis(); Long startTime = redisTo.getStartTime(); Long endTime = redisTo.getEndTime(); //如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间 if (currentTime >= startTime && currentTime <= endTime) { return redisTo; } redisTo.setRandomCode(null); return redisTo; } } } return null; }
-
查询秒杀对应信息
注意所有的时间都是距离1970的差值
/** * 根据skuId查询商品异步线程查询商品基本信息 * @param skuId * @return */ @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 = this.getById(skuId); skuItemVo.setInfo(info); return info; }, executor); CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> { //3、获取spu的销售属性组合 List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId()); skuItemVo.setSaleAttr(saleAttrVos); }, executor); CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> { //4、获取spu的介绍 pms_spu_info_desc SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId()); skuItemVo.setDesc(spuInfoDescEntity); }, executor); CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> { //5、获取spu的规格参数信息 List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId()); skuItemVo.setGroupAttrs(attrGroupVos); }, executor); // Long spuId = info.getSpuId(); // Long catalogId = info.getCatalogId(); //2、sku的图片信息 pms_sku_images CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> { List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId); skuItemVo.setImages(imagesEntities); }, executor); CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> { //3、远程调用查询当前sku是否参与秒杀优惠活动 R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId); if (skuSeckilInfo.getCode() == 0) { //查询成功 SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() { }); skuItemVo.setSeckillSkuVo(seckilInfoData); if (seckilInfoData != null) { long currentTime = System.currentTimeMillis(); if (currentTime > seckilInfoData.getEndTime()) { skuItemVo.setSeckillSkuVo(null); } } } }, executor); //等到所有任务都完成 CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get(); return skuItemVo; }
秒杀最终处理
-
秒杀流程图示
-
秒杀业务
-
点击立即抢购时,会发送请求;
-
秒杀会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单
-
-
秒杀方案
-
消息队列
-
样例源码
创建订单发消息
/** * 当前商品进行秒杀(秒杀开始) * @param killId * @param key * @param num * @return */ @Override public String kill(String killId, String key, Integer num) throws InterruptedException { long s1 = System.currentTimeMillis(); //获取当前用户的信息 MemberResponseVo user = LoginUserInterceptor.loginUser.get(); //1、获取当前秒杀商品的详细信息从Redis中获取 BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); String skuInfoValue = hashOps.get(killId); if (StringUtils.isEmpty(skuInfoValue)) { return null; } //(合法性效验) SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class); Long startTime = redisTo.getStartTime(); Long endTime = redisTo.getEndTime(); long currentTime = System.currentTimeMillis(); //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性) if (currentTime >= startTime && currentTime <= endTime) { //2、效验随机码和商品id String randomCode = redisTo.getRandomCode(); String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId(); if (randomCode.equals(key) && killId.equals(skuId)) { //3、验证购物数量是否合理和库存量是否充足 Integer seckillLimit = redisTo.getSeckillLimit(); //获取信号量 String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode); Integer count = Integer.valueOf(seckillCount); //判断信号量是否大于0,并且买的数量不能超过库存 if (count > 0 && num <= seckillLimit && count > num ) { //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId //SETNX 原子性处理 String redisKey = user.getId() + "-" + skuId; //设置自动过期(活动结束时间-当前时间) Long ttl = endTime - currentTime; Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS); if (aBoolean) { //占位成功说明从来没有买过,分布式锁(获取信号量-1) RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode); //TODO 秒杀成功,快速下单 boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS); //保证Redis中还有商品库存 if (semaphoreCount) { //创建订单号和订单信息发送给MQ // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右 String timeId = IdWorker.getTimeId(); SeckillOrderTo orderTo = new SeckillOrderTo(); orderTo.setOrderSn(timeId); orderTo.setMemberId(user.getId()); orderTo.setNum(num); orderTo.setPromotionSessionId(redisTo.getPromotionSessionId()); orderTo.setSkuId(redisTo.getSkuId()); orderTo.setSeckillPrice(redisTo.getSeckillPrice()); rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo); long s2 = System.currentTimeMillis(); log.info("耗时..." + (s2 - s1)); return timeId; } } } } } long s3 = System.currentTimeMillis(); log.info("耗时..." + (s3 - s1)); return null; }
创建秒杀消息队列
/** * 商品秒杀队列 * @return */ @Bean public Queue orderSecKillOrrderQueue() { Queue queue = new Queue("order.seckill.order.queue", true, false, false); return queue; } @Bean public Binding orderSecKillOrrderQueueBinding() { //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; }
消息接收监听队列
@Slf4j @Component @RabbitListener(queues = "order.seckill.order.queue") public class OrderSeckillListener { @Autowired private OrderService orderService; @RabbitHandler public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException { log.info("准备创建秒杀单的详细信息..."); try { orderService.createSeckillOrder(orderTo); channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } catch (Exception e) { channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); } } }
创建秒杀订单
/** * 创建秒杀单 * @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); //保存商品的spu信息 R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId()); if (spuInfo.getCode() == 0) { SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {}); //保存订单项信息 OrderItemEntity orderItem = new OrderItemEntity(); orderItem.setOrderSn(orderTo.getOrderSn()); orderItem.setRealAmount(totalPrice); orderItem.setSkuQuantity(orderTo.getNum()); orderItem.setSpuId(spuInfoData.getId()); orderItem.setSpuName(spuInfoData.getSpuName()); orderItem.setSpuBrand(spuInfoData.getBrandName()); orderItem.setCategoryId(spuInfoData.getCatalogId()); //保存订单项数据 orderItemService.save(orderItem); } }
统一响应实体
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.TypeReference; import org.apache.http.HttpStatus; import java.util.HashMap; import java.util.Map; /** * 返回数据 * */ public class R extends HashMap<String, Object> { private static final long serialVersionUID = 1L; public R setData(Object data) { put("data",data); return this; } //利用fastjson进行反序列化 public <T> T getData(TypeReference<T> typeReference) { Object data = get("data"); //默认是map String jsonString = JSON.toJSONString(data); T t = JSON.parseObject(jsonString, typeReference); return t; } //利用fastjson进行反序列化 public <T> T getData(String key,TypeReference<T> typeReference) { Object data = get(key); //默认是map String jsonString = JSON.toJSONString(data); T t = JSON.parseObject(jsonString, typeReference); return t; } public R() { put("code", 0); put("msg", "success"); } public static R error() { return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员"); } public static R error(String msg) { return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg); } public static R error(int code, String msg) { R r = new R(); r.put("code", code); r.put("msg", msg); return r; } public static R ok(String msg) { R r = new R(); r.put("msg", msg); return r; } public static R ok(Map<String, Object> map) { R r = new R(); r.putAll(map); return r; } public static R ok() { return new R(); } public R put(String key, Object value) { super.put(key, value); return this; } public Integer getCode() { return (Integer) this.get("code"); } }
参考链接
-
【谷粒商城】分布式事务与下单
https://blog.csdn.net/hancoder/article/details/114983771
-
全网最强电商教程《谷粒商城》对标阿里P6/P7,40-60万年薪
https://www.bilibili.com/video/BV1np4y1C7Yf?p=284
-
mall源码工程
https://github.com/CharlesKai/mall