谷粒商城-高级篇-秒杀业务

1、后台添加秒杀商品


1、配置网关

- id: coupon_route
      uri: lb://gulimall-coupon
      predicates:
        - Path=/api/coupon/**
      filters:
        - RewritePath=/api/(?<segment>.*),/$\{segment}

2、每日秒杀关联商品功能实现

点击关联商品后,应该查询当前场次的所有商品

点击关联商品的时候,会弹出一个页面,并且F12可以看到会调用一个url请求:

http://localhost:88/api/coupon/seckillskurelation/list?t=1716706075726&page=1&limit=10&key=&promotionSessionId=1

根据此url去完善该接口

修改“com.atguigu.gulimall.coupon.service.impl.SeckillSkuRelationServiceImpl”代码如下:

@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
    QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
    // 场次id不是null
    String promotionSessionId = (String) params.get("promotionSessionId");
    if (!StringUtils.isEmpty(promotionSessionId)) {
        queryWrapper.eq("promotion_session_id", promotionSessionId);
    }
    IPage<SeckillSkuRelationEntity> page = this.page(new Query<SeckillSkuRelationEntity>().getPage(params), queryWrapper);

    return new PageUtils(page);
}
}

2、搭建秒杀服务环境


1、导入pom.xml依赖

4.0.0

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-seckill</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-seckill</name>
    <description>秒杀</description>
 
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>
 
    <dependencies>
        <!--以后使用redis.client作为所有分布式锁,分布式对象等功能框架-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
 
        <dependency>
            <groupId>com.auguigu.gulimall</groupId>
            <artifactId>gulimall-commom</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
 
</project>

2、添加配置

spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=192.168.119.127

3、主启动类添加注解

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
    SpringApplication.run(GulimallSeckillApplication.class, args);
}
}


3、定时任务

3.1、cron 表达式

语法:秒 分 时 日 月 周 年(Spring 不支持)

http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

特殊字符:


3.2、SpringBoot 整合定时任务


springboot整合定时任务流程:

定时任务:

1、@EnableScheduling 开启定时任务

2、@Scheduled 开启一个定时任务

3、自动配置类 TaskSchedulingAutoConfiguration

异步任务 :

1、@EnableAsync 开启异步任务功能

2、@Async 给希望异步执行的方法上标注

3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在TaskExecutionProperties

添加“com.atguigu.gulimall.seckill.scheduled.HelloSchedule”类,代码如下:

@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
 * 1、Spring中6位组成,不允许7位d的年
 * 2、周的位置,1-7代表周一到周日
 * 3、定时任务不应该阻塞。默认是阻塞的
 *      1)、可以让业务运行以异步的方式,自己提交到线程池
 *      2)、支持定时任务线程池;设置TaskSchedulingProperties;
 *              spring.task.scheduling.pool.size=5
 *      3)、让定时任务异步执行,自己提交到线程池
 *          异步任务
 *
 *      解决:使用异步任务来完成定时任务不阻塞的功能@Async
 */						定时任务+异步任务,实现异步任务不阻塞
@Async
@Scheduled(cron = "*/5 * * * * ?")
public void hello() throws InterruptedException {
    log.info("hello......");
    Thread.sleep(3000);
}
}

配置定时任务参数

spring.task.execution.pool.core-size=20
spring.task.execution.pool.max-size=50

4、秒杀商品上架

4.1、秒杀商品上架思路

项目独立部署,独立秒杀模块gulimall-seckill
使用定时任务每天三点上架最新秒杀商品,削减高峰期压力

4.2、秒杀商品上架流程

4.3、存储模型设计


1、查询秒杀活动场次和sku信息的存储模型

添加“com.atguigu.gulimall.seckill.vo.SeckillSessionWithSkus”类,代码如下:

package com.atguigu.gulimall.seckill.vo;
 
import lombok.Data;
 
import java.util.Date;
import java.util.List;
 
@Data
public class SeckillSessionWithSkus {
    private Long id;
    /**
     * 场次名称
     */
    private String name;
    /**
     * 每日开始时间
     */
    private Date startTime;
    /**
     * 每日结束时间
     */
    private Date endTime;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 创建时间
     */
    private Date createTime;
 
    private List<SeckillSkuVo> relationEntities;
 
}

2、查询秒杀活动商品关联的存储模型

添加“com.atguigu.gulimall.seckill.vo.SeckillSkuVo”类,代码如下:

package com.atguigu.gulimall.seckill.vo;
 
import lombok.Data;
 
import java.math.BigDecimal;
 
@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;
 
}

3、查询商品信息的存储模型

添加“com.atguigu.gulimall.seckill.vo.SkuInfoVo”类,代码如下:

package com.atguigu.gulimall.seckill.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class SkuInfoVo {
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;
}

4、缓存获得秒杀活动场次和sku信息的存储模型

添加"com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo"类,代码如下:

package com.atguigu.gulimall.seckill.to;
 
import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;
 
import java.math.BigDecimal;
 
@Data
public class SeckillSkuRedisTo {
    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;
    //以上都为SeckillSkuRelationEntity的属性
 
    //skuInfo
    private SkuInfoVo skuInfo;
 
    //当前商品秒杀的开始时间
    private Long startTime;
 
    //当前商品秒杀的结束时间
    private Long endTime;
 
    //当前商品秒杀的随机码
    private String randomCode;
}

4.4、定时上架


配置定时任务

添加“com.atguigu.gulimall.seckill.config.ScheduledConfig”类,代码如下:

@EnableAsync // 开启对异步的支持,防止定时任务之间相互阻塞
@EnableScheduling // 开启对定时任务的支持
@Configuration
public class ScheduledConfig {
}

每天凌晨三点远程调用coupon服务上架最近三天的秒杀商品

添加“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,代码如下

@Slf4j
@Component
public class SeckillSkuScheduled {
@Autowired
private SeckillService seckillService;

/**
 * TODO 幂等性处理
 * 上架最近三天的秒杀商品
 */
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days() {
    // 重复上架无需处理
    log.info("上架秒杀的信息......");
    seckillService.uploadSeckillSkuLatest3Days();
}
}

添加“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下

public interface SeckillService {
    void uploadSeckillSkuLatest3Days();
}

添加“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下

@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
private CouponFeignService couponFeignService;

@Autowired
private ProductFeignService productFeignService;

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Autowired
private 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、扫描最近三天需要参与秒杀的活动的场次和sku信息
    R session = couponFeignService.getLasts3DaySession();
    if (session.getCode() == 0){
        // 上架商品
        List<SeckillSessionWithSkus> data = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
        });
        // 2、缓存到redis

        // 2.1、缓存活动信息
        saveSessionInfos(data);

        // 2.2、缓存获得关联商品信息
        saveSessionSkuInfos(data);
    }
}
}

4.4.1、获取最近三天的秒杀信息

思路:
1.获取最近三天的秒杀场次信息,

先获取今天的日期,通过LocalDate()

获取今天的最大日期和最小日期LocalTime.Min和Max

拼接获取到要查询的时间。

2.再通过秒杀场次id查询对应的商品信息

遍历场次获取商品信息(通过关联表@TableField(exist=false))

添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:

@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@GetMapping("/coupon/seckillsession/lasts3DaySession")
R getLasts3DaySession();
}

gulimall-coupon

修改“com.atguigu.gulimall.coupon.controller.SeckillSessionController”类,代码如下:

/**
 * 获取最近3天的秒杀商品
 */
@GetMapping("/lasts3DaySession")
public R getLasts3DaySession(){
    List<SeckillSessionEntity> session = seckillSessionService.getLasts3DaySession();
    return R.ok().setData(session);
}

添加“com.atguigu.gulimall.coupon.service.SeckillSessionService”类,代码如下:

/**
 * 获取最近3天的秒杀商品
 *
 * @return
 */
List<SeckillSessionEntity> getLasts3DaySession();

添加“com.atguigu.gulimall.coupon.service.impl.SeckillSessionServiceImpl”类,代码如下:

在查询近三天的秒杀场次时时,同时把关联的消息<List>存储进去。

包含的字段:

商品id

秒杀价格

秒杀数量

秒杀限制数量

秒杀排序

@Override
public List<SeckillSessionEntity> getLasts3DaySession() {
    // 计算最近三天
    List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
    if (!CollectionUtils.isEmpty(list)) {
        return list.stream().map(session -> {
            Long id = session.getId();
            List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
            session.setRelationEntities(relationEntities);
            return session;
        }).collect(Collectors.toList());
    }
    return null;
}

/**
 * 起始时间
 *
 * @return
 */
private String startTime() {
    LocalDate now = LocalDate.now();
    LocalTime time = LocalTime.MIN;
    LocalDateTime start = LocalDateTime.of(now, time);
    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);
    LocalTime time = LocalTime.MIN;
    LocalDateTime end = LocalDateTime.of(localDate, time);
    String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    return format;
}

4.4.2、在redis中缓存秒杀活动信息

以key,value的方式存储在redis

private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";

key:(key为 seckill:sessions:+时间范围)

SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;

value:(值为 场次id_商品id<List>)

item.getPromotionSessionId().toString() + "_" + item.getSkuId().toString()

    /**
     * 缓存秒杀活动信息
     *
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            if (!hasKey) {
                List<String> collect = session.getRelationEntities()
                        .stream()
                        .map(item -> item.getPromotionSessionId().toString() + "_" + item.getSkuId().toString())
                        .collect(Collectors.toList());
                System.out.println("saveSessionInfos------------------------" + collect);
                // 缓存活动信息(list操作)
                stringRedisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }

4.4.3、在redis中缓存获得关联秒杀活动的商品信息

实现思路:

1.

   /**
     * 缓存获得关联秒杀的商品信息
     *
     * @param List<SeckillSessionWithSkus> 
     */
    private void saveSessionSkuInfos(List<SeckillSessionWithSkus>  sessions) {         // 准备hash操作         
    BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);         
    sessions.stream().
    forEach(session -> {             session.getRelationEntities().stream().forEach(seckillSkuVo -> {                 if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())) {                     // 缓存商品                     SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
    
                // 1、sku的基本信息
                R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if (0 == r.getCode()) {
                    SkuInfoVo skuInfo = r.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、随机码
                String token = UUID.randomUUID().toString().replace("_", "");
                redisTo.setRandomCode(token);
                
                // 如果当前这个场次的商品的库存信息已经上架就不需要上架
                // 5、使用库存作为分布式信号量 ==》限流
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                // 5.1、商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                String jsonString = JSON.toJSONString(redisTo);
                log.info("saveSessionSkuInfos------------------------" + jsonString);
                ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);
            }
        });
    });
}
    /**
     * 缓存秒杀活动所关联的商品信息
     * @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);

                    //序列化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());
                }
            });
        });
    }

添加“com.atguigu.gulimall.seckill.fegin.ProductFeignService”类,代码如下:

@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);
}

4.5、幂等性保证

定时任务-分布式下的问题

由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法
分布式锁:锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态


修改“com.atguigu.gulimall.seckill.scheduled.SeckillSkuScheduled”类,代码如下:

@Slf4j
@Component
public class SeckillSkuScheduled {
@Autowired
private SeckillService seckillService;

@Autowired
private RedissonClient redissonClient;

private final String upload_lock = "seckill:upload:lock";

/**
 * 上架最近3天的秒杀商品
 * 幂等性处理
 */
@Scheduled(cron = "*/3 0 0 * * ?")
public void uploadSeckillSkuLatest3Days() {
    // 重复上架无需处理
    log.info("上架秒杀的信息......");
    // 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态
    RLock lock = redissonClient.getLock(upload_lock);
    lock.lock(10, TimeUnit.SECONDS);
    try {
        seckillService.uploadSeckillSkuLatest3Days();
    } finally {
        lock.unlock();
    }

}
}

5、获取当前秒杀商品

5.1、获取到当前可以参加秒杀的商品信息

添加“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下

@Controller
public class SeckillController {
@Autowired
private SeckillService seckillService;

/**
 * 获取到当前可以参加秒杀的商品信息
 *
 * @return
 */
@ResponseBody
@GetMapping(value = "/getCurrentSeckillSkus")
public R getCurrentSeckillSkus() {
    List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
    return R.ok().setData(vos);
}
}

添加“com.atguigu.gulimall.seckill.service.SeckillService”类:代码如下:

 List getCurrentSeckillSkus(); 

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

获取当前秒杀场次的商品

逻辑:

1. 查询到所有的场次信息,场次的key的格式为==>前缀:开始时间_结束时间

   @Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
    // 获取秒杀活动场次的的所有key
    Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
    long currentTime = System.currentTimeMillis();
    for (String key : keys) {
        String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
        String[] split = replace.split("_");
        long startTime = Long.parseLong(split[0]);
        long endTime = Long.parseLong(split[1]);
        // 当前秒杀活动处于有效期内
        // 幂等性判断
        if (currentTime > startTime && currentTime < endTime) {
            // 获取这个秒杀场次的所有商品信息
            // 遍历每个场次,拿到每个场次的所有值,范围在-100, 100
            // 值的格式为 场次id_商品id
            List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
            
            /*
              private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:"; //秒杀活动信息
              private final String SKUKILL_CACHE_PREFIX = "seckill:skus:"; // 秒杀商品信息
              private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码
            */
            // 获取SKUKILL_CACHE_PREFIX 秒杀活动的所有信息
            BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            assert range != null;
            // Has 存储SKUKILL_CACHE_PREFIX中 通过场次id_商品id获取商品的具体信息
            List<String> strings = hashOps.multiGet(range);
            if (!CollectionUtils.isEmpty(strings)) {
            // 将获取到的商品信息转换成SeckillSkuRedisTo类型
                return strings.stream().map(item -> JSON.parseObject(item, SeckillSkuRedisTo.class))
                        .collect(Collectors.toList());
            }
            break;
        }
    }
    return null;
}

5.2、首页获取并拼装数据

1、配置网关

    - id: gulimall_seckill_route
      uri: lb://gulimall-seckill
      predicates:
        - Host=seckill.gulimall.com

2、配置域名

#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com
192.168.119.127 cart.gulimall.com
192.168.119.127 order.gulimall.com
192.168.119.127 member.gulimall.com
192.168.119.127 seckill.gulimall.com


3、修改gulimall-product模块的index.html页面,代码如下:

    <div class="swiper-container swiper_section_second_list_left">
        <div class="swiper-wrapper">
            <div class="swiper-slide">
                <!-- 动态拼装秒杀商品信息 -->
                <ul id="seckillSkuContent"></ul>

            </div>

4、首页效果展示

6、商品详情页获取当前商品的秒杀信息

修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

@Override
public SkuItemVo item(Long skuId) {
    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> imageFuture = CompletableFuture.runAsync(() -> {
        // 2、sku的图片信息      pms_sku_images
        List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);
    }, 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的介绍 pms_spu_info_desc
        SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
        skuItemVo.setDesc(desc);
    }, executor);

    CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
        // 5、获取spu的规格参数信息
        List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
        skuItemVo.setGroupAttrs(attrGroupVos);
    }, executor);

    CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
        // 6、查询当前sku是否参与秒杀优惠
        R r = seckillFeignService.getSkuSecKillInfo(skuId);
        if (0 == r.getCode()) {
            SeckillInfoVo skillInfo = r.getData(new TypeReference<SeckillInfoVo>() {
            });
            skuItemVo.setSeckillInfo(skillInfo);
        }
    }, executor);
    // 等待所有任务执行完成
    try {
        CompletableFuture.allOf(saleAttrFuture, imageFuture, descFuture, baseAttrFuture, seckillFuture).get();
    } catch (InterruptedException e) {
        log.error("1等待所有任务执行完成异常{}", e);
    } catch (ExecutionException e) {
        log.error("2等待所有任务执行完成异常{}", e);
    }
    return skuItemVo;
}

添加“com.atguigu.gulimall.product.vo.SeckillInfoVo”类,代码如下:

@Data
public class SeckillInfoVo {
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;

/**
 * 当前商品秒杀的开始时间
 */
private Long startTime;

/**
 * 当前商品秒杀的结束时间
 */
private Long endTime;

/**
 * 当前商品秒杀的随机码
 */
private String randomCode;
}

修改“com.atguigu.gulimall.product.vo.SkuItemVo”类,代码如下:

远程调用gulimall-seckill

添加“com.atguigu.gulimall.product.feign.SeckillFeignService”类,代码如下:

@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}


修改“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

/**
 * 获取秒杀商品的详情信息
 */
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSecKillInfo(@PathVariable("skuId") Long skuId){
    SeckillSkuRedisTo to = seckillService.getSkuSecKillInfo(skuId);
    return R.ok().setData(to);
}

修改“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下:

SeckillSkuRedisTo getSkuSecKillInfo(Long skuId);

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

查获取指定skuid的商品的秒杀信息

1.商品的存储格式为 场次id_商品id

2.通过正则表达式找到匹配的 String regx = "\\d_" + skuId;

3.处于秒杀时间段的商品设置随机码

@Override
public SeckillSkuRedisTo getSkuSecKillInfo(Long skuId) {
    // 1、获取所有需要参与秒杀的商品的key
    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    Set<String> keys = hashOps.keys();
    if (null != keys){
        // 1_10(正则表达式)
        String regx = "\\d_" + skuId;
        for (String key : keys) {
            // 匹配场次商品id
            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()){
                    skuRedisTo.setRandomCode(null);
                }
                return skuRedisTo;
            }

        }
    }
    return null;
}

修改gulimall-product模块的item.html页面,代码如下:

            <div class="box-summary clear">
                <ul>
                    <li>京东价</li>

                    <li>
                        <span>¥</span>

                        <span th:text="${#numbers.formatDecimal(item.info.price,3,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="${item.seckillInfo.startTime <= #dates.createNow().getTime() && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
                                    秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
                                </span>


                    </li>

                    <li>
                        <a href="/static/item/">
                            预约说明
                        </a>

                    </li>

                </ul>

            </div>

详情页效果展示:

7、登录检查

1.设置session域,

2.设置拦截器

1、pom引入SpringSession依赖和redis

    <!--整合SpringSession完成session共享问题-->
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>


    <!--引入redis-->
    <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>

2、在配置文件中添加SpringSession的保存方式

#SpringSession保存方式
spring.session.store-type=redis


3、SpringSession的配置

添加“com.atguigu.gulimall.seckill.config.GulimallSessionConfig”类,代码如下:

@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
    DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
    cookieSerializer.setDomainName("gulimall.com");
    cookieSerializer.setCookieName("GULISESSION");
    return cookieSerializer;
}

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
    return new GenericJackson2JsonRedisSerializer();
}
}

4、主启动类开启RedisHttpSession这个功能

@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
    SpringApplication.run(GulimallSeckillApplication.class, args);
}
}

添加用户登录拦截器

添加“com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor”类,代码如下:

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String requestURI = request.getRequestURI();
    AntPathMatcher matcher = new AntPathMatcher();
    boolean match = matcher.match("/kill", requestURI);
    // 如果是秒杀,需要判断是否登录,其他路径直接放行不需要判断
    if (match) {
        MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute != null){
            loginUser.set(attribute);
            return true;
        }else {
            //没登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
    return true;
}
}

把拦截器配置到spring中,否则拦截器不生效。
添加addInterceptors表示当前项目的所有请求都要讲过这个拦截请求

添加“com.atguigu.gulimall.seckill.config.SeckillWebConfig”类,代码如下:

@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}


修改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}]];//true
        if (isLogin) {
            var killId = $(this).attr("sessionId") + "_" + $(this).attr("skuId");
            var key = $(this).attr("code");
            var num = $("#numInput").val();
            location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + key + "&num=" + num;
        } else {
            alert("秒杀请先登录!");
        }
 
        return false;
    });

8、秒杀系统设计

8.1、秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)+ 独立部署。
限流方式:
1.前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
2.nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
3.网关限流,限流的过滤器
4.代码中使用分布式信号量
5.abbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。

8.2、秒杀架构


秒杀架构思路

项目独立部署,独立秒杀模块gulimall-seckill
使用定时任务每天三点上架最新秒杀商品,削减高峰期压力
秒杀链接加密,为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口
库存预热,先从数据库中扣除一部分库存以redisson信号量的形式存储在redis中
队列削峰,秒杀成功后立即返回,然后以发送消息的形式创建订单
秒杀架构图

8.3、秒杀( 高并发) 系统关注的问题

8.4、秒杀流程


秒杀流程图一

秒杀流程图二

我们使用秒杀流程图二来实现功能

8.5、代码实现

8.5.1、秒杀接口


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

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

添加“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

* @param killId 秒杀商品id

* @param key 随机码

* @param num 数量

@GetMapping("/kill")
public R seckill(@RequestParam("killId") String killId,
                 @RequestParam("key") String key,
                 @RequestParam("num") Integer num){
    // 1、判断是否登录(登录拦截器已经自动处理)

    String orderSn = seckillService.kill(killId, key, num);
    return R.ok().setData(orderSn);
}

修改“com.atguigu.gulimall.seckill.service.SeckillService”类,代码如下:

/**
 * 秒杀
 *
 * @param killId 秒杀商品id
 * @param key 随机码
 * @param num 数量
 * @return
 */
String kill(String killId, String key, Integer num);

使用队列削峰 做流量削峰

引入rabbitMQ依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

在配置文件中添加rabbitMQ的配置

#RabbitMQ的地址
spring.rabbitmq.host=192.168.119.127
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
配置RabbitMQ(消息确认机制暂未配置)

添加“com.atguigu.gulimall.seckill.config.MyRabbitConfig”类,代码如下:

@Configuration
public class MyRabbitConfig {
    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
 
}

主启动类不用启动//@EnableRabbit 不用监听RabbitMQ, 因为我们只用来发送消息,不接收消息

重要

修改“com.atguigu.gulimall.seckill.service.impl.SeckillServiceImpl”类,代码如下:

// TODO 上架秒杀商品的时候,每一个数据都有一个过期时间。
// TODO 秒杀后续的流程,简化了 收货地址等信息。
// String killId 商品id, String key 随机码, Integer num 购买数量
@Override
public String kill(String killId, String key, Integer num) {
    long s1 = System.currentTimeMillis();
    // 0、从拦截器获取用户信息
    MemberResponseVO repsVo = LoginUserInterceptor.loginUser.get();
    // 1、获取当前商品的详细信息
    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
    // killId格式为 场次id_商品id
    String json = hashOps.get(killId);
    if (!StringUtils.isEmpty(json)){
        // 拿到商品的秒杀信息
        SeckillSkuRedisTo redis = JSON.parseObject(json, SeckillSkuRedisTo.class);
        // 2、校验合法性
        Long startTime = redis.getStartTime();
        Long endTime = redis.getEndTime();
        long current = new Date().getTime();
        long ttl = endTime - startTime; // 场次存活时间
        // 2.1、校验时间的合法性
        if (current >= startTime && current <= endTime){
            // 2.2、校验随机码和商品id
            String randomCode = redis.getRandomCode();
            String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
            // 判断随机码 和 商品id是否匹配
            if (randomCode.equals(key) && skuId.equals(killId)){
                // 2.3、验证购物的数量是否合理,小于秒杀限制量
                if (num <= redis.getSeckillLimit()){
                    // 2.4、验证这个人是否购买过。
                    // 幂等性处理。如果只要秒杀成功,就去占位  
                    // userId_sessionId_skillId
                    // SETNX
                    String redisKey = repsVo.getId() + "_" + skuId;
                    // 2.4.1、自动过期--
                    // 通过在redis中使用 用户id_skuId 来占位看是否买过
                    // 存储的是购买数量
                    Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (ifAbsent){
                        // 2.5、占位成功,说明该用户未秒杀过该商品,则继续尝试获取库存信号量
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        // 尝试消耗 num个信号量
                        boolean b = semaphore.tryAcquire(num);
                        if (b){
                            // 秒杀成功
                            // 2.6、快速下单发送MQ消息 10ms
                            String orderSn = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(orderSn);
                            orderTo.setMemberId(repsVo.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);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2-s1));
                            return orderSn;
                        }
                        return null;

                    }else {
                        // 2.4.2、说明已经买过
                        return null;
                    }

                }

            }else {
                return null;
            }
        }else {

            return null;
        }

    }
    return null;
}

新建“com.atguigu.common.to.mq.SeckillOrderTo”类,代码如下:

@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;
}

8.5.2、创建订单


gulimall-order

1、创建秒杀队列,并绑定队列到订单交换机

修改“com.atguigu.gulimall.order.config.MyMQConfig”类,代码如下:

/**
 * 订单秒杀队列
 */
@Bean
public Queue orderSeckillOrderQueue() {
    return new Queue("order.seckill.order.queue", true, false, false);
}

/**
 * 绑定订单秒杀队列
 */
@Bean
public Binding orderSeckillOrderQueueBinding() {
    return new Binding("order.seckill.order.queue",
            Binding.DestinationType.QUEUE,
            "order-event-exchange",
            "order.seckill.order",
            null);
}

2、监听消息队列

添加“com.atguigu.gulimall.order.listener.OrderSeckillListener”类,代码如下:

@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
@Autowired
private OrderService orderService;

@RabbitHandler
public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
    try {
        log.info("准备创建秒杀单的详细信息。。。");
        orderService.createSeckillOrder(seckillOrder);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        // 修改失败 拒绝消息 使消息重新入队
        channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
    }
}
}

3、创建秒杀订单

添加“com.atguigu.gulimall.order.service.OrderService”类,代码如下:

/**
 * 创建秒杀订单
 * 
 * @param seckillOrder
 */
void createSeckillOrder(SeckillOrderTo seckillOrder);

修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:

@Override
public void createSeckillOrder(SeckillOrderTo seckillOrder) {
    // TODO 1、保存订单信息
    OrderEntity orderEntity = new OrderEntity();
    orderEntity.setOrderSn(seckillOrder.getOrderSn());
    orderEntity.setMemberId(seckillOrder.getMemberId());
    orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));
    orderEntity.setPayAmount(multiply);
    this.save(orderEntity);
    // TODO 2、保存订单项信息
    OrderItemEntity entity = new OrderItemEntity();
    entity.setOrderSn(seckillOrder.getOrderSn());
    entity.setRealAmount(multiply);
    //TODO 3、获取当前sku的详细信息进行设置
    entity.setSkuQuantity(seckillOrder.getNum());

    orderItemService.save(entity);
}

8.5.3、秒杀页面完成


把gulimall-cart服务的成功页面放到gulimall-seckill服务里

修改里面的静态资源路径,我们借用购物车的资源,替换如下:

引入thymeleaf依赖

     <!--模板引擎 thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

在配置里关闭thymeleaf缓存

#关闭缓存

spring.thymeleaf.cache=false



修改“com.atguigu.gulimall.seckill.controller.SeckillController”类,代码如下:

@GetMapping("/kill")
public String seckill(@RequestParam("killId") String killId,
                 @RequestParam("key") String key,
                 @RequestParam("num") Integer num,
                 Model model){
    // 1、判断是否登录(登录拦截器已经自动处理)

    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.com/payOrder?orderSn='+orderSn}">去支付</a></h2>
 
                </div>
                <div th:if="${orderSn == null}">
                    <h1>手气不好,秒杀失败,下次再来</h1>
                </div>
            </div>
        </div>
    </div>
 
</div>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/944183.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JuOne核心模块揭秘:从智能硬件到Web3生态的完美连接

JuOne核心模块揭秘&#xff1a;从智能硬件到Web3生态的完美连接在全球数字经济的浪潮中&#xff0c;Web3 正以前所未有的速度重塑我们的生活方式、商业模式和价值创造体系。它不仅仅是互联网的下一阶段&#xff0c;更是一场关于未来的革命。去中心化、用户主权、价值互联&#…

Kafka高性能设计

高性能设计概述 Kafka高性能是多方面协同的结果&#xff0c;包括集群架构、分布式存储、ISR数据同步及高效利用磁盘和操作系统特性等。主要体现在消息分区、顺序读写、页缓存、零拷贝、消息压缩和分批发送六个方面。 消息分区 存储不受单台服务器限制&#xff0c;能处理更多数据…

若依框架之简历pdf文档预览功能

一、前端 &#xff08;1&#xff09;安装插件vue-pdf&#xff1a;npm install vue-pdf &#xff08;2&#xff09;引入方式&#xff1a;import pdf from "vue-pdf"; &#xff08;3&#xff09;components注入方式&#xff1a;components:{pdf} &#xff08;4&…

【社区投稿】自动特征auto trait的扩散规则

自动特征auto trait的扩散规则 公式化地概括&#xff0c;auto trait marker trait derived trait。其中&#xff0c;等号右侧的marker与derived是在Rustonomicon书中的引入的概念&#xff0c;鲜见于Rust References。所以&#xff0c;若略感生僻&#xff0c;不奇怪。 marker …

Elasticsearch检索之三:官方推荐方案search_after检索实现(golang)

Elasticsearch8.17.0在mac上的安装 Kibana8.17.0在mac上的安装 Elasticsearch检索方案之一&#xff1a;使用fromsize实现分页 快速掌握Elasticsearch检索之二&#xff1a;滚动查询(scrool)获取全量数据(golang) 1、search_after检索 在前面的文章介绍了fromsize的普通分页…

精读DeepSeek v3技术文档的心得感悟

最近宋大宝同学读完了DeepSeekv3的文档&#xff0c;心中颇多感慨&#xff0c;忍不住想在这里记录一下对这款“业界有望启示未来低精度训练走向”的开源大模型的观察与思考。DeepSeek v3的亮点绝不仅仅是“Float8”或“超长上下文”这么简单&#xff0c;而是贯穿了从数值精度、注…

WAV文件双轨PCM格式详细说明及C语言解析示例

WAV文件双轨PCM格式详细说明及C语言解析示例 一、WAV文件双轨PCM格式详细说明1. WAV文件基本结构2. PCM编码方式3. 双轨PCM格式详细说明二、C语言解析WAV文件的代码示例代码说明一、WAV文件双轨PCM格式详细说明 WAV文件是一种用于存储未压缩音频数据的文件格式,广泛应用于音频…

Day1 微服务 单体架构、微服务架构、微服务拆分、服务远程调用、服务注册和发现Nacos、OpenFeign

目录 1.导入单体架构项目 1.1 安装mysql 1.2 后端 1.3 前端 2.微服务 2.1 单体架构 2.2 微服务 2.3 SpringCloud 3.微服务拆分 3.1 服务拆分原则 3.1.1 什么时候拆 3.1.2 怎么拆 3.2 拆分购物车、商品服务 3.2.1 商品服务 3.2.2 购物车服务 3.3 服务调用 3.3.1 RestTemplate 3.…

DeepSpeed 使用 LoRA 训练后文件结构详解

DeepSpeed 使用 LoRA 训练后文件结构详解 在大语言模型&#xff08;LLM&#xff09;的训练过程中&#xff0c;DeepSpeed 提供了强大的分布式训练能力&#xff0c;而 LoRA&#xff08;Low-Rank Adaptation&#xff09;通过参数高效微调技术显著减少了资源占用。完成训练后&…

Llama 3 预训练(二)

目录 3. 预训练 3.1 预训练数据 3.1.1 网络数据筛选 PII 和安全过滤 文本提取与清理 去重&#xff08;De-duplication&#xff09; 启发式过滤&#xff08;Heuristic Filtering&#xff09; 基于模型的质量过滤 代码和数学推理数据处理 多语言数据处理 3.1.2 确定数…

Autoware Universe 安装记录

前提&#xff1a; ubuntu20.04&#xff0c;英伟达显卡。 ROS2-Galactic安装 wget http://fishros.com/install -O fishros && . fishros 选择galactic(ROS2)版本&#xff0c;桌面版 ROS2-dev-tools安装 sudo apt install python3-testresources sudo apt update …

【小程序】自定义组件的data、methods、properties

目录 自定义组件 - 数据、方法和属性 1. data 数据 2. methods 方法 3. properties 属性 4. data 和 properties 的区别 5. 使用 setData 修改 properties 的值 自定义组件 - 数据、方法和属性 1. data 数据 在小程序组件中&#xff0c;用于组件模板渲染的私有数据&…

socket编程(C++/Windows)

相关文章推荐&#xff1a; Socket 编程基础 面试官&#xff0c;不要再问我三次握手和四次挥手 TCP的三次握手与四次挥手 参考视频&#xff1a; https://www.bilibili.com/video/BV1aW4y1w7Ui/?spm_id_from333.337.search-card.all.click TCP通信流程 服务端 #include<…

linux自动化一键批量检查主机端口

1、准备 我们可以使用下面命令关闭一个端口 sudo iptables -A INPUT -p tcp --dport 端口号 -j DROP我关闭的是22端口&#xff0c;各位可以关其它的或者打开其它端口测试&#xff0c;谨慎关闭22端口&#xff01;不然就会像我下面一样握手超时&#x1f62d;&#x1f62d;&…

实验五 时序逻辑电路部件实验

一、实验目的 熟悉常用的时序逻辑电路功能部件&#xff0c;掌握计数器、了解寄存器的功能。 二、实验所用器件和仪表 1、双 D触发器 74LS74 2片 2、74LS162 1片 3、74194 1片 4、LH-D4实验仪 1台 1.双…

开源轻量级文件分享服务Go File本地Docker部署与远程访问

???欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老…

flask基础

from flask import Flask, requestapp Flask(__name__)# app.route(/) # def hello_world(): # put applications code here # return Hello World!app.route(/) # 路由 当用户访问特定 URL 时&#xff0c;Flask 会调用对应的视图函数来处理请求 def index():return …

WPF使用OpenCvSharp4

WPF使用OpenCvSharp4 创建项目安装OpenCvSharp4 创建项目 安装OpenCvSharp4 在解决方案资源管理器中&#xff0c;右键单击项目名称&#xff0c;选择“管理 NuGet 包”。搜索并安装以下包&#xff1a; OpenCvSharp4OpenCvSharp4.ExtensionsOpenCvSharp4.runtime.winSystem.Man…

社媒运营专线 - SD-WAN 跨境网络专线 —— 外贸企业社媒平台的专属 “快车道”

在当今全球化的商业浪潮中&#xff0c;社交媒体平台已成为外贸企业拓展国际市场、提升品牌知名度和促进业务增长的关键阵地。然而&#xff0c;网络访问速度慢、IP 不纯净等问题却如影随形&#xff0c;严重制约了企业社媒运营的效率和效果。幸运的是&#xff0c;社媒运营专线 - …

RustDesk内置ID服务器,Key教程

RustDesk内置ID服务器&#xff0c;Key教程 首先需要准备一个域名&#xff0c;并将其指定到你的 rustdesk 服务器 ip 地址上&#xff0c;这里编译采用的是Github Actions &#xff0c;说白了是就workflows&#xff0c;可以创建一些自动化的工作流程&#xff0c;例如代码的检查&a…