一、概述
如下图秒杀活动:
在这个秒杀活动中,需要自动上架一定时间段的商品,我们如何实现自动上传呢?
我们可以通过定时任务来实现的。在秒杀活动开始前,需要将商品信息存储到数据库中,并设置好库存和价格等信息。然后,可以通过定时任务的方式,每天定时从数据库中读取商品信息,并将其上传到秒杀页面上。这样,就可以实现自动上传商品的功能了。
二、Springboot定时任务配置
由于秒杀活动涉及的商品比较多,采用异步上传的方式
@EnableAsync
// 开启定时任务
@EnableScheduling
// 配置类
@Configuration
public class ScheduledConfig {
}
三、秒杀表设计
1. 秒杀场次表,主要展示哪个时间段进行秒杀
CREATE TABLE `kmall_coupon`.`sms_seckill_session` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '场次名称',
`start_time` datetime(0) NULL DEFAULT NULL COMMENT '每日开始时间',
`end_time` datetime(0) NULL DEFAULT NULL COMMENT '每日结束时间',
`status` tinyint(1) NULL DEFAULT NULL COMMENT '启用状态',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀活动场次' ROW_FORMAT = Dynamic;
2 秒杀商品表,主要存储哪个时间段内进行秒杀的商品
与场次关联的字段是 promotion_session_id
四、具体业务实现
1. 流程图
2. 开启定时任务,上传商品到redis
由于是异步开启定时任务,在上架商品时采用了redisson分布式锁机制
@Slf4j
@Service
public class SeckillScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
/**
* 秒杀商品定时上架,保证幂等性问题
* 每天晚上3点,上架最近三天需要秒杀的商品
* 当天00:00:00 - 23:59:59
* 明天00:00:00 - 23:59:59
* 后天00:00:00 - 23:59:59
*/
@Scheduled(cron = "*/10 * * * * ? ")
public void uploadSeckillSkuLatest3Days() {
// 重复上架无需处理
log.info("上架秒杀的商品...");
// 分布式锁(幂等性)
RLock lock = redissonClient.getLock(SeckillConstant.UPLOAD_LOCK);
try {
lock.lock(10, TimeUnit.SECONDS);
// 上架最近三天需要秒杀的商品
seckillService.uploadSeckillSkuLatest3Days();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
3. 上架最近三天需要秒杀的商品
- 查询最近三天需要参加秒杀的场次+商品
- 上架场次信息
- 上架商品信息
@Override
public void uploadSeckillSkuLatest3Days() {
// 1.查询最近三天需要参加秒杀的场次+商品
R lates3DaySession = couponFeignService.getLates3DaySession();
if (lates3DaySession.getCode() == 0) {
// 获取场次
List<SeckillSessionWithSkusTO> sessions = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusTO>>() {
});
// 2.上架场次信息
saveSessionInfos(sessions);
// 3.上架商品信息
saveSessionSkuInfo(sessions);
}
}
2.1 查询最近三天需要参加秒杀的场次+商品
- 计算最近三天起止时间
- 查询起止时间内的秒杀场次
- 组合秒杀关联的商品信息
@Override
public List<SeckillSessionEntity> getLates3DaySession() {
// 计算最近三天起止时间
String startTime = DateUtils.currentStartTime();// 当天00:00:00
String endTime = DateUtils.getTimeByOfferset(2);// 后天23:59:59
// 查询起止时间内的秒杀场次
List<SeckillSessionEntity> sessions = baseMapper.selectList(new QueryWrapper<SeckillSessionEntity>()
.between("start_time", startTime, endTime));
// 组合秒杀关联的商品信息
if (!CollectionUtils.isEmpty(sessions)) {
// 组合场次ID
List<Long> sessionIds = sessions.stream().map(SeckillSessionEntity::getId).collect(Collectors.toList());
// 查询秒杀场次关联商品信息
Map<Long, List<SeckillSkuRelationEntity>> skuMap = seckillSkuRelationService
.list(new QueryWrapper<SeckillSkuRelationEntity>().in("promotion_session_id", sessionIds))
.stream().collect(Collectors.groupingBy(SeckillSkuRelationEntity::getPromotionSessionId));
sessions.forEach(session -> session.setRelationSkus(skuMap.get(session.getId())));
}
return sessions;
}
2.2 上架场次信息
- 遍历场次
- 判断场次是否已上架(幂等性)
- 封装场次信息
- 上架 redisTemplate.opsForList().leftPushAll(key, skuIds);
private void saveSessionInfos(List<SeckillSessionWithSkusTO> sessions) {
if (!CollectionUtils.isEmpty(sessions)) {
sessions.stream().forEach(session -> {
// 1.遍历场次
long startTime = session.getStartTime().getTime();// 场次开始时间戳
long endTime = session.getEndTime().getTime();// 场次结束时间戳
String key = SeckillConstant.SESSION_CACHE_PREFIX + startTime + "_" + endTime;// 场次的key
// 2.判断场次是否已上架(幂等性)
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey) {
// 未上架
// 3.封装场次信息
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString())
.collect(Collectors.toList());// skuId集合
// 4.上架
redisTemplate.opsForList().leftPushAll(key, skuIds);
}
});
}
}
2.3 上架商品信息
- 查询所有商品信息
- 将查询结果封装成Map集合
- 绑定秒杀商品hash
- 遍历场次,遍历商品,判断商品是否已上架(幂等性)
- 封装商品信息
- 上架商品(序列化成json格式存入Redis中)
- 上架商品的分布式信号量,key:商品随机码 值:库存(限流)
private void saveSessionSkuInfo(List<SeckillSessionWithSkusTO> sessions) {
if (!CollectionUtils.isEmpty(sessions)) {
// 查询所有商品信息
List<Long> skuIds = new ArrayList<>();
sessions.stream().forEach(session -> {
List<Long> ids = session.getRelationSkus().stream().map(SeckillSkuVO::getSkuId).collect(Collectors.toList());
skuIds.addAll(ids);
});
R info = productFeignService.getSkuInfos(skuIds);
if (info.getCode() == 0) {
// 将查询结果封装成Map集合
Map<Long, SkuInfoTO> skuMap = info.getData(new TypeReference<List<SkuInfoTO>>() {
}).stream().collect(Collectors.toMap(SkuInfoTO::getSkuId, val -> val));
// 绑定秒杀商品hash
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SeckillConstant.SECKILL_CHARE_KEY);
// 1.遍历场次
sessions.stream().forEach(session -> {
// 2.遍历商品
session.getRelationSkus().stream().forEach(seckillSku -> {
// 判断商品是否已上架(幂等性)
String skuKey = seckillSku.getPromotionSessionId().toString() + "_" + seckillSku.getSkuId().toString();// 商品的key(需要添加场次ID前缀,同一款商品可能场次不同)
if (!operations.hasKey(skuKey)) {
// 未上架
// 3.封装商品信息
SeckillSkuRedisTO redisTo = new SeckillSkuRedisTO();// 存储到redis的对象
SkuInfoTO sku = skuMap.get(seckillSku.getSkuId());
BeanUtils.copyProperties(seckillSku, redisTo);// 商品秒杀信息
redisTo.setSkuInfo(sku);// 商品详细信息
redisTo.setStartTime(session.getStartTime().getTime());// 秒杀开始时间
redisTo.setEndTime(session.getEndTime().getTime());// 秒杀结束时间
// 商品随机码:用户参与秒杀时,请求需要带上随机码(防止恶意攻击)
String token = UUID.randomUUID().toString().replace("-", "");// 商品随机码(随机码只会在秒杀开始时暴露)
redisTo.setRandomCode(token);// 设置商品随机码
// 4.上架商品(序列化成json格式存入Redis中)
String jsonString = JSONObject.toJSONString(redisTo);
operations.put(skuKey, jsonString);
// 5.上架商品的分布式信号量,key:商品随机码 值:库存(限流)
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + token);
// 信号量(扣减成功才进行后续操作,否则快速返回)
semaphore.trySetPermits(seckillSku.getSeckillCount());
}
});
});
}
}
}