1)、添加网关
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon
@RequestMapping("/list")
public R list(@RequestParam Map params){
PageUtils page = seckillSkuRelationService.queryPage(params);
return R.ok().put("page", page);
}
4)、测试后台秒杀服务的增删改查功能
二、秒杀上架1)、配置定时推送功能
@EnableAsync
@EnableScheduling
@Configuration
public class ScheduledConfig {
}
2)、定时推送秒杀
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
//TODO 幂等性处理
// @Scheduled(cron = "*/3 * * * * ?")
@Scheduled(cron = "0 * * * * ?") //每分钟执行一次吧,上线后调整为每天晚上3点执行
// @Scheduled(cron = "0 0 3 * * ?") 线上模式
public void uploadSeckillSkuLatest3Days(){
//1、重复上架无需处理
log.info("上架秒杀的商品信息...");
// 分布式锁。锁的业务执行完成,状态已经更新完成。释放锁以后。其他人获取到就会拿到最新的状态。
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try{
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}
}
3)、上架秒杀功能
@Override
public void uploadSeckillSkuLatest3Days() {
//1、扫描最近三天需要参与秒杀的活动
R session = couponFeignService.getLates3DaySession();
if (session.getCode() == 0) {
//上架商品
List sessionData = session.getData(new TypeReference>() {
});
//缓存到redis
//1、缓存活动信息
saveSessionInfos(sessionData);
//2、缓存活动的关联商品信息
saveSessionSkuInfos(sessionData);
}
}
4)、获取最近三天的秒杀
@GetMapping("/lates3DaySession")
public R getLates3DaySession(){
List sessions = seckillSessionService.getLates3DaySession();
return R.ok().setData(sessions);
}
5)、缓存活动信息
private void saveSessionInfos(Listsesssions) { if (sesssions != null){ sesssions.stream().forEach(sesssion -> { Long startTime = sesssion.getStartTime().getTime(); Long endTime = sesssion.getEndTime().getTime(); String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime; Boolean hasKey = redisTemplate.hasKey(key); if (!hasKey) { List collect = sesssion.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString()).collect(Collectors.toList()); //缓存活动信息 redisTemplate.opsForList().leftPushAll(key, collect); //TODO 设置过期时间[已完成] redisTemplate.expireAt(key, new Date(endTime)); } }); } }
6)、缓存活动的关联商品信息
private void saveSessionSkuInfos(List三、秒杀上架sesssions) { if (sesssions != null){ sesssions.stream().forEach(sesssion -> { //准备hash操作 BoundHashOperations ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); sesssion.getRelationSkus().stream().forEach(seckillSkuVo -> { //4、随机码? seckill?skuId=1&key=dadlajldj; String token = UUID.randomUUID().toString().replace("-", ""); 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 () { }); redisTo.setSkuInfo(info); } //2、sku的秒杀信息 BeanUtils.copyProperties(seckillSkuVo, redisTo); //3、设置上当前商品的秒杀时间信息 redisTo.setStartTime(sesssion.getStartTime().getTime()); redisTo.setEndTime(sesssion.getEndTime().getTime()); redisTo.setRandomCode(token); String jsonString = JSON.toJSonString(redisTo); //TODO 每个商品的过期时间不一样。所以,我们在获取当前商品秒杀信息的时候,做主动删除,代码在 getSkuSeckillInfo 方法里面 ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString); //如果当前这个场次的商品的库存信息已经上架就不需要上架 //5、使用库存作为分布式的信号量 限流; RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); //商品可以秒杀的数量作为信号量 semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); //TODO 设置过期时间。 semaphore.expireAt(sesssion.getEndTime()); } }); }); } }
1)、获取秒杀信息的json
@ResponseBody
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus(){
log.info("currentSeckillSkus正在执行。。。");
List vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
2)、在首页添加秒杀信息
function to_href(skuId){
location.href = "http://item.gulimall.com/"+skuId+".html";
}
$.get("http://seckill.gulimall.com/currentSeckillSkus",function(resp){
if(resp.data.length > 0){
resp.data.forEach(function(item){
$("")
.append($(""))
.append($(""+item.skuInfo.skuTitle+"
")) .append($(""+item.seckillPrice+"")) .append($("3)、秒杀页面的渲染
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
SecKillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}
4)、查询当前商品是否参与秒杀
//3、查询当前sku是否参与秒杀优惠
CompletableFuture secKillFuture = CompletableFuture.runAsync(() -> {
R seckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
if (seckillInfo.getCode() == 0) {
SeckillInfoVo seckillInfoVo = seckillInfo.getData(new TypeReference() {
});
skuItemVo.setSeckillInfo(seckillInfoVo);
}
}, executor);
如果有秒杀,就在价格旁进行回显
四、秒杀流程1)、登录验证
1.引入依赖
2.配置应用session和redis
3.添加拦截器和拦截配置
2)、秒杀业务
@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);
//1、判断是否登录
return "success";
}
3)、业务实现
// TODO 上架秒杀商品的时候,每一个数据都有过期时间。
// TODO 秒杀后续的流程,简化了收货地址等信息。
@Override
public String kill(String killId, String key, Integer num) {
long s1 = System.currentTimeMillis();
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
//1、获取当前秒杀商品的详细信息
BoundHashOperations 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);
//校验合法性
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
long time = new Date().getTime();
long ttl = endTime - time;
//1、校验时间的合法性
if (time >= startTime && time <= endTime) {
//2、校验随机码和商品id
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
//3、验证购物数量是否合理
if (num <= redis.getSeckillLimit()) {
//4、验证这个人是否已经购买过。幂等性; 如果只要秒杀成功,就去占位。 userId_SessionId_skuId
//SETNX
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);
//120 20ms
boolean b = semaphore.tryAcquire(num);
if (b) {
//秒杀成功;
//快速下单。发送MQ消息 10ms
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);
long s2 = System.currentTimeMillis();
log.info("耗时...{}", (s2 - s1));
return timeId;
}
return null;
} else {
//说明已经买过了
return null;
}
}
} else {
return null;
}
} else {
return null;
}
}
return null;
}
4)、监听秒杀订单
@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("准备创建秒杀单的详细信息。。。");
orderService.createSeckillOrder(seckillOrder);
//手动调用支付宝收单;
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}



