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>