秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流+异步+缓存(页面静态化)+独立部署
限流方式:
秒杀架构思路
1、配置网关
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
2、新增场次,关联商品
修改“com.atguigu.gulimall.coupon.service.impl.SeckillSkuRelationServiceImpl”代码如下:
package com.atguigu.gulimall.coupon.service.impl;
@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>();
String promotionSessionId = (String) params.get("promotionSessionId");
// 场次id不是null
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);
}
}
秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流+异步+缓存(页面静态化)+独立部署
1、创建微服务模块
2、导入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.12.0version>
dependency>
<dependency>
<groupId>com.atguigu.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
exclusion>
exclusions>
dependency>
3、添加配置
spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.redis.host=124.222.223.222
4、主启动类添加注解
package com.atguigu.gulimall.seckill;
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
由于秒杀服务是在高并发的情况下访问,每次访问都要查询数据库的话,可能会把数据库压垮!
我们可以在秒杀的商品在秒杀之前,将其上架 (放在缓存当中),每次从缓存中拿。秒杀需要用到的库存也可以存到缓存中。
使用 Cron Trigger Tutorial 框架,来做定时任务。
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1-12 或者 JAN-DEC | , - * / |
星期 | 1-7 或者 SUN-SAT | , - * ? / L C # |
年(可选) | 留空, 1970-2099 | , - * / |
特殊符号:
,
:枚举,表示附加一个可能值
-
: 表示一个指定的范围;
*
:任意,所有值
/
:步长,符号前表示开始时间,符号后表示每次递增的值;
?
:表示未说明的值,即不关心它为何值(出现在日和周几的位置,为了防止日和周几冲突,在周和日上如果要写通配符使用?)
L
:(出现在日和周的位置)
W
:
#
:第几个,只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。
一些cron表达式案例
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0 12 ? * WED 表示每个星期三中午12点
0 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 0 23 L * ? 每月最后一天23点执行一次
0 15 10 L * ? 每月最后一日的上午10:15触发
0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
0 15 10 * * ? 2005 2005年的每天上午10:15触发
0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
"30 * * * * ?" 每半分钟触发任务
"30 10 * * * ?" 每小时的10分30秒触发任务
"30 10 1 * * ?" 每天1点10分30秒触发任务
"30 10 1 20 * ?" 每月20号1点10分30秒触发任务
"30 10 1 20 10 ? *" 每年10月20号1点10分30秒触发任务
"30 10 1 20 10 ? 2011" 2011年10月20号1点10分30秒触发任务
"30 10 1 ? 10 * 2011" 2011年10月每天1点10分30秒触发任务
"30 10 1 ? 10 SUN 2011" 2011年10月每周日1点10分30秒触发任务
"15,30,45 * * * * ?" 每15秒,30秒,45秒时触发任务
"15-45 * * * * ?" 15到45秒内,每秒都触发任务
"15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?" 每小时的第0分0秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI" 星期一到星期五的10点15分0秒触发任务
"0 15 10 L * ?" 每个月最后一天的10点15分0秒触发任务
"0 15 10 LW * ?" 每个月最后一个工作日的10点15分0秒触发任务
"0 15 10 ? * 5L" 每个月最后一个星期四的10点15分0秒触发任务
"0 15 10 ? * 5#3" 每个月第三周的星期四的10点15分0秒触发任务
package com.atguigu.gulimall.seckill.scheduled;
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;
/**
* Data time:2022/4/16 20:20
* StudentID:2019112118
* Author:hgw
* Description: 定时调度测试
* 定时任务:
* 1、@EnableScheduling 开启定时任务
* 2、@Scheduled 开启一个定时任务
* 3、自动配置类 TaskSchedulingAutoConfiguration
* 异步任务:
* 1、@EnableAsync 开启异步任务功能
* 2、@Async :给我希望异步执行的方法上标注
* 3、自动配置类 TaskExecutionAutoConfiguration 属性绑定在 TaskExecutionProperties
*/
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
/**
* 1、spring中corn 表达式由6为组成,不允许第7位的年 Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
* 2、在周几的位置,1-7分别代表:周一到周日(MON-SUN)
* 3、定时任务默认是阻塞的。如何让它不阻塞?
* 1)、可以让业务运行以异步的方式,自己提交到线程池
* 2)、Cron expression must consist of 6 fields (found 7 in "* * * * * ? 2022")
* spring.task.scheduling.pool.size=5
* 3)、让定时任务异步执行
* 异步任务
* 解决:使用异步+定时任务来完成定时任务不阻塞的功能
*/
@Async
@Scheduled(cron = "* * * * * 6")
public void hello() throws InterruptedException {
log.info("hello.....");
Thread.sleep(3000);
}
}
配置定时任务参数
1)、gulimall-seckill服务中编写 gulimall-coupon服务的远程调用接口
1、gulimall-seckill服务中编写 gulimall-coupon服务的远程调用接口
gulimall-seckill服务 的 com.atguigu.gulimall.seckill.feign
路径下的 CouponFeignService类
package com.atguigu.gulimall.seckill.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Data time:2022/4/16 21:05
* StudentID:2019112118
* Author:hgw
* Description: 远程调用优惠服务接口
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@GetMapping("/coupon/seckillsession/lates3DaySession")
R getLates3DaySession();
}
2、gulimall-seckill服务中编写 gulimall-coupon服务获取的数据的Vo
package com.atguigu.gulimall.seckill.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class SeckillSessionsWithSkus {
/**
* id
*/
private Long id;
/**
* 场次名称
*/
private String name;
/**
* 每日开始时间
*/
private Date startTime;
/**
* 每日结束时间
*/
private Date endTime;
/**
* 启用状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
private List<SeckillSkuVo> relationSkus;
}
package com.atguigu.gulimall.seckill.vo;
import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;
@Data
public class SeckillSkuVo {
/**
* id
*/
private Long id;
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private BigDecimal seckillCount;
/**
* 每人限购数量
*/
private BigDecimal seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
}
2)、gulimall-coupon服务 编写扫描数据库最近3天需要上架的秒杀活动 以及 秒杀活动需要的商品
1、Controller 层接口编写
package com.atguigu.gulimall.coupon.controller;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.atguigu.gulimall.coupon.entity.SeckillSessionEntity;
import com.atguigu.gulimall.coupon.service.SeckillSessionService;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.R;
/**
* 秒杀活动场次
*
* @author leifengyang
* @email [email protected]
* @date 2019-10-08 09:36:40
*/
@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {
@Autowired
private SeckillSessionService seckillSessionService;
/**
* 查询三天内需要上架的服务
* @return
*/
@GetMapping("/lates3DaySession")
public R getLates3DaySession(){
List<SeckillSessionEntity> sessions = seckillSessionService.getLates3DaySession();
return R.ok().setData(sessions);
}
2、Service 层实现类编写
package com.atguigu.gulimall.coupon.service.impl;
@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {
@Autowired
SeckillSkuRelationService seckillSkuRelationService;
@Override
public List<SeckillSessionEntity> getLates3DaySession() {
// 计算最近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 -> {
Long id = session.getId();
List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
session.setRelationSkus(relationEntities);
return session;
}).collect(Collectors.toList());
return collect;
}
return null;
}
package com.atguigu.gulimall.seckill.service.impl;
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
StringRedisTemplate redisTemplate;
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
/**
* 缓存活动信息
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
sessions.stream().forEach(session ->{
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
System.out.println(key);
List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
// 缓存活动信息
redisTemplate.opsForList().leftPushAll(key,collect);
});
}
/**
* 缓存活动的关联商品信息
* @param sessions
*/
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){
sessions.stream().forEach(session->{
// 准备Hash操作
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
// 缓存商品
SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
// 1、Sku的基本数据
R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (skuInfo.getCode() == 0) {
SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
redisTo.setSkuInfo(info);
}
// 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);
semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getSkuId().toString(),jsonString);
});
});
}
1)、封装秒杀商品的详细信息 To
package com.atguigu.gulimall.seckill.to;
import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import lombok.Data;
import java.math.BigDecimal;
/**
* Data time:2022/4/16 22:20
* StudentID:2019112118
* Author:hgw
* Description: 秒杀商品的详细信息
*/
@Data
public class SecKillSkuRedisTo {
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 商品秒杀的随机码
*/
private String randomCode;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private BigDecimal seckillCount;
/**
* 每人限购数量
*/
private BigDecimal seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
/**
* sku的详细信息
*/
private SkuInfoVo skuInfo;
/**
* 当前商品秒杀活动的开始时间
*/
private Long startTime;
/**
* 当前商品秒杀活动的结束时间
*/
private Long endTime;
}
package com.atguigu.gulimall.seckill.vo;
@Data
public class SkuInfoVo {
/**
* skuId
*/
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;
}
2)、编写远程查询 Sku基本信息 的接口
package com.atguigu.gulimall.seckill.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Data time:2022/4/16 22:36
* StudentID:2019112118
* Author:hgw
* Description:
*/
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);
}
第一步、加锁
package com.atguigu.gulimall.seckill.scheduled;
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
// TODO 幂等性处理
@Scheduled(cron = "* * 3 * * ?")
public void uploadSeckillSkuLatest3Days() {
// 1、重复上架无需处理
log.info("上架秒杀商品的信息");
// 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后,其他人获取到就会拿到最新的状态
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try {
seckillService.uploadSeckillSkuLatest3Days();
} finally {
lock.unlock();
}
}
}
第二步、判断Redis中是否已上架
package com.atguigu.gulimall.seckill.service.impl;
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
ProductFeignService productFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
RedissonClient redissonClient;
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // + 商品随机码
/**
* 远程查询最近 3 天内秒杀的活动 以及 秒杀活动的关联的商品信息
*/
@Override
public void uploadSeckillSkuLatest3Days() {
// 1、扫描最近三天数据库需要参与秒杀的活动
R session = couponFeignService.getLates3DaySession();
if (session.getCode() == 0) {
// 上架商品
List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
});
// 缓存到Redis
// 1)、缓存活动信息
saveSessionInfos(sessionData);
// 2)、缓存活动的关联商品信息
saveSessionSkuInfo(sessionData);
}
}
/**
* 缓存活动信息
*
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) {
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey) {
// 缓存活动信息
List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId().toString()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key, collect);
}
});
}
/**
* 缓存活动的关联商品信息
*
* @param sessions
*/
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions) {
sessions.stream().forEach(session -> {
// 准备Hash操作
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
// 生成随机码
String token = UUID.randomUUID().toString().replace("_", "");
// 1)、缓存商品
if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString())) {
SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo();
// 1、Sku的基本数据
R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (skuInfo.getCode() == 0) {
SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
redisTo.setSkuInfo(info);
}
// 2、Sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo, redisTo);
// 3、设置上当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
// 4、商品的随机码
redisTo.setRandomCode(token);
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(), jsonString);
// 如果当前这个场次的商品的库存信息已经上架就不需要上架
// 5、引入分布式的信号量 限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
}
});
});
}
}
package com.atguigu.gulimall.seckill.controller;
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 返回当前时间可以参与秒杀的商品信息
* @return
*/
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
List<SecKillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
}
gulimall-seckill 服务的 com/atguigu/gulimall/seckill/service/impl
路径下的 SeckillServiceImpl.java
/**
* 获取当前参与秒杀的商品
* @return
*/
@Override
public List<SecKillSkuRedisTo> getCurrentSeckillSkus() {
// 1、确定当前时间属于哪个秒杀场次
long time = new Date().getTime();
Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
for (String key : keys) {
// seckill:sessions:1650153600000_1650160800000
String replace = key.replace(SESSION_CACHE_PREFIX, "");
String[] s = replace.split("_");
long start = Long.parseLong(s[0]);
long end = Long.parseLong(s[1]);
if (time>= start && time<=end) {
// 2、获取指定秒杀场次需要的所有商品信息
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<String> list = hashOps.multiGet(range);
if (list!=null) {
List<SecKillSkuRedisTo> collect = list.stream().map(item -> {
SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class);
redis.setRandomCode(null); // 当前秒杀开始了需要随机码
return redis;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
return null;
}
1、配置网关
- id: gulimall_seckill_route
uri: lb://gulimall-seckill
predicates:
- Host=seckill.gulimall.cn
2、配置域名 vim /etc/hosts
# Gulimall Host Start
127.0.0.1 gulimall.cn
127.0.0.1 search.gulimall.cn
127.0.0.1 item.gulimall.cn
127.0.0.1 auth.gulimall.cn
127.0.0.1 cart.gulimall.cn
127.0.0.1 order.gulimall.cn
127.0.0.1 member.gulimall.cn
127.0.0.1 seckill.gulimall.cn
# Gulimall Host End
修改 gulimall-product 服务的 index.html :
<div class="section_second_list">
<div class="swiper-container swiper_section_second_list_left">
<div class="swiper-wrapper">
<div class="swiper-slide">
<ul id="seckillSkuContent">
ul>
function to_href(skuId) {
location.href = "http://item.gulimall.cn/"+skuId+".html";
}
$.get("http://seckill.gulimall.cn/currentSeckillSkus",function (resp) {
if (resp.data.length > 0) {
resp.data.forEach(function (item) {
$("+ item.skuId +")'> ")
.append($(""))
.append($(""
+ item.skuInfo.skuTitle +""))
.append($(""+ item.seckillPrice +""))
.append($(""+ item.skuInfo.price +""))
.appendTo("#seckillSkuContent");
});
}
主体:修改 gulimall-product 服务的SkuInfoServiceImpl 类的 item 方法
gulimall-product 服务的 com.atguigu.gulimall.product.service.impl
路径下的 SkuInfoServiceImpl类:
@Override
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
// 1、sku基本信息 pms_sku_info
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
// 2、获取 spu 的销售属性组合
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync(res -> {
List<SkuItemSaleAttrsVo> saleAttrVos = saleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
// 3、获取 spu 的介绍 pms_spu_info_desc
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
// 4、获取 spu 的规格参数信息 pms_spu_info_desc
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(res -> {
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
// 5、sku的图片信息 pms_sku_images
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
// 6、查询当前sku是否参与秒杀优惠
CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
R seckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
if (seckillInfo.getCode() == 0) {
SeckillInfoVo seckillInfoVo = seckillInfo.getData(new TypeReference<SeckillInfoVo>() {
});
skuItemVo.setSeckillInfo(seckillInfoVo);
}
}, executor);
// 等待所有任务都完成
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,secKillFuture).join();
return skuItemVo;
}
第一步、在gulimall-product 服务中编写 远程调用gulimall-seckill 服务的feign接口
package com.atguigu.gulimall.product.feign;
@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
封装接收VO:
package com.atguigu.gulimall.product.vo;
/**
* Data time:2022/4/5 10:34
* StudentID:2019112118
* Author:hgw
* Description: 商品详情
*/
@Data
public class SkuItemVo {
// 1、sku基本信息 pms_sku_info
SkuInfoEntity info;
// 是否有货
boolean hasStock = true;
// 2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images;
// 3、获取 spu 的销售属性组合
List<SkuItemSaleAttrsVo> saleAttr;
// 4、获取 spu 的介绍 pms_spu_info_desc
SpuInfoDescEntity desp;
// 5、获取 spu 的规格参数信息
List<SpuItemAttrGroupVo> groupAttrs;
// 6、当前商品的秒杀优惠信息
SeckillInfoVo seckillInfo;
}
package com.atguigu.gulimall.product.vo;
@Data
public class SeckillInfoVo {
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 商品秒杀的随机码
*/
private String randomCode;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private BigDecimal seckillCount;
/**
* 每人限购数量
*/
private BigDecimal seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
/**
* 当前商品秒杀活动的开始时间
*/
private Long startTime;
/**
* 当前商品秒杀活动的结束时间
*/
private Long endTime;
}
第二步、在gulimall-seckill 服务中编写 获取某个商品的秒杀预告信息 接口
1、gulimall-seckill 服务 com.atguigu.gulimall.seckill.controller
路径下的 SeckillController 类,代码如下:
package com.atguigu.gulimall.seckill.controller;
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 获取某个商品的秒杀预告信息
* @param skuId
* @return
*/
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
SecKillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}
}
2、gulimall-seckill 服务 com.atguigu.gulimall.seckill.service.impl
路径下的 SeckillServiceImpl 类,代码如下:
/**
* 获取某个商品的秒杀预告信息
* @param skuId
* @return
*/
@Override
public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) {
// 1、找到所有需要参与秒杀的key
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
Set<String> keys = hashOps.keys();
if (keys != null && keys.size()>0) {
String regx = "\\d_"+skuId;
for (String key : keys) {
if (Pattern.matches(regx,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("");
}
return skuRedisTo;
}
}
}
return null;
}
修改 item.html 页面
<div class="box-summary clear">
<ul>
<li>京东价li>
<li>
<span>¥span>
<span th:text="${#numbers.formatDecimal(item.info.price,0,2)}">4499.00span>
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="${#dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
span>
li>
<li>
<a href="/static/item/">
预约说明
a>
li>
ul>
div>
1、修改 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}]];
if (islogin) {
var killId = $(this).attr("sessionid")+"_"+$(this).attr("skuid");
var key = $(this).attr("code");
var num = $("#numInput").val();
location.href = "http://seckill.gulimall.cn/kill?killId="+killId+"&key="+key+"&num="+num;
} else {
alert("秒杀请先登录!");
}
return false;
});
1、引入SpringSession依赖的Redis
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
2、在配置文件中添加SpringSession的保存方式
#SpringSession的保存方式
spring.session.store-type=redis
3、主启动类开启RedisHttpSession这个功能
package com.atguigu.gulimall.seckill;
@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
4、编写SpringSession的配置
package com.atguigu.gulimall.seckill.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* Data time:2022/4/9 10:19
* StudentID:2019112118
* Author:hgw
* Description: 自定义Session 配置
*/
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.cn");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
5、编写用户登录拦截器 并 配置到Spring容器中
package com.atguigu.gulimall.seckill.interceptoe;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match("/kill", uri);
if (match){
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
} else {
// 没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.cn/login.html");
return false;
}
}
return true;
}
}
添加“com.atguigu.gulimall.seckill.config.SeckillWebConfig”类,代码如下:
package com.atguigu.gulimall.seckill.config;
@Configuration
public class SeckillWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GJrfVDgy-1650200866286)(谷粒商城-分布式高级篇[商城业务-秒杀服务].assets/image-20220417170203168.png)]
1、Controller层接口的编写
package com.atguigu.gulimall.seckill.controller;
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 秒杀请求
* @return
*/
@GetMapping("/kill")
public R secKill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num) {
String orderSn = seckillService.kill(killId,key,num);
return R.ok().setData(orderSn);
}
}
使用队列进行削峰
1、引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
2、编写配置
#RabbitMq的配置
spring.rabbitmq.host=124.222.223.222
spring.rabbitmq.virtual-host=/
3、编写配置类
package com.atguigu.gulimall.seckill.config;
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
4、编写 创建消息队列、以及消息队列和交换器的绑定
在 gulimall-order 服务的 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);
}
Service层实现类的方法编写
gulimall-seckill 服务的 com.atguigu.gulimall.seckill.service.impl
路径下的 SeckillServiceImpl实现类
/**
* 秒杀处理,发送消息给MQ
* @param killId 存放的key
* @param key 随机码
* @param num 购买数量
* @return 生成的订单号
*/
@Override
public String kill(String killId, String key, Integer num) {
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
// 1、获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String json = hashOps.get(killId);
if (StringUtils.isEmpty(json)) {
return null;
} else {
SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class);
// 2、校验合法性
long time = new Date().getTime();
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
long ttl = endTime - time;
// 2.1、校验时间的合法性
if (time >= startTime && time <= endTime) {
// 2.2、校验随机码 和 商品id 是否正确
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
// 2.3、验证购物车数量是否合理
if (num <= redis.getSeckillLimit().intValue()) {
// 2.4、验证这个人是否购买过。幂等性:如果只要秒杀成功,就去占位。 userId_SessionId_skuId
String redisKey = respVo.getId() + "_" + skuId;
// 自动过期
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
// 占位成功说明从来没有买过
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
try {
boolean tryAcquire = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
// 秒杀成功
// 3、快速下单,给MQ发送消息
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(respVo.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);
return timeId;
} catch (InterruptedException e) {
return null;
}
} else {
// 说明已经买过了
return null;
}
}
} else {
return null;
}
} else {
return null;
}
}
return null;
}
消息传递的TO
package com.atguigu.common.to.mq;
import lombok.Data;
import java.math.BigDecimal;
/**
* Data time:2022/4/17 17:50
* StudentID:2019112118
* Author:hgw
* Description: 秒杀订单
*/
@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;
}
package com.atguigu.gulimall.order.listener;
@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
try {
log.info("准备创建秒杀单的详细信息:"+seckillOrder);
orderService.createSeckillOrder(seckillOrder);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
2、gulimall-order 服务的 com.atguigu.gulimall.order.service.impl
路径下 OrderServiceImpl,方法:
/**
* 秒杀单的详细信息创建
* @param seckillOrder
*/
@Override
public void createSeckillOrder(SeckillOrderTo seckillOrder) {
//TODO 保存订单信息
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 保存订单项信息
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setOrderSn(seckillOrder.getOrderSn());
orderItemEntity.setRealAmount(multiply);
orderItemEntity.setSkuQuantity(seckillOrder.getNum());
// TODO 获取当前SKU的详细信息进行设置
orderItemService.save(orderItemEntity);
}
1、引入thymeleaf
导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
在配置里关闭thymeleaf缓存
#关闭缓存
spring.thymeleaf.cache=false
2、修改Controller层代码进行页面跳转
package com.atguigu.gulimall.seckill.controller;
@Controller
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 返回当前时间可以参与秒杀的商品信息
* @return
*/
@ResponseBody
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
List<SecKillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
/**
* 获取某个商品的秒杀预告信息
* @param skuId
* @return
*/
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
SecKillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}
/**
* 秒杀请求
* @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);
model.addAttribute("orderSn",orderSn);
return "success";
}
}
3、前端页面修改
<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.cn/payOrder?orderSn='+orderSn}">去支付a>h2>
div>
div>
<div th:if="${orderSn==null}">
<h1>手气不好,秒杀失败!h1>
div>
div>
div>
div>