秒杀具有瞬间高并发特点,针对这一特点,必须要做限流+异步+缓存(页面静态化)+独立部署。
限流方式:
配置域名映射及网关
192.168.139.10 seckill.gmall.com
spring:
cloud:
gateway:
routes:
- id: gmall_seckill_route
uri: lb://gmall-seckill
predicates:
- Host=seckill.gmall.com
后台管理系统:优惠营销 -> 每日秒杀
SeckillSkuScheduled
package com.atguigu.gmall.seckill.scheduled;
import com.atguigu.gmall.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 秒杀商品定时上架 {@link SeckillSkuScheduled}
* 每天凌晨3点,上架最近三天需要秒杀商品
* 当天 00:00:00 - 23:59:59
* 明天 00:00:00 - 23:59:59
* 后天 00:00:00 - 23:59:59
*
* @author zhangwen
* @email: [email protected]
*/
@Slf4j
@Service
public class SeckillSkuScheduled {
private final String UPLOAD_LOCK = "seckill:upload:lock";
@Autowired
private SeckillService seckillService;
@Autowired
private RedissonClient redissonClient;
/**
* 秒杀商品定时上架
*
* 幂等处理:
* - 1.分布式锁
* - 2.缓存数据时判断是否已经存在key
* - key不存在就缓存
* - key存在不做处理
*/
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLast3Days(){
log.info("秒杀商品定时上架...");
// 幂等处理,分布式锁
// 锁的业务执行完成,状态已经更新完成,释放锁以后,其他人获取到的就是最新的状态
RLock lock = redissonClient.getLock(UPLOAD_LOCK);
lock.lock(10, TimeUnit.SECONDS);
try {
seckillService.uploadSeckillSkuLast3Days();
} finally {
lock.unlock();
}
}
}
SeckillServcieImpl
/**
* 上架最近三天的秒杀商品
*/
@Override
public void uploadSeckillSkuLast3Days() {
// 扫描最近三天需要参与的秒杀活动与商品信息
R r = couponFeignService.getLast3DaySession();
if (r.getCode() == 0) {
// 需要上架的商品
List<SeckillSessionsWithSkusVO> sessions = r.getData("data",
new TypeReference<List<SeckillSessionsWithSkusVO>>() {
});
// 缓存秒杀活动信息
saveSessions(sessions);
// 缓存秒杀活动关联的商品信息
saveSessionSkus(sessions);
} else {
log.error("远程调用 gmall-coupon 获取秒杀活动失败");
}
}
/**
* 缓存秒杀活动信息
* @param sessions
*/
private void saveSessions(List<SeckillSessionsWithSkusVO> sessions) {
if (sessions != null && sessions.size() > 0) {
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
Boolean hasKey = redisTemplate.hasKey(key);
if (!hasKey) {
// 缓存秒杀活动信息
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "_" + item.getSkuId())
.collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key, skuIds);
}
});
}
}
/**
* 缓存秒杀活动关联的商品信息
* @param sessions
*/
private void saveSessionSkus(List<SeckillSessionsWithSkusVO> sessions){
if (sessions != null && sessions.size() > 0) {
sessions.stream().forEach(session -> {
BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(SECKILL_SKU_CACHE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVO -> {
String hashKey = seckillSkuVO.getPromotionSessionId() + "_" + seckillSkuVO.getSkuId();
if (!hashOps.hasKey(hashKey)) {
// 缓存商品
SeckillSkuRedisTO redisTO = new SeckillSkuRedisTO();
// 1.sku基本信息
R r = productFeignService.getSkuInfo(seckillSkuVO.getSkuId());
if (r.getCode() == 0) {
SkuInfoVO skuInfo = r.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.设置随机码,防止恶意攻击
String token = UUID.randomUUID().toString().replace("-", "");
redisTO.setRandomCode(token);
String json = JsonUtils.objectToJson(redisTO);
hashOps.put(hashKey, json);
**加粗样式**
// 5.使用秒杀商品库存作为分布式的信号量,限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
semaphore.trySetPermits(seckillSkuVO.getSeckillCount().intValue());
}
});
});
}
}
index.html
//获取当前秒杀场次商品
$.get('http://seckill.gmall.com/currentSeckillSkus', function(resp){
if(resp.data.length > 0){
resp.data.forEach(function(item){
$('<li onclick="toItemPage('+item.skuId+')"> </li>')
.append($('<img src="'+item.skuInfo.skuDefaultImg+'"
style="width: 130px;height: 130px">'))
.append($(''
+item.skuInfo.skuTitle+''))
.append($(''+item.seckillPrice+'</span>'))
.append($(''+item.skuInfo.price+''))
.appendTo($('#seckillSkus'))
})
}
})
function toItemPage(skuId){
location.href = `http://item.gmall.com/${skuId}.html`
}
SkuInfoServiceImpl
/**
* 商品详情
* @param skuId
* @return
*/
@Override
public SkuItemVO item(Long skuId) throws Exception {
SkuItemVO skuItemVO = new SkuItemVO();
// 异步编排
CompletableFuture<SkuInfoEntity> skuInfoFuture = CompletableFuture.supplyAsync(() -> {
// 1.sku基本信息 pms_sku_info
SkuInfoEntity skuInfo = getById(skuId);
skuItemVO.setSkuInfo(skuInfo);
return skuInfo;
}, executor);
CompletableFuture<Void> saleAttrFuture = skuInfoFuture.thenAcceptAsync((res) -> {
// 2.spu销售属性组合
List<SkuSaleAttrVO> saleAttrs = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVO.setSaleAttrs(saleAttrs);
}, executor);
CompletableFuture<Void> descFuture = skuInfoFuture.thenAcceptAsync((res) -> {
// 3.spu商品介绍
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVO.setSpuDesc(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = skuInfoFuture.thenAcceptAsync((res) -> {
// 4.spu规格参数
List<SpuAttrGroupVO> groupAttrs = attrGroupService.getAttrGroupWithAttrsBySpuId(
res.getCatalogId(), res.getSpuId());
skuItemVO.setGroupAttrs(groupAttrs);
}, executor);
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
// 5.sku图片信息 pms_sku_images
List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
skuItemVO.setImages(images);
}, executor);
CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
// 查询当前sku是否参与秒杀优惠
R r = seckillFeignService.getSkuSeckillInfo(skuId);
if (r.getCode() == 0) {
SeckillSkuVO seckillSkuVO = r.getData("data", new TypeReference<SeckillSkuVO>() {
});
skuItemVO.setSeckillInfo(seckillSkuVO);
} else {
log.error("远程调用 gmall-seckill 获取商品秒杀信息失败");
}
}, executor);
// 等待所有任务执行完
CompletableFuture.allOf(saleAttrFuture, descFuture, baseAttrFuture, imageFuture, seckillFuture).get();
// TODO 查询库存
skuItemVO.setHasStock(true);
return skuItemVO;
}
item.html
<li th:if="${skuItemVO.seckillInfo!=null}" style="color: red">
<span th:if="${#dates.createNow().getTime() >
商品将会在[[${#dates.format(new java.util.Date(skuItemVO.seckillInfo.startTime), 'yyyy-MM-dd HH:mm:ss')}]]进行秒杀
</span>
<span th:if="${#dates.createNow().getTime() >= skuItemVO.seckillInfo.startTime && #dates.createNow().getTime() <= skuItemVO.seckillInfo.endTime}">
秒杀价:[[${#numbers.formatDecimal(skuItemVO.seckillInfo.seckillPrice,1,2)}]]
</span>
</li>
<div class="box-btns-two" th:if="${skuItemVO.seckillInfo!=null && (#dates.createNow().getTime() >= skuItemVO.seckillInfo.startTime && #dates.createNow().getTime() <= skuItemVO.seckillInfo.endTime)}">
<a href="#" id="toSeckill" th:attr="skuId=${skuItemVO.skuInfo.skuId},sessionId=${skuItemVO.seckillInfo.promotionSessionId},code=${skuItemVO.seckillInfo.randomCode}">立即抢购</a>
</div>
<div class="box-btns-two" th:if="${skuItemVO.seckillInfo==null || (#dates.createNow().getTime() < skuItemVO.seckillInfo.startTime || #dates.createNow().getTime() > skuItemVO.seckillInfo.endTime)}">
<a href="#" id="addToCart" th:attr="skuId=${skuItemVO.skuInfo.skuId}">加入购物车</a>
</div>
<script>
// 立即抢购
$('#toSeckill').click(function(){
let isLogin = [[${session.loginUser!=null}]]
if(isLogin){
let killId = $(this).attr('sessionId') + "_" + $(this).attr('skuId')
let code = $(this).attr('code')
let num = $('#num').val()
location.href = `http://seckill.gmall.com/seckill?killId=${killId}&key=${code}&num=${num}`
} else {
alert('秒杀商品,请先登录!')
location.href = 'http://auth.gmall.com/login.html'
}
return false
})
</script>
SeckillController
/**
* 秒杀
* @param killId sessionId_skuId
* @param key 商品随机码
* @param num 秒杀数量
* @return
*/
@GetMapping("/seckill")
public String seckill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num,
Model model){
String orderSn = seckillService.seckill(killId, key, num);
model.addAttribute("orderSn", orderSn);
return "success";
}
SeckillServcieImpl
/**
* 秒杀
* @param killId 秒杀场次id_商品id
* @param key 随机码
* @param num 商品数量
* @return
*/
@Override
public String seckill(String killId, String key, Integer num) {
MemberVO memberVO = LoginInterceptor.threadLocal.get();
// 获取当前秒杀商品信息
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_SKU_CACHE_PREFIX);
String json = hashOps.get(killId);
if (StringUtils.isEmpty(json)) {
return null;
}
SeckillSkuRedisTO redisTO = JsonUtils.jsonToPojo(json, SeckillSkuRedisTO.class);
// 校验合法性
// 1.校验时间
long currentTime = System.currentTimeMillis();
if (currentTime >= redisTO.getStartTime() && currentTime <= redisTO.getEndTime()) {
// 2.校验随机码和商品id
String randomCode = redisTO.getRandomCode();
String skuId = redisTO.getPromotionSessionId() + "_" + redisTO.getSkuId();
if (randomCode.equals(key) && skuId.equals(killId)) {
// 3.校验购物数量
if (num <= redisTO.getSeckillLimit().intValue()) {
// 4.验证是否购买过
// 幂等性,只要秒杀成功,就去redis占位 SETNX,userId_sessionId_skuId
String redisKey = memberVO.getId() + "_" + skuId;
Long ttl = redisTO.getEndTime() - redisTO.getStartTime();
Boolean ifAbsent = redisTemplate.opsForValue()
.setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (ifAbsent) {
// 占位成功,说明没有购买过
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
// 快速尝试
boolean acquire = semaphore.tryAcquire(num);
if (acquire) {
// 秒杀成功,快速下单,发消息到MQ
String orderSn = IdWorker.getTimeId();
SeckillOrderTO seckillOrderTO = new SeckillOrderTO();
seckillOrderTO.setOrderSn(orderSn);
seckillOrderTO.setMemberId(memberVO.getId());
seckillOrderTO.setNum(num);
seckillOrderTO.setPromotionSessionId(redisTO.getPromotionSessionId());
seckillOrderTO.setSkuId(redisTO.getSkuId());
seckillOrderTO.setSeckillPrice(redisTO.getSeckillPrice());
rabbitTemplate.convertAndSend(SECKILL_ORDER_EVENT_EXCHANGE,
SECKILL_ORDER_QUEUE_ROUTING_KEY, seckillOrderTO);
return orderSn;
}
}
}
}
}
return null;
}
success.html
<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">
<h3 style="margin: 20px 0px">恭喜,秒杀成功,订单号:[[${orderSn}]]</h3>
<p>
<a th:href="'http://order.gmall.com/payOrder?orderSn='+${orderSn}" id="pay">
正在准备订单数据,请您耐心等待 <span id="payTime">10</span> 秒后进行支付!
</a>
</p>
</div>
<div th:if="${orderSn==null}" class="mc success-cont">
<h3 style="margin: 20px 0px">手气不佳,秒杀失败,下次再来!</h3>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
// 倒计时跳转支付
$(function () {
let href = $('#pay').attr('href')
$('#pay').removeAttr('href')
$('#pay').attr('disabled', true)
let orderSn = [[${orderSn}]]
let count = 10
let countdown = setInterval(CountDown, 1000)
function CountDown() {
$("#payTime").text(count)
if (count == 0) {
clearInterval(countdown)
$('#pay').text('支付订单')
$('#pay').attr('href', href)
$('#pay').removeAttr('disabled')
}
count--;
}
});
</script>
语法: 秒 分 时 日 月 周 年 (年,Spring不支持)
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
@Slf4j
@Component
@EnableScheduling //开启定时任务
public class MyScheduled {
/**
* 1.Spring中6位组成,不允许第7位的年
* 2.在周几的位置,1-7表示周一到周日,和quartz有区别(1-7表示周日到周六)
*/
@Scheduled(cron = "* * * ? * 5")
public void hello(){
log.info("hello ю ")
}
}
注意:
Spring中6位组成,不允许第7位的年
在周几的位置,1-7表示周一到周日,和quartz有区别(1-7表示周日到周六)
解决定时任务不阻塞,默认是阻塞的
@Scheduled(cron = "* * * ? * 5")
public void hello(){
log.info("hello ю ");
//异步方式运行
CompletableFuture.runAsync(() Ѷ ۏ {
xxxServcie.method();
});
}
@Slf4j
@Component
@EnableAsync //开启异步任务
@EnableScheduling //开启定时任务
public class MyScheduled {
/**
* 1.Spring中6位组成,不允许第7位的年
* 2.在周几的位置,1-7表示周一到周日,和quartz有区别(1-7表示周日到周六)
*/
@Async
@Scheduled(cron = "* * * ? * 5")
public void hello(){
log.info("hello ... ");
}
}