1. 定时任务启动秒杀活动
(1)使用定时任务开启秒杀活动;
(2)在分布式场景中,需要先获取锁,然后执行上架操作,最后释放锁;
(3)其他任务在获取锁后如果商品已经上架,那么就不用再次上架;
(4)使用redis缓存秒杀活动信息和活动相关的商品信息;
@Slf4j
@Service
public class SeckillScheduled {
@Autowired
private SeckillService seckillService;
@Autowired
private RedissonClient redissonClient;
//秒杀商品上架功能的锁
private final String upload_lock = "seckill:upload:lock";
// @Scheduled(cron = "0 0 3 * * ?")
// @Scheduled(cron = "*/10 * * * * ?")
public void uploadSeckillSkuLatest3Days() {
log.info("商品秒杀信息上架");
//在分布式的场景下,需要先获取到锁,然后执行上架操作,最后释放锁。
//其他任务在获取锁后,如果商品已经上架,那么就不用再次上架了
RLock lock = redissonClient.getLock(upload_lock);
try {
lock.lock(10, TimeUnit.SECONDS);
seckillService.uploadSeckillSkuLatest3Days();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
private final String SESSION__CACHE_PREFIX = "seckill:sessions:";
private final String SECKILL_CHARE_PREFIX = "seckill:skus";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码
@Override
public void uploadSeckillSkuLatest3Days() {
//1、扫描最近三天的商品需要参加秒杀的活动
R lates3DaySession = couponFeignService.getLates3DaySession();
if (lates3DaySession.getCode() == 0) {
//上架商品
List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
});
//缓存到Redis
//1、缓存活动信息
saveSessionInfos(sessionData);
//2、缓存活动的关联商品信息
saveSessionSkuInfo(sessionData);
}
}
/**
* 缓存秒杀活动信息
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
sessions.stream().forEach(session -> {
//获取当前活动的开始和结束时间的时间戳
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
//存入到Redis中的key
String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;
//判断Redis中是否有该信息,如果没有才进行添加
Boolean hasKey = redisTemplate.hasKey(key);
//缓存活动信息
if (!hasKey) {
//获取到活动中所有商品的skuId
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,skuIds);
}
});
}
/**
* 缓存秒杀活动所关联的商品信息
* @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());
}
});
});
}
(1)在秒杀实体缓存中设置随机码token,在秒杀前端请求时需要携带token才可以执行请求,用来防止恶意攻击;
(2)使用库存量作为分布式redisson信息量,只有获取到信号量才可以执行库存操作,减轻数据库压力
2. 商品抢购
(1)当商品在秒杀活动中时,执行抢购请求,不在活动中时执行普通请求。
(2)在抢购请求中获取携带商品id,随机码,和商品数量信息;
<div class="box-btns-two"
th:if="${item.seckillSkuVo == null }">
<a class="addToCart" href="#" th:attr="skuId=${item.info.skuId}">
加入购物车
</a>
</div>
<div class="box-btns-two"
th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}">
<a class="seckill" href="#"
th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}">
立即抢购
</a>
</div>
$(".seckill").click(function () {
var isLogin = [[${session.loginUser != null}]]; //true
if (isLogin) {
var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");
var code = $(this).attr("code");
var num = $("#productNum").val();
location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + code + "&num=" + num;
} else {
alert("秒杀请先登录");
}
return false;
});
/**
* 商品进行秒杀(秒杀开始)
* @param killId
* @param key
* @param num
* @return
*/
@GetMapping(value = "/kill")
public String seckill(@RequestParam("killId") String killId,
@RequestParam("num") Integer num,
Model model) {
String orderSn = null;
try {
//1、判断是否登录
orderSn = seckillService.kill(killId,num);
model.addAttribute("orderSn",orderSn);
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
/**
* 当前商品进行秒杀(秒杀开始)
* @param killId
* @param key
* @param num
* @return
*/
@Override
public String kill(String killId, String key, Integer num) throws InterruptedException {
long s1 = System.currentTimeMillis();
//获取当前用户的信息
MemberResponseVo user = LoginUserInterceptor.loginUser.get();
//1、获取当前秒杀商品的详细信息从Redis中获取
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
String skuInfoValue = hashOps.get(killId);
if (StringUtils.isEmpty(skuInfoValue)) {
return null;
}
//(合法性效验)
SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long currentTime = System.currentTimeMillis();
//判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
if (currentTime >= startTime && currentTime <= endTime) {
//2、效验随机码和商品id
String randomCode = redisTo.getRandomCode();
String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
//3、验证购物数量是否合理和库存量是否充足
Integer seckillLimit = redisTo.getSeckillLimit();
//获取信号量
String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
Integer count = Integer.valueOf(seckillCount);
//判断信号量是否大于0,并且买的数量不能超过库存
if (count > 0 && num <= seckillLimit && count > num ) {
//4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
//SETNX 原子性处理
String redisKey = user.getId() + "-" + skuId;
//设置自动过期(活动结束时间-当前时间)
Long ttl = endTime - currentTime;
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功说明从来没有买过,分布式锁(获取信号量-1)
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//TODO 秒杀成功,快速下单
boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
//保证Redis中还有商品库存
if (semaphoreCount) {
//创建订单号和订单信息发送给MQ
// 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(user.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSkuId(redisTo.getSkuId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
long s2 = System.currentTimeMillis();
log.info("耗时..." + (s2 - s1));
return timeId;
}
}
}
}
}
long s3 = System.currentTimeMillis();
log.info("耗时..." + (s3 - s1));
return null;
}
(1)在秒杀时需要进行判断用户是否已经购买过,如果没有购买过才可以执行购买;
(2)没有购买的用户可以获取信号量,获取到信号量后,信号量会减去当前购买的数量;
(3)如果购买成功,会执行创建订单操作,保存订单项信息,保存商品sku信息等操作,整个操作会发送到mq中来执行,这样整个秒杀订单的请求都会在redis中来执行,耗时较短。
每执行一次下单请求,信号量会-1,同时会保存当前userId-sessionId-skuId,记录当前用户是否已经抢购过。