文章目录
- 秒杀流程
- cubemall-common
- SeckillOrderTo.java
- cubemall-seckill
- SeckillController.java
- SeckillServiceImpl.java
- success.html
- pom.xml
秒杀流程
cubemall-common
SeckillOrderTo.java
package com.xd.cubemall.common.to;import lombok.Data;import java.io.Serializable;
import java.math.BigDecimal;/****/@Data
public class SeckillOrderTo implements Serializable {/*** 订单号*/private String orderSn;/*** 活动场次id*/private Long promotionSessionId;/*** 商品id*/private Long skuId;/*** 秒杀价格*/private BigDecimal seckillPrice;/*** 购买数量*/private Integer num;/*** 会员ID*/private Long memberId;}
cubemall-seckill
SeckillController.java
package com.xd.cubemall.seckill.controller;import com.xd.cubemall.common.utils.R;
import com.xd.cubemall.seckill.service.SeckillService;
import com.xd.cubemall.seckill.to.SeckillSkuRedisTo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;import java.util.List;@Controller
public class SeckillController {@Autowiredprivate SeckillService seckillService;@GetMapping(value = "/getCurrentSeckillSkus")@ResponseBodypublic R getCurrentSeckillSkus(){List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();return R.ok().setData(vos);}@GetMapping(value = "/uploadSeckillSkuLatest3Days")@ResponseBodypublic R uploadSeckillSkus(){seckillService.uploadSeckillSkuLateset3Days();return R.ok();}@GetMapping(value = "/sku/seckill/{skuId}")@ResponseBodypublic R getSeckillSkuRedisTo(@PathVariable("skuId") Long skuId){SeckillSkuRedisTo vos = seckillService.getSeckillSkuRedisTo(skuId);return R.ok().setData(vos);}@GetMapping(value = "/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);return "success";}}
SeckillServiceImpl.java
package com.xd.cubemall.seckill.service.impl;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.sun.org.apache.xpath.internal.operations.Bool;
import com.xd.cubemall.common.to.SeckillOrderTo;
import com.xd.cubemall.common.utils.R;
import com.xd.cubemall.common.vo.MemberResponseVo;
import com.xd.cubemall.seckill.feign.CouponFeignService;
import com.xd.cubemall.seckill.feign.ProductFeignService;
import com.xd.cubemall.seckill.interceptor.LoginUserInterceptor;
import com.xd.cubemall.seckill.service.SeckillService;
import com.xd.cubemall.seckill.to.SeckillSkuRedisTo;
import com.xd.cubemall.seckill.vo.SeckillSessionWithSkusVo;
import com.xd.cubemall.seckill.vo.SkuInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate CouponFeignService couponFeignService;@Autowiredprivate ProductFeignService productFeignService;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate RabbitTemplate rabbitTemplate;//存储到redis,秒杀活动信息key前缀private final String SESSION_CACHE_PREFIX = "seckill:sessions:";//商品详细信息的前缀private final String SECKILL_CHARE_PREFIX = "seckill:skus";//库存的前缀private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";/*** 当前商品进行秒杀* @param killId* @param key* @param num* @return*/@Overridepublic String kill(String killId, String key, Integer num) {long s1 = System.currentTimeMillis();//获取当前登录用户MemberResponseVo user = LoginUserInterceptor.loginUser.get();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&¤tTime<=endTime){String randomCode = redisTo.getRandomCode();String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();//校验随机码和商品IDif (randomCode.equals(key) && killId.equals(skuId)){Integer seckillLimit = redisTo.getSeckillLimit();String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE+randomCode);Integer count = Integer.parseInt(seckillCount);//判断信号量是否大于0,并且购买的数量不能超过库存if (count>0&&num<=seckillLimit&& count > num){String redisKeys = user.getId() + "-" + skuId;Long ttl = endTime - currentTime;//Boolean aboolean = redisTemplate.opsForValue().setIfAbsent(redisKeys,num.toString(),ttl, TimeUnit.MILLISECONDS);Boolean aboolean = redisTemplate.opsForValue().setIfAbsent(redisKeys,num.toString(),24 * 60 * 60 * 1000L, TimeUnit.MILLISECONDS);//占位成功说明从来没有购买过if (aboolean){RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE+randomCode);try {boolean semaphoreCount = semaphore.tryAcquire(num,100,TimeUnit.MILLISECONDS);//说明还有库存if (semaphoreCount){String timeid = IdWorker.getTimeId();//TODO 发送消息到MQSeckillOrderTo 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;}} catch (InterruptedException e) {e.printStackTrace();}}}}//}return null;}//查询当前的秒杀商品列表@Overridepublic List<SeckillSkuRedisTo> getCurrentSeckillSkus() {//查询当前时间long currentTime = System.currentTimeMillis();List<SeckillSkuRedisTo> result = new ArrayList<>();//查询所有的活动场次Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX+"*");if (keys==null||keys.isEmpty()){return null;}for (String key:keys){//seckill:sessions:1622602800000_1624935600000String replace = key.replace(SESSION_CACHE_PREFIX,"");//1622602800000_1624935600000String[] array = replace.split("_");long startTime = Long.parseLong(array[0]);//开始时间long endTime = Long.parseLong(array[1]);//结束时间log.info("startTime:"+startTime+",endTime:"+endTime+",currentTime:"+currentTime);//判断当前场次是否进行秒杀//if(currentTime>=startTime&¤tTime<=endTime){//获取这个场次所有的商品信息List<String> range = redisTemplate.opsForList().range(key,-100,100);//4-18BoundHashOperations<String,String,String> hasOps= redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);List<String> listValue = hasOps.multiGet(range);if (listValue!=null&&!listValue.isEmpty()){List<SeckillSkuRedisTo> collect = listValue.stream().map(item->{String items = (String)item;SeckillSkuRedisTo redisTo = JSON.parseObject(items,SeckillSkuRedisTo.class);return redisTo;}).collect(Collectors.toList());result.addAll(collect);}//}}return result;}/*** 上架最近3天需要秒杀的商品*/@Overridepublic void uploadSeckillSkuLateset3Days() {//1.获取最近3天的需要秒杀的活动R lates3DaysSession = couponFeignService.getLates3DaySession();if(lates3DaysSession.getCode() == 0){List<SeckillSessionWithSkusVo> sessionData =lates3DaysSession.getData("data",new TypeReference<List<SeckillSessionWithSkusVo>>(){});if (sessionData==null||sessionData.size()<=0){return;}//2 缓存活动信息saveSessionInfos(sessionData);//3 缓存活动关联的商品信息saveSessionSkuInfos(sessionData);}}//缓存秒杀活动信息到Redisprivate void saveSessionInfos(List<SeckillSessionWithSkusVo> sessionData) {sessionData.stream().forEach(session->{//获取当前活动的开始时间和结束的时间戳long startTime = session.getStartTime().getTime();long endTime = session.getEndTime().getTime();//生成Redis的key值String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;log.info("saveSessionInfos-key="+key);Boolean hashKey = redisTemplate.hasKey(key);//判断Redis是否已有该活动信息,如果没有才进行添加if (!hashKey){//缓存活动信息//获取缓存value,活动ID+skuIDList<String> sessionSkuIds = session.getRelationEntities().stream().map(item->item.getPromotionSessionId()+"-"+item.getSkuId().toString()).collect(Collectors.toList());redisTemplate.opsForList().leftPushAll(key,sessionSkuIds);}});}//缓存秒杀活动关联商品信息private void saveSessionSkuInfos(List<SeckillSessionWithSkusVo> sessionData) {sessionData.stream().forEach(session->{//准备hash操作,绑定hash值BoundHashOperations<String,Object,Object> operations =redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);session.getRelationEntities().stream().forEach(seckillSkuVo -> {//构建RediskeyString redisKey = seckillSkuVo.getPromotionSessionId().toString()+"-"+seckillSkuVo.getSkuId().toString();log.info("redisKey:"+redisKey);//判断当前的key是否存在进行,如果不存在则进行缓存if(!operations.hasKey(redisKey)){//缓存sku商品信息SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();//获取到SKUIDLong skuId = seckillSkuVo.getSkuId();log.info("skuId:"+skuId);//1) 先查询sku的基本信息,通过feign远程调用R info = productFeignService.getSkuInfo(skuId);if (info.getCode()==0){SkuInfoVo skuInfoVo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});seckillSkuRedisTo.setSkuInfo(skuInfoVo);}//2) 设置sku的秒杀信息BeanUtils.copyProperties(seckillSkuVo,seckillSkuRedisTo);//3) 设置当前商品的秒杀时间seckillSkuRedisTo.setStartTime(session.getStartTime().getTime());seckillSkuRedisTo.setEndTime(session.getEndTime().getTime());//4) 设置商品的随机码(防止恶意攻击)String randcode = UUID.randomUUID().toString().replace("-","");seckillSkuRedisTo.setRandomCode(randcode);String seckillValue = JSON.toJSONString(seckillSkuRedisTo);//缓存秒杀商品的详细信息operations.put(redisKey,seckillValue);//5) 使用库存作为分布式Redisson信号量(限流)RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE+randcode);//商品秒杀数量作为信号量semaphore.trySetPermits(seckillSkuVo.getSeckillCount());}});});}/*** 根据skuid查询商品秒杀详情* @param skuId* @return*/@Overridepublic SeckillSkuRedisTo getSeckillSkuRedisTo(Long skuId) {BoundHashOperations<String,String,String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);//查询seckill:skus下所有的key值Set<String> keys = hashOps.keys();if (keys==null||keys.isEmpty()){return null;}//4-18 活动id-skuIdString reg = "\\d-"+skuId;for (String key:keys){//开始遍历if (Pattern.matches(reg,key)){//如果匹配成功,说明找到当前的skuIdString redisvalue = hashOps.get(key);//进行反序列化SeckillSkuRedisTo redisTo = JSON.parseObject(redisvalue,SeckillSkuRedisTo.class);Long currentTime = System.currentTimeMillis();Long startTime = redisTo.getStartTime();Long endTime = redisTo.getEndTime();//if (currentTime>=startTime&¤tTime<=endTime){// return redisTo;//}//redisTo.setRandomCode(null);return redisTo;}}return null;}
}
success.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"/><title>商品已成功加入购物车</title><script type="text/javascript" src="../static/static/cart/js/jquery-3.1.1.min.js"></script><script type="text/javascript" src="../static/static/cart/bootstrap/js/bootstrap.js"></script><script type="text/javascript" src="../static/static/cart/js/swiper.min.js"></script><link rel="stylesheet" type="text/css" href="../static/static/cart/css/swiper.min.css"/><link rel="stylesheet" type="text/css" href="../static/static/cart/bootstrap/css/bootstrap.css"/><link rel="stylesheet" type="text/css" href="../static/static/cart/css/success.css"/></head><body>
<!--头部-->
<div class="alert-info"><div class="hd_wrap_top"><ul class="hd_wrap_left"><li class="hd_home"><i class="glyphicon glyphicon-home"></i><a href="http://cubemall.com/">魔方商城首页</a></li></ul><ul class="hd_wrap_right"><li><a th:if="${session.loginUser == null}" href="http://auth.cubemall.com/login.html" class="li_2">请登录</a><a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a></li></ul></div>
</div><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"><h1>恭喜秒杀成功 订单号[[${orderSn}]]</h1><h1>正在准备订单数据 10秒钟自动跳转<a style="color:red" th:href="${'http://127.0.0.1:8084/alipay/order/pay?orderId='+orderSn}">点击支付</a></h1></div></div><div th:if="${orderSn==null}"><h1>手气不好,秒杀失败,下次再来</h1></div></div></div></div>
</body>
<script type="text/javascript" src="../static/static/cart/js/success.js"></script></html>
pom.xml
<!-- 引入RabbitMQ --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency>