一、后台添加秒杀商品
1、SeckillSkuRelationController 控制层
/**
* 秒杀活动商品关联
*
* @author lianxiaoyu
* @email 376536577@qq.com
* @date 2021-06-04 16:44:55
*/
@RestController
@RequestMapping("coupon/seckillskurelation")
public class SeckillSkuRelationController {
@Autowired
private SeckillSkuRelationService seckillSkuRelationService;
/**
* 列表
* params 代表请求参数
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = seckillSkuRelationService.queryPage(params);
return R.ok().put("page", page);
}
}
2、实现类
根据活动场次id获取SeckillSkuRelationEntity信息
@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
//封装条件构造器
QueryWrapper<SeckillSkuRelationEntity> wrapper = new QueryWrapper<>();
//请求参数获取场次id
String promotionSessionId = (String) params.get("promotionSessionId");
if (!StringUtils.isEmpty(promotionSessionId)){
wrapper.eq("promotion_session_id",promotionSessionId);
}
IPage<SeckillSkuRelationEntity> page = this.page(
new Query<SeckillSkuRelationEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
}
二、创建秒杀服务
第1步:pom
<dependencies>
<dependency>
<groupId>com.lian.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</exclusion>
</exclusions>
</dependency>
<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>
<!--redis的 redisson 实现分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
<!--引入分布式session-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--引入rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
第2步:application配置
application.yaml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.56.10:3306/gulimall_seckill
username: root
password: root
application:
name: gulimall-seckill
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
#配置oss对象存储
#配置日期格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
#关闭thymeleaf缓存
thymeleaf:
cache: false
#配置redis
redis:
host: 192.168.56.10
port: 6379
#配置redis缓存类型
cache:
type: redis
redis:
time-to-live: 3600000 # 指定redis中的过期时间为1h
use-key-prefix: true #使用key前缀
cache-null-values: true #缓存空值,解决缓存穿透问题,缓存穿透是 数据库和缓存中不存在的数据
# alicloud:
# access-key: LTAI5t6PMA6dybm1iuVL6RXQ
# secret-key: EkkMyqaTwC3DgYWnSTuJOCPoR1kzJr
# oss:
# endpoint: oss-cn-beijing.aliyuncs.com
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
#配置逻辑删除,mybatisplus官网
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
server:
port: 25000
#配置日志打印
logging:
level:
com.lian.gulimall: debug
application.properties
#spring.task.scheduling.pool.size=10
#session中存储的类型
spring.session.store-type=redis
#redis主机
spring.redis.host=192.168.56.10
#配置rabbitmq的虚拟主机
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#关闭thymeleaf的缓存
spring.thymeleaf.cache=false
第3步:主启动类加注解
@EnableRedisHttpSession //开启分布式事务
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
第4步: 定时任务和cron表达式
cron表达式
corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
cron表达式生成器
https://cron.qqe2.com/
第5步:SpringBoot整合定时任务和异步任务
定时任务启动,就会查到最近3天的秒杀商品
package com.lian.gulimall.seckill.schedule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 定时任务
* 1、Scheduling 开启定时任务
* 2、Scheduled 开启一个定时任务
* 3、自动配置类 TaskSchedulingAutoConfiguration
*
* 异步任务
* 1、@EnableAsync 开启异步任务
* 2、@Async 给执行异步的方法加上
* 3、自动配置类 TaskExecutionAutoConfiguration
*
* 使用异步+定时任务 完成定时任务不阻塞的功能
*/
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
* cron = "* * * * * ?"
* 秒 分 时 日 月 周
* 日和周的位置,随便出现一个问号 ?
*/
@Async
@Scheduled(cron = "* * * * * *")
public void hello() throws Exception {
log.info("hello...");
Thread.sleep(1000);
}
}
第6步:时间日期处理
定时任务和异步任务配置类
/**
* 定时任务和异步任务的配置类
* @EnableScheduling 定时任务
* @EnableAsync 异步任务
*/
@EnableScheduling
@EnableAsync
@Configuration
public class ScheduledConfig {
}
秒杀商品定时上架功能控制层
/**
* 秒杀商品的定时上架
* 每天晚上3点,上架最近3天需要秒杀的商品
*/
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
//秒杀商品上架的锁
private final String upload_lock = "seckill:upload:lock";
@Async
@Scheduled(cron = "0 * 3 * * ?")
public void uploadSeckillLatest3Days(){
//上架参与秒杀的商品
log.info("上架秒杀的商品信息");
//上锁,锁10秒钟
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
//无论成功与失败,最后都要解锁
try {
//执行业务
seckillService.uploadSeckillLatest3Days();
} finally {
//解锁
lock.unlock();
}
}
}
优惠服务控制层
@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {
@Autowired
private SeckillSessionService seckillSessionService;
@GetMapping("/latest3DaySession")
public R getLatest3DaySession(){
//获取最近3天参与秒杀的活动及每个活动场次的秒杀商品列表
List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
return R.ok().setData(sessions);
}
}
优惠服务业务层
获取最近3天参与秒杀的活动及每个活动场次的秒杀商品列表
两个表:sms_seckill_session(活动表)、sms_seckill_sku_relation(活动场次对应秒杀商品表)
@Override
public List<SeckillSessionEntity> getLatest3DaySession() {
//查询出最近3天参与的秒杀活动
List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
if (list != null && list.size()>0){
List<SeckillSessionEntity> collect = list.stream().map((session) -> {
//获取活动场次id
Long id = session.getId();
//根据活动场次id获取到对应的 秒杀商品列表
List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
//关联活动场次和秒杀商品列表
session.setRelationSkus(relationEntities);
return session;
}).collect(Collectors.toList());
//返回活动场次类列表,及每场活动id对应的秒杀商品列表
return collect;
}
return null;
}
//开始时间
private String startTime(){
//获取此时日期 年月日
LocalDate now = LocalDate.now();
//获取此时最小时间 时分秒
LocalTime localTime = LocalTime.MIN;
//获取此时 年月日 时分秒
LocalDateTime startTime = LocalDateTime.of(now, localTime);
//格式化日期模式
String formatStartTime = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return formatStartTime;
}
//结束时间
private String endTime(){
LocalDate now = LocalDate.now();
LocalDate localDate = now.plusDays(2);
LocalTime localTime = LocalTime.MAX;
LocalDateTime endTime = LocalDateTime.of(localDate, localTime);
String formatEndTime = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return formatEndTime;
}
}
秒杀服务远程调用优惠服务
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
/**
* 获取最近3天参与的秒杀活动及每场活动场次id对应的所有商品
* sms_seckill_session 活动场次表
* sms_seckill_sku_relation 活动场次对应秒杀商品列表
*/
@GetMapping("/coupon/seckillsession/latest3DaySession")
R getLatest3DaySession();
}
秒杀商品定时上架功能业务层
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService; //优惠服务远程调用
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
ProductFeignService productFeignService; //商品服务远程调用
@Autowired
RedissonClient redissonClient;
@Autowired
RabbitTemplate rabbitTemplate;
//秒杀场次
private final String SESSIONS_CACHE_PREFIX ="seckill:sessions:";
//秒杀商品
private final String SKUKILL_CACHE_PREDIX ="seckill:skus";
//信号量
private final String SKU_STOCK_SEMPHORE ="seckill:stock:"; //后缀加商品随机码
@Override
public void uploadSeckillLatest3Days() {
//扫描最近3天需要参与秒杀的活动
//远程调用优惠服务,获取最近3天的所有活动场次及所有秒杀商品
R session = couponFeignService.getLatest3DaySession();
if (session.getCode() == 0){
//得到最近3天所有活动场次及对应的所有秒杀商品 SeckillSessionsWithSkus 等同于 SeckillSessionEntity(里面包含了sms_seckill_sku_relation表)
List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {});
//上架秒杀商品,将秒杀商品缓存到redis里
//1、缓存活动信息 map(key=seckill:sessions:startTime_endTime,value=List<活动场次id_商品id>)
saveSessionInfos(sessionData);
//2、缓存活动的关联商品信息
saveSessionSkuInfos(sessionData);
}
}
//sessions:代表最近3天的所有活动场次 及 每场次对应的所有秒杀商品
private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
//stream流处理每一个活动场次 sms_seckill_session
sessions.stream().forEach((session)->{
//获取每个活动场次的开始和结束时间
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
//redis中保存的key=seckill:sessions:startTime_endTime
String key = SESSIONS_CACHE_PREFIX+startTime+"_"+endTime;
//判断redis中是否保存了此 key
Boolean hasKey = stringRedisTemplate.hasKey(key);
if (!hasKey){
//获取所有参与秒杀商品的 活动场次id_商品id
//stream流处理 sms_seckill_sku_relation 秒杀商品表
List<String> collect = session.getRelationSkus().stream().map((item) -> {
// 每个秒杀商品的 活动场次id_商品id 例如:2_3
return item.getPromotionSessionId()+"_"+item.getSkuId().toString();
}).collect(Collectors.toList());
//缓存活动信息 map(key,list<活动场次id_商品id>),将商品id的集合保存到redis中
stringRedisTemplate.opsForList().leftPushAll(key, collect);
}
});
}
private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions) {
//遍历活动场次类(包含了sms_seckill_sku_relation)
sessions.stream().forEach((session)->{
//准备hash操作
BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
//遍历秒杀商品类 sms_seckill_sku_relation = seckillSkuVo
session.getRelationSkus().stream().forEach((seckillSkuVo -> {
//4、生成商品的秒杀随机码
String token = UUID.randomUUID().toString().replace("-", "");
//redis保存的key: 活动场次id_商品id
Boolean hasKey = ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString());
if (!hasKey) {
//缓存商品
//此to 封装了 秒杀商品表和商品属性信息表,SeckillSkuVo SkuInfoEntity
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
//1、sku的基本数据 pms_sku_info
R r = productFeignService.info(seckillSkuVo.getSkuId());
if (r.getCode() == 0) {
SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
redisTo.setSkuInfoVo(skuInfo);
}
//2、sku的秒杀信息 sms_seckill_sku_relation
BeanUtils.copyProperties(seckillSkuVo, redisTo);
//3、设置当前商品的秒杀开始和结束时间
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
//秒杀商品设置随机码
redisTo.setRandomCode(token);
//转为json字符串保存到redis中
String jsonString = JSON.toJSONString(redisTo);
//redis保存商品 map(场次id_商品id,redisTo)
ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(), jsonString);
//5、引入分布式的信号量,使用库存作为分布式的信号量 seckill:stock:token
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMPHORE + token);
//商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
}));
});
}
分布式锁做信号量需要引入pom
<!--redis的 redisson 实现分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
redisson配置返回RedissonClient
redisson是专门负责做分布式锁的
@Configuration
public class MyRedissonConfig {
/**
* 所有对redisson的使用都是通过 redissonClient对象
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
//2、根据config创建出redissonClient示例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
redisson信号量
<!--redis的 redisson 实现分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
配置类
package com.lian.gulimall.seckill.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyRedissonConfig {
/**
* 所有对redisson的使用都是通过 redissonClient对象
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient(){
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
//2、根据config创建出redissonClient示例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
redis保存结果
只要开启定时任务,redis就会查询出最近3天的活动场次及所有参与秒杀的商品
第7步:查询秒杀商品
控制层
@Controller
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 返回当前时间可以参与的秒杀商品信息
*/
@ResponseBody
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
}
实现层
//返回当前时间可以参与的秒杀商品信息
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
//1、确定当前时间属于哪个秒杀场次
//获取当前时间
long time = new Date().getTime();
//获取redis中保存的所有秒杀商品场次 key:seckill:sessions:开始时间_结束时间
Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
for (String key : keys) {
//用空串替代前缀,留下 开始时间_结束时间
String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] s = replace.split("_");
long startTime = Long.parseLong(s[0]);
long endTime = Long.parseLong(s[1]);
if (time >= startTime && time <= endTime){
//2、获取这个秒杀场次需要的所有商品信息
//获取在活动期间的所有key的值 1_1 、 2_1 key:seckill:sessions:1629245700000_1629302399000
//range:取出此key中的所有值
List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
//绑定值操作
BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
//批量获取 key 1_1 、 2_1 的值
List<String> list = hashOps.multiGet(range);
if (list != null){
List<SeckillSkuRedisTo> collect = list.stream().map((item) -> {
SeckillSkuRedisTo redisTo = JSON.parseObject((String) item, SeckillSkuRedisTo.class);
//redisTo.setRandomCode(null); //当前秒杀开始才需要随机码
return redisTo;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
return null;
}
查询结果
第8步:秒杀页面渲染
控制层
/**
* 获取当前sku的秒杀信息
* 判读当前sku的商品是否参与秒杀活动
*/
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId){
SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}
业务层
@Override
public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
//找到所有需要参与秒杀的商品的key
BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
Set<String> keys = hashOps.keys();
if (keys.size()>0 && keys!=null){
//场次id + 商品skuId 6_4
String regex = "\\d_"+skuId;
for (String key : keys) {
//只要活动场次中有此skuId的商品,就都匹配
if (Pattern.matches(regex, key)){
String json = hashOps.get(key);
SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
//随机码
//如果在秒杀期间,就有随机码,否则随机码就为空
long current = new Date().getTime();
Long startTime = skuRedisTo.getStartTime();
Long endTime = skuRedisTo.getEndTime();
if (current >= startTime && current <= endTime){
}else {
//否则,随机码为空
skuRedisTo.setRandomCode(null);
}
return skuRedisTo;
}
}
}
return null;
}
商品服务调用远程服务
@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
/**
* 获取当前sku的秒杀信息
*/
@GetMapping("/sku/seckill/{skuId}")
R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
商品服务业务类
商品服务的 itemController层查询skuId商品是否参与秒杀,并将其封装起来,供前台详情页调用
@Override
public SkuItemVo item(Long skuId) throws Exception{
//商品详情页返回数据都封装到 SkuItemVo
SkuItemVo skuItemVo = new SkuItemVo();
/**
* 使用异步编排,节省时间提升效率,一起执行不阻塞等待
* supplyAsync 有返回值,其他任务可以用
* 开启一个异步任务,创建异步对象
* infoFuture 任务完成后,saleAttrFuture、descFuture、baseAttrFuture 才开始执行,因为他们都需要依赖任务1的数据结果
*/
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息获取,标题、副标题、价格等 pms_sku_info
SkuInfoEntity info = baseMapper.selectById(skuId);
skuItemVo.setInfo(info);
//因为其他任务要用基本信息,所以我们返回基本信息
return info;
//executor代表要放到自己的线程池里面
}, executor);
//接下来接收任务的返回结果,accept只是接收上一个任务的结果,自己不返回结果
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//执行第二个任务
//3、获取spu的销售属性组合
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
});
//继续执行任务
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
//4、获取spu的介绍
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesc(spuInfoDescEntity);
}, executor);
//继续执行任务,任务3、4、5都依赖任务1的结果 获取spuId
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//5、获取spu的规格参数信息
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
/**
* 任务2 不需要依赖任务1提供的结果数据,所以不需要等待任务1完成,直接和任务1同步执行,所以自己也开启一个异步任务
* runAsync 代表不需要返回结果,因为也没有其他任务需要依赖任务2的数据
*/
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
//2、sku图片信息 pms_sku_images
List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
/**
* 查询当前sku是否参加秒杀优惠
*/
CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
R r = seckillFeignService.getSkuSeckillInfo(skuId);
if (r.getCode() == 0) {
SeckillInfoVo data = r.getData(new TypeReference<SeckillInfoVo>() {
});
skuItemVo.setSeckillInfoVo(data);
}
}, executor);
/**
* 等待所有任务都完成,因为每一个任务都是在给 vo 中封装数据
* get()方法就是阻塞等待所有任务都执行完
* infoFuture 也可以不写,因为别人是依赖她的,如果别人都执行完了,那么她肯定也执行完了
*/
CompletableFuture.allOf(infoFuture, saleAttrFuture, descFuture, baseAttrFuture, imageFuture,secKillFuture).get();
return skuItemVo;
}
第9步:秒杀系统设计
第10步:登录检查
整合SpringSession
pom依赖
<!--引入分布式session-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
application.properties
#session中存储的类型
spring.session.store-type=redis
#redis主机
spring.redis.host=192.168.56.10
主启动类配置注解
/**
* 定时任务和异步任务的配置类
* @EnableScheduling 定时任务
* @EnableAsync 异步任务
*/
@EnableScheduling
@EnableAsync
@EnableRedisHttpSession //开启分布式事务
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
springSession配置类
/**
* 自定义cookie的序列化
* 自定义redis的序列化
*/
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setDomainName("gulimall.com");
defaultCookieSerializer.setCookieName("GULISESSION");
defaultCookieSerializer.setCookiePath("/");
return defaultCookieSerializer;
}
@Bean
public RedisSerializer<Object> redisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
登录拦截器配置
@Component
public class LoginUserIntereptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//对指定路径放行。秒杀请求: localhost:8080/kill
//获取请求路径 uri:代表localhost:8080/ 斜杠后面的请求 url:代表是localhost:8080/ 全部的请求路径
String uri = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match("/kill", uri);
//如果是秒杀请求,才需要判断是否登录,只有登录才可以进行秒杀活动
if (match){
MemberRespVo respVo = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (respVo != null){
loginUser.set(respVo);
return true;
}else {
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}else {
//如果不是秒杀请求,其他的请求都放行
return true;
}
}
}
拦截器配置类
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserIntereptor loginUserIntereptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserIntereptor).addPathPatterns("/**");
}
}
第11步:秒杀流程业务类
秒杀控制层
/**
* 秒杀方法
* @param killId 场次id_商品id
* @param key 随机码
* @param num 秒杀数量
* @return
*/
@GetMapping("/kill")
public String secKill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num,
Model model){
//秒杀成功就返回一个订单号
String orderSn = seckillService.kill(killId,key,num);
System.out.println("orderSn是:"+orderSn);
model.addAttribute("orderSn", orderSn);
return "success";
}
秒杀业务层
@Override
public String kill(String killId, String key, Integer num) {
//获取拦截器中登录的用户
MemberRespVo memberRespVo = LoginUserIntereptor.loginUser.get();
//1、获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX);
//根据秒杀id获取商品信息 2_1
String json = hashOps.get(killId);
if (StringUtils.isEmpty(json)){
return null;
}else {
SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
//todo 校验合法性
//1、校验时间的合法性
//获取开始和结束时间
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
//获取活动持续时间
Long ttl = endTime - startTime;
//获取当前时间
long time = new Date().getTime();
//如果秒杀时间正确,就进行秒杀,否则返回为空
if (time >= startTime || time <= endTime){
//2、校验随机码和商品id
//获取随机码
String randomCode = redisTo.getRandomCode();
//获取秒杀id = 场次id+商品skuid
String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
//匹配传入的随机码和秒杀id是否正确
if (randomCode.equals(key) && skuId.equals(killId)){
//3、验证购物数量是否合理
Integer seckillLimit = redisTo.getSeckillLimit();
//如果购买数量小于每人限购数量,才是合法的
if (num <= seckillLimit){
//4、验证这个人是否已经购买过此秒杀商品(防止一人多次重复秒杀商品)
String redisKey = memberRespVo.getId()+"_"+skuId;
//自动过期 等同于 setnx 设置如果不存在
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean){
//如果此 redisKey 设置key-v成功,代表从来没有买过秒杀商品
//获取信号量 + 后缀随机码
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMPHORE+randomCode);
//从信号量中取出一个
//请求一个信号量,减少num个
boolean b = semaphore.tryAcquire(num);
if (b) {
//秒杀成功
//快速下单,发送mq消息,10ms
//获取商品订单 ID
String timeId = IdWorker.getTimeId();
//封装
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSkuId(redisTo.getSkuId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
orderTo.setNum(num);
//用户id,说明是哪个用户秒杀的商品
orderTo.setMemberId(memberRespVo.getId());
//利用rabbitmq发送消息到mq(订单服务接收此消息)
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
return timeId;
}
return null;
}
}else {
//说明此人已经买过秒杀商品,就不能买了
return null;
}
}
}else {
return null;
}
}
return null;
}
第12步:rabbitmq
秒杀服务秒杀成功后,发送消息到mq中,订单服务监听消息,负责下单处理
导入amqp的pom
<!--引入rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
rabbitmq配置
#配置rabbitmq的虚拟主机
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.simple.acknowledge-mode=manual
为了保证消息是json格式,所以对rabbitmq做了消息转换配置
@Configuration
public class MyRabbitmqConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
订单服务里mq秒杀队列和交换机绑定配置
//声明秒杀队列
@Bean
public Queue orderSeckillOrderQueue(){
Queue queue = new Queue("order.seckill.order.queue", true, false, false, null);
return queue;
}
//声明秒杀队列与交换机绑定
@Bean
public Binding orderSeckillOrderQueueBinding(){
return new Binding("order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null
);
}
监听秒杀控制层
@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Service
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo seckillOrderTo, Channel channel, Message message){
try {
log.info("准备创建秒杀单的详细信息");
orderService.createSeckillOrder(seckillOrderTo);
} catch (Exception e) {
e.printStackTrace();
}
}
}
监听后创建秒杀订单
/**
* 创建秒杀单的详细信息,就是保存订单 orderEntity 和 orderItemEntity
* @param seckillOrderTo
*/
@Override
public void createSeckillOrder(SeckillOrderTo seckillOrderTo) {
//1、保存订单信息
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(seckillOrderTo.getOrderSn());
orderEntity.setMemberId(seckillOrderTo.getMemberId());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
BigDecimal price = seckillOrderTo.getSeckillPrice().multiply(new BigDecimal("" + seckillOrderTo.getNum()));
orderEntity.setPayAmount(price);
this.save(orderEntity);
//2、保存订单项信息
OrderItemEntity itemEntity = new OrderItemEntity();
//设置sku的详细设置
R r = productFeignService.getSpuInfoBySkuId(seckillOrderTo.getSkuId());
itemEntity.setSkuQuantity(seckillOrderTo.getNum());
SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {});
itemEntity.setSpuBrand(spuInfo.getBrandId().toString());
itemEntity.setSpuName(spuInfo.getSpuName());
itemEntity.setCategoryId(spuInfo.getCatalogId());
orderItemService.save(itemEntity);
}
秒杀测试
http://localhost:25000/kill?killId=1_1&key=a7397e32a3794ac3acaa7639a2e51917&num=1