电商项目实战之商品秒杀

  • 定时任务
    • corn表达式
    • 实现方式
      • 基于注解
      • 基于接口
    • 实战
  • 秒杀系统
    • 秒杀系统关注问题
    • 秒杀架构设计
    • 商品上架
    • 获取当前秒杀商品
    • 获取当前商品的秒杀信息
    • 秒杀最终处理
  • 参考链接

定时任务

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 {@Mapperpublic interface CronMapper {@Select("select cron from cron limit 1")public String getCron();}@Autowired      //注入mapper@SuppressWarnings("all")CronMapper cronMapper;/*** 执行定时任务.*/@Overridepublic 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//@Componentpublic 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分布式信号量 -> 秒杀服务

    1. 独立部署:独立部署秒杀模块gulimall-seckill;

    2. 定时任务:每天三点上架最新秒杀商品,削减高峰期压力;

    3. 秒杀链接加密:为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口;

    4. 库存预热:先从数据库中扣除一部分库存以 redisson 信号量的形式存储在redis中

    5. 队列削峰:秒杀成功后立即返回,然后以发送消息的形式创建订单

  • redis数据存储设计

    秒杀活动:存储于scekill:sesssions这个redis-key里,value为 skuIds[]

    秒杀活动里具体商品项:是一个map,redis-key是seckill:skus,map-key是skuId+商品随机码

  • redis存储模型设计

    1. 秒杀场次存储的List可以当做hash key在SECKILL_CHARE_PREFIX中获得对应的商品数据;

    2. 随机码防止人在秒杀一开放就秒杀完,必须携带上随机码才能秒杀

    3. 结束时间;

    4. 设置分布式信号量,这样就不会每个请求都访问数据库。seckill:stock:#(商品随机码);

    5. session里存了session-sku[]的列表,而seckill:skus的key也是session-sku,不要只存储sku,不能区分场次

  • 案例代码设计

    Redis中存放的skuInfo的信息

    @Datapublic 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)的Listprivate final String SESSION_CACHE_PREFIX = "seckill:sessions:";// 存储的秒杀商品数据// K: 固定值SECKILL_CHARE_PREFIX// V: hash,k为sessionId(活动场次id)+"-"+skuId(商品id),v为对应的商品信息SeckillSkuRedisToprivate 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());

商品上架

  • 定时上架

    1. 开启对定时任务支持

      @EnableAsync //开启对异步的支持,防止定时任务之间相互阻塞@EnableScheduling //开启对定时任务的支持@Configurationpublic class ScheduledConfig {}
    2. 每天凌晨三点远程调用coupon(优惠券)服务上架最近三天的秒杀商品;

    3. 由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法

    4. 上架后无需再次上架,用分布式锁做好幂等性

      /*** 秒杀商品定时上架*  每天晚上3点,上架最近三天需要三天秒杀的商品*  当天00:00:00 - 23:59:59*  明天00:00:00 - 23:59:59*  后天00:00:00 - 23:59:59*/@Slf4j@Servicepublic class SeckillScheduled {@Autowiredprivate SeckillService seckillService;@Autowiredprivate 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@Servicepublic class SeckillServiceImpl implements SeckillService {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate CouponFeignService couponFeignService;@Autowiredprivate ProductFeignService productFeignService;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate 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:";    //+商品随机码@Overridepublic 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);}}}
  • 获取最近三天的秒杀信息

    1. 获取最近三天的秒杀场次信息通过秒杀场次id查询对应的商品信息

    2. 防止集群多次上架

    @Overridepublic 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表中关联的skuIdList<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中的keyString key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;//判断Redis中是否有该信息,如果没有才进行添加Boolean hasKey = redisTemplate.hasKey(key);//缓存活动信息if (!hasKey) {//获取到活动中所有商品的skuIdList<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操作,绑定hashBoundHashOperations<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")@Overridepublic 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_1594453242000String 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*/@Overridepublic SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {//1、找到所有需要秒杀的商品的key信息---seckill:skusBoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);//拿到所有的keySet<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*/@Overridepublic SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {SkuItemVo skuItemVo = new SkuItemVo();CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {//1、sku基本信息的获取  pms_sku_infoSkuInfoEntity 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_descSpuInfoDescEntity 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_imagesCompletableFuture<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;}

秒杀最终处理

  • 秒杀流程图示

  • 秒杀业务

    1. 点击立即抢购时,会发送请求;

    2. 秒杀会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单

  • 秒杀方案

  • 消息队列

  • 样例源码

    创建订单发消息

    /*** 当前商品进行秒杀(秒杀开始)* @param killId* @param key* @param num* @return*/@Overridepublic 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、效验随机码和商品idString 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*/@Beanpublic Queue orderSecKillOrrderQueue() {Queue queue = new Queue("order.seckill.order.queue", true, false, false);return queue;}@Beanpublic Binding orderSecKillOrrderQueueBinding() {//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;}

    消息接收监听队列

    @Slf4j@Component@RabbitListener(queues = "order.seckill.order.queue")public class OrderSeckillListener {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic 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*/@Overridepublic 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");    //默认是mapString 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);   //默认是mapString 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

电商项目实战之商品秒杀相关推荐

  1. java spu sku_SpringBoot电商项目实战 — 商品的SPU/SKU实现

    最近事情有点多,所以系列文章已停止好多天了.今天我们继续Springboot电商项目实战系列文章.到目前为止,整个项目的架构和基础服务已经全部实现,分布式锁也已经讲过了.那么,现在应该到数据库设计及代 ...

  2. 电商项目实战第五节: CSS3+HTML5+JS 设计案例【考拉海购网站】之【商品栏及右侧垂直导航】

    上一节:电商项目实战第四节: CSS3+HTML5+JS 设计案例[考拉海购网站]之[轮播图特效] 文章目录 [考拉海购网站]之[商品栏及右侧垂直导航] 第一步,页面布局分布情况分析 第二步,根据页面 ...

  3. 软件测试电商项目实战(写进简历没问题)

    前言 说实话,在找项目的过程中,我下载过(甚至付费下载过)N多个项目.联系过很多项目的作者,但是绝大部分项目,在我看来,并不适合你拿来练习,它们或多或少都存在着"问题",比如: 1 ...

  4. 电商项目实战之分布式事务解决方案

    电商项目实战之分布式事务解决方案 本地事务 事务隔离级别 事务传播机制 分布式事务 CAP理论 选举与同步理论 BASE理论 解决方案 2PC模式(XA事务) 柔性事务-TCC事务补偿型方案 柔性事务 ...

  5. 400集高并发分布式超级电商项目实战

    带走一盏渔火 让他温暖我的双眼 留下一段真情 让它停泊在枫桥边 久违的你 一定保存着那套网盘 许多年以后 躺在网盘里的视频 依然尘封未动 涛声依旧不见当初的夜晚 今天的你我 怎样重复昨天的故事 涛哥说 ...

  6. 电商项目实战第一节: CSS3+HTML5+JS 设计案例【考拉海购网站】之【顶部导航】

    文章目录 [考拉海购网站]之[顶部导航] 第一步,分析布局 第二步,建立基本的文本目录及文件 第三步,根据第一步对导航栏的分析,在html代码里面补全需要的标签 index.html文件代码 第四步, ...

  7. 前端电商项目实战,如何从 0 开始创造一个【考拉海购官网】?( 共6节教程 )

    文章目录 声明 一,关于页面还原度效果比较 二,第一组演示图是 考拉海购官网的 三,第二组演示图是 本次教程从0开发的 四,教程目录(共6节) 五,全部代码下载地址 新手提示 (1)如何从github ...

  8. 电商项目实战第三节: CSS3+HTML5+JS 设计案例【考拉海购网站】之【分类导航栏】

    上一节:电商项目实战第二节: CSS3+HTML5+JS 设计案例[考拉海购网站]之[搜索框那一栏] 文章目录 [考拉海购网站]之[分类导航栏] 第一步,分析页面布局 第二步,写需要的html标签 i ...

  9. 电商项目实战第六节: CSS3+HTML5+JS 设计案例【考拉海购网站】之【页底信息,网站备案信息】

    上一节:电商项目实战第五节: CSS3+HTML5+JS 设计案例[考拉海购网站]之[商品栏及右侧垂直导航] 文章目录 电商项目实战第六节: CSS3+HTML5+JS 设计案例[考拉海购网站]之[页 ...

最新文章

  1. 每天都用ArrayList,你读过它的源码么?
  2. 强化学习AI:它菜了,我慌了
  3. go的25个关键字(保留字)和36个预定标识符
  4. 构建增强现实移动应用程序的六款顶级工具
  5. PHP中foreach遍历循环的使用(两种用法)
  6. java读取mysql配置文件_MySql主从复制,从原理到实践
  7. python贪吃蛇手机版代码_Python贪吃蛇简单的代码
  8. java doc、docx、pdf格式互转
  9. matlab log函数
  10. 红包小游戏php源码,H5抢红包 小游戏源码
  11. c#调用labview实现巴特沃斯滤波器
  12. 测试的阿萨德萨达阿萨德
  13. php语言标记可用什么符号,【单选题】不可用作PHP语言标记用的是什么( )符号 A. ? B. 〈php C. ?...
  14. Matlab_R2016a 中文破解版 安装教程
  15. 架构师的工作都干些什么?!想做架构师必看!
  16. MATLAB 像素画绘制APP
  17. os.listdir(path)
  18. 【已解决】问题:打开Chrome显示2345浏览器而不是Google浏览器
  19. 谈谈最近管理情绪和时间的心得:真的是破心中贼难
  20. Machine Learning Approach to RF Transmitter Identification

热门文章

  1. 公钥 私钥 数字签名 CA证书
  2. 聊聊 C++ 和 C# 中的 lambda 玩法
  3. 昆山苏南交易批发市场_库存管理系统项目总结
  4. 网易微专业python爬虫工程师_ai工程师 自然语言处理
  5. 通付盾受邀出席区块链技术和应用峰会暨第五届中国区块链开发大赛成果发布会及颁奖仪式
  6. 【Java】简易视频播放器
  7. 用java制作小游戏:小恐龙跑酷
  8. 【新梦想学员干货】必看!年薪30W的软件测试“老司机”工作经验。
  9. 利用word分词通过计算词的语境来获得相关词
  10. xss for u7 BOM