秒杀服务
一、商品上架
秒杀活动的结构图
通过定时任务触发:
- 定时任务由spring提供,需要通过注解开启,这里通过定义一个配置类,注入spring,对其配置类进行相应的注解,当然也可以注解放在我们的服务启动类上
- cron表达式定时示例
0 * * * * ? 每1分钟触发一次
0 0 * * * ? 每天每1小时触发一次
0 0 10 * * ? 每天10点触发一次
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 30 9 1 * ? 每月1号上午9点半
0 15 10 15 * ? 每月15日上午10:15触发
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0 0 1 * ? 每月1号凌晨执行一次
对照上面的字段含义写自定义的cron时间表达式基本就ok了,写完后可以到 http://cron.qqe2.com/ 验证下。
注意:"0 0/50 * * * ?"这个表达式很多人会认为是每隔50分钟执行,实际不是,会每个小时的50分、60分钟跑一次,例如1:50,2:00,2:50,3:00……
//开启异步,还需要在具体的任务方法加@Async表示需要异步 ,定时任务默认是同步的,主要是为了定时可以按时跑起来,比如说我们任务是每10秒执行一次,但是任务逻辑跑完不止10秒,这样就需要等任务执行完再接着跑,这样频率就没有达到预期效果,所以我们开启异步,多线程去每隔10秒运行任务,即使任务10秒内没跑完也会开启其他线程来跑,而这里的线程池的数量,是系统默认给的,这里可以通过配置参数在yaml文件自定义
//spring.task.execution.pool.core-size=5 .max-size=20 进行设置
@EnableAsync
//开启定时功能 还需要在具体扥任务方法加@Scheduled表示定时频率
@EnableScheduling
@Configuration
public class ScheduleConfig {
}
/**
* 定时上架秒杀商品信息
*/
@Slf4j
@Component
public class SeckillSkuSchedule {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
/**
*
*/
@Async
//每5s执行一次
@Scheduled(cron = "*/5 * * * * *")
public void uploadSeckillSku3Days(){
log.info("定时上架秒杀商品执行了...." + new Date());
// 分布式锁 避免多节点多集群场景下,会重复执行,先提供一个分布式锁,使各个节点通过这种获取锁方式来执行,当然拿到锁还是会去重复执行的,所以接着再业务方法里头还要通过逻辑判断去过滤处理,比如已经存储在redis的key 那么就不再执行,不存在的说明还没执行就进行插入 if(!key)
RLock lock = redissonClient.getLock("seckill:upload:lock");
lock.lock(10, TimeUnit.SECONDS);
try {
// 调用上架商品的方法
seckillService.uploadSeckillSku3Days();
}catch (Exception e){
lock.unlock();
}
}
}
进入到Service中处理
@Override
public void uploadSeckillSku3Days() {
// 1. 通过OpenFegin 远程调用Coupon服务中接口来获取未来三天的秒杀活动的商品
R r = couponFeignService.getLates3DaysSession();
if(r.getCode() == 0){
// 表示查询操作成功
String json = (String) r.get("data");
List<SeckillSessionEntity> seckillSessionEntities = JSON.parseArray(json,SeckillSessionEntity.class);
// 2. 上架商品 Redis数据保存
// 缓存商品
// 2.1 缓存每日秒杀的SKU基本信息
saveSessionInfos(seckillSessionEntities);
// 2.2 缓存每日秒杀的商品信息
saveSessionSkuInfos(seckillSessionEntities);
}
}
/**
* 保存每日活动的信息到Redis中
* @param seckillSessionEntities
*/
private void saveSessionInfos(List<SeckillSessionEntity> seckillSessionEntities) {
for (SeckillSessionEntity seckillSessionEntity : seckillSessionEntities) {
// 循环缓存每一个活动 key: start_endTime
long start = seckillSessionEntity.getStartTime().getTime();
long end = seckillSessionEntity.getEndTime().getTime();
// 生成Key
String key = SeckillConstant.SESSION_CHACE_PREFIX+start+"_"+end;
Boolean flag = redisTemplate.hasKey(key);
if(!flag){// 表示这个秒杀活动在Redis中不存在,也就是还没有上架,那么需要保存
// 需要存储到Redis中的这个秒杀活动涉及到的相关的商品信息的SKUID
List<String> collect = seckillSessionEntity.getRelationEntities().stream().map(item -> {
// 秒杀活动存储的 VALUE是 sessionId_SkuId
return item.getPromotionSessionId()+"_"+item.getSkuId().toString();
}).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,collect);
}
}
}
/**
* 存储活动对应的 SKU信息
* @param seckillSessionEntities
*/
private void saveSessionSkuInfos(List<SeckillSessionEntity> seckillSessionEntities) {
seckillSessionEntities.stream().forEach(session -> {
// 循环取出每个Session,然后取出对应SkuID 封装相关的信息
BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
session.getRelationEntities().stream().forEach(item->{
String skuKey = item.getPromotionSessionId()+"_"+item.getSkuId();
Boolean flag = redisTemplate.hasKey(skuKey);
if(!flag){
SeckillSkuRedisDto dto = new SeckillSkuRedisDto();
// 1.获取SKU的基本信息
R info = productFeignService.info(item.getSkuId());
if(info.getCode() == 0){
// 表示查询成功
String json = (String) info.get("skuInfoJSON");
dto.setSkuInfoVo(JSON.parseObject(json,SkuInfoVo.class));
}
// 2.获取SKU的秒杀信息
/*dto.setSkuId(item.getSkuId());
dto.setSeckillPrice(item.getSeckillPrice());
dto.setSeckillCount(item.getSeckillCount());
dto.setSeckillLimit(item.getSeckillLimit());
dto.setSeckillSort(item.getSeckillSort());*/
BeanUtils.copyProperties(item,dto);
// 3.设置当前商品的秒杀时间
dto.setStartTime(session.getStartTime().getTime());
dto.setEndTime(session.getEndTime().getTime());
// 4. 随机码
String token = UUID.randomUUID().toString().replace("-","");
dto.setRandCode(token);
// 分布式信号量的处理 限流的目的
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + token);
// 把秒杀活动的商品数量作为分布式信号量的信号量
semaphore.trySetPermits(item.getSeckillCount().intValue());
hashOps.put(skuKey,JSON.toJSONString(dto));
}
});
});
}
通过OpenFegin 远程调用Coupon服务中接口来获取未来三天的秒杀活动的商品
在秒杀服务中,创建fegin调用接口,去调用Coupon的接口服务
package com.msb.mall.feign;
@FeignClient("mall-coupon")
public interface CouponFeignService {
@GetMapping("/coupon/seckillsession/getLates3DaysSession")
public R getLates3DaysSession();
}
Coupon服务也就是会员服务中创建接口,获取会员可以调用的 秒杀活动商品信息
controller层
@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {
@Autowired
private SeckillSessionService seckillSessionService;
@GetMapping("/getLates3DaysSession")
public R getLates3DaysSession(){
List<SeckillSessionEntity> lates3DaysSession = seckillSessionService.getLates3DaysSession();
String json = JSON.toJSONString(lates3DaysSession);
return R.ok().put("data",json);
}
}
service层
@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {
@Autowired
SeckillSkuRelationService relationService;
@Override
public List<SeckillSessionEntity> getLates3DaysSession() {
// 计算未来3天的时间
List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().
between("start_time",startTime(),endTime()));
List<SeckillSessionEntity> newList = list.stream().map(session -> {
// 根据对应的sessionId活动编号查询出对应的活动商品信息
List<SeckillSkuRelationEntity> relationEntities = relationService.list(new QueryWrapper<SeckillSkuRelationEntity>()
.eq("promotion_session_id", session.getId()));
session.setRelationEntities(relationEntities);
return session;
}).collect(Collectors.toList());
return newList;
}
private String startTime(){
LocalDate now = LocalDate.now();
LocalDate startDay = now.plusDays(0);
LocalTime min = LocalTime.MIN;
LocalDateTime start = LocalDateTime.of(startDay, min);
return start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
private String endTime(){
LocalDate now = LocalDate.now();
LocalDate endDay = now.plusDays(2);
LocalTime max = LocalTime.MAX;
LocalDateTime end = LocalDateTime.of(endDay, max);
return end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
启动服务,数据会被保存到Redis中
二、秒杀商品查询
通过当前时间获取对应的秒杀活动及对应的SKU信息。
/**
* 查询出当前时间内的秒杀活动及对应的商品SKU信息
* @return
*/
@Override
public List<SeckillSkuRedisDto> getCurrentSeckillSkus() {
// 1.确定当前时间是属于哪个秒杀活动的
long time = new Date().getTime();
// 从Redis中查询所有的秒杀活动
Set<String> keys = redisTemplate.keys(SeckillConstant.SESSION_CHACE_PREFIX + "*");
for (String key : keys) {
//seckill:sessions1656468000000_1656469800000
String replace = key.replace(SeckillConstant.SESSION_CHACE_PREFIX, "");
// 1656468000000_1656469800000
String[] s = replace.split("_");
Long start = Long.parseLong(s[0]); // 活动开始的时间
Long end = Long.parseLong(s[1]); // 活动结束的时间
if(time > start && time < end){
// 说明的秒杀活动就是当前时间需要参与的活动
// 取出来的是SKU的ID 2_9
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
List<String> list = ops.multiGet(range);
if(list != null && list.size() > 0){
List<SeckillSkuRedisDto> collect = list.stream().map(item -> {
SeckillSkuRedisDto seckillSkuRedisDto = JSON.parseObject(item, SeckillSkuRedisDto.class);
return seckillSkuRedisDto;
}).collect(Collectors.toList());
return collect;
}
}
}
return null;
}
然后定义相关的Controller接口就可以访问了
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
SeckillService seckillService;
@GetMapping("/currentSeckillSessionSkus")
public R getCurrentSeckillSessionSkus(){
List<SeckillSkuRedisDto> currentSeckillSkus = seckillService.getCurrentSeckillSkus();
return R.ok().put("data", JSON.toJSONString(currentSeckillSkus));
}
}
然后对应的访问效果:
三、页面渲染
1.网关配置
首先在host中配置域名
然后在网关中配置路由信息
然后重启服务访问:
能访问到数据就表示域名配置成功
2.首页配置
通过Ajax来访问获取秒杀的相关信息
$.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp){
if(resp.data.length > 0){
// 说明有秒杀的数据
console.log($.parseJSON(resp.data))
$.parseJSON(resp.data).forEach(function(item){
$("<li></li>").append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
.append("<p>"+item.skuInfoVo.skuSubtitle+"</p>")
.append("<span>"+item.seckillPrice+"</span>")
.append("<s>"+item.skuInfoVo.price+"</s>")
.appendTo("#seckillSessionContent");
})
/*<li>
<img src="/static/index/img/section_second_list_img1.jpg" alt="">
<p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千克) (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千</p>
<span>¥83.9</span><s>¥99.9</s>
</li>*/
}
})
展示的效果
3.商品详情
在购买商品的时候,进入到商品详情页,如果该商品也参与了秒杀活动,那么对应的需要展示相关的信息
首先我们需要在秒杀服务中提供一个根据SKUID查询相关的秒杀活动的接口
/**
* 根据SKUID查询秒杀活动对应的信息
* @param skuId
* @return
*/
@Override
public SeckillSkuRedisDto getSeckillSessionBySkuId(Long skuId) {
// 1.找到所有需要参与秒杀的商品的sku信息
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
Set<String> keys = ops.keys();
if(keys != null && keys.size() > 0){
String regx = "\\d_"+ skuId;
for (String key : keys) {
boolean matches = Pattern.matches(regx, key);
if(matches){
// 说明找到了对应的SKU的信息
String json = ops.get(key);
SeckillSkuRedisDto dto = JSON.parseObject(json, SeckillSkuRedisDto.class);
return dto;
}
}
}
return null;
}
然后在查询商品详情的时候异步查询出对应的秒杀活动信息
然后在模板页面中展示相关的信息
<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">
<span th:if="${#dates.createNow().getTime() < item.seckillVO.startTime}">
商品将在:[[${#dates.format(new java.util.Date(item.seckillVO.startTime),'yyyy-MM-dd HH:mm:ss')}]] 开始秒杀
</span>
<span th:if="${#dates.createNow().getTime() > item.seckillVO.startTime
&& #dates.createNow().getTime() < item.seckillVO.endTime }">
秒杀价: [[${#numbers.formatDecimal(item.seckillVO.seckillPrice,1,2)}]]
</span>
</li>
<li>
<a href="/static/item/">
预约说明
</a>
</li>
</ul>
</div>
首页调整到商品详情页
function goItem(skuId){
location.href="http://item.msb.com/"+skuId+".html"
}
$.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp){
if(resp.data.length > 0){
// 说明有秒杀的数据
console.log($.parseJSON(resp.data))
$.parseJSON(resp.data).forEach(function(item){
$("<li οnclick='goItem("+item.skuId+")'></li>")
.append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
.append("<p>"+item.skuInfoVo.skuSubtitle+"</p>")
.append("<span>"+item.seckillPrice+"</span>")
.append("<s>"+item.skuInfoVo.price+"</s>")
.appendTo("#seckillSessionContent");
})
/*<li>
<img src="/static/index/img/section_second_list_img1.jpg" alt="">
<p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千克) (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千</p>
<span>¥83.9</span><s>¥99.9</s>
</li>*/
}
})