秒杀活动的最大特点就是高并发而且是短时间内的高并发,那么对我们的服务要求就非常高,针对这种情况所产生的共性问题,对应的解决方案:
当我们点击 秒杀抢购
按钮后,对应我们需要把当前的商品信息提交到后端服务。活动编号+“_”+SkuId,Code随机码,抢购商品的数量。
<div class="box-btns-two" th:if="${#dates.createNow().getTime() < item.seckillVO.startTime
|| #dates.createNow().getTime() > item.seckillVO.endTime }">
<a href="#" id="addCart" th:attr="skuId=${item.info.skuId}">
加入购物车
a>
div>
<div class="box-btns-two" th:if="${#dates.createNow().getTime() > item.seckillVO.startTime
&& #dates.createNow().getTime() < item.seckillVO.endTime }">
<a href="#" id="seckillId" th:attr="skuId=${item.info.skuId},sessionId=${item.seckillVO.promotionSessionId},code=${item.seckillVO.randCode}">
抢购商品
a>
div>
对应的js操作
$("#seckillId").click(function(){
var isLogin = [[${session.loginUser !=null}]]
if(isLogin){
// 1. 获取活动编号和SkuId 2_10
var killId = $(this).attr("sessionId") + "_" + $(this).attr("skuId");
// 2. 获取对应的随机码
var code = $(this).attr("code");
// 3. 获取秒杀的商品数量
var num = $("#numInput").val();
location.href="http://seckill.msb.com/seckill/kill?killId="+killId + "&code="+code+"&num="+num;
}else{
alert("请先登录才能参加秒杀活动!!!");
}
return false;
});
访问测试:
前端提交的秒杀请求,在后端具体的处理
秒杀活动必须是在登录状态下进行的,如果没有认证就不让秒杀。这时我们需要整合进来SpringSession。 当然我们一般是把用户信息存在reids缓存,所以redis依赖也是要引入的
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
然后添加对应的配置信息
然后添加拦截器
/**
* 秒杀活动的拦截器 确认是杂登录的状态下操作的
*/
public class AuthInterceptor implements HandlerInterceptor {
public static ThreadLocal threadLocal = new ThreadLocal();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 通过HttpSession获取当前登录的用户信息
HttpSession session = request.getSession();
Object attribute = session.getAttribute(AuthConstant.AUTH_SESSION_REDIS);
if(attribute != null){
MemberVO memberVO = (MemberVO) attribute;
threadLocal.set(memberVO);
return true;
}
// 如果 attribute == null 说明没有登录,那么我们就需要重定向到登录页面
session.setAttribute(AuthConstant.AUTH_SESSION_MSG,"请先登录");
response.sendRedirect("http://auth.msb.com/login.html");
return false;
}
}
配置拦截器
@Configuration
public class MyWebInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/seckill/kill");
}
}
设置Cookie的配置
@Configuration
public class MySessionConfig {
/**
* 自定义Cookie的配置
* @return
*/
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("msb.com"); // 设置session对应的一级域名
cookieSerializer.setCookieName("msbsession");
return cookieSerializer;
}
/**
* 对存储在Redis中的数据指定序列化的方式
* @return
*/
@Bean
public RedisSerializer<Object> redisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
最后在启动类中开启
登录校验
通过拦截器处理:在秒杀活动中并不是所有的请求都是需要在登录状态下的,所有这个拦截器应该只需要拦截部分的请求。
/**
* 秒杀活动的拦截器 确认是杂登录的状态下操作的
*/
public class AuthInterceptor implements HandlerInterceptor {
public static ThreadLocal threadLocal = new ThreadLocal();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 通过HttpSession获取当前登录的用户信息
HttpSession session = request.getSession();
Object attribute = session.getAttribute(AuthConstant.AUTH_SESSION_REDIS);
if(attribute != null){
MemberVO memberVO = (MemberVO) attribute;
threadLocal.set(memberVO);
return true;
}
// 如果 attribute == null 说明没有登录,那么我们就需要重定向到登录页面
session.setAttribute(AuthConstant.AUTH_SESSION_MSG,"请先登录");
response.sendRedirect("http://auth.msb.com/login.html");
return false;
}
}
配置拦截部分请求
@Configuration
public class MyWebInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/seckill/kill");
}
}
/**
* 秒杀活动涉及的常量
*/
public class SeckillConstant {
public static final String SESSION_CHACE_PREFIX = "seckill:sessions";
public static final String SKU_CHACE_PREFIX = "seckill:skus";
public static final String SKU_STOCK_SEMAPHORE = "seckill:stock:";
}
秒杀请求接口
controller层
/**
* 秒杀抢购 跳转到success页面 显示是否秒杀成功
* killId=1_9&code=69d55333c9ec422381024d34fdfd3e85&num=1
* @return
*/
@Controller
@RequestMapping("/seckill")
public class SeckillController {
@GetMapping("/kill")
public String seckill(@RequestParam("killId") String killId,
@RequestParam("code") String code,
@RequestParam("num") Integer num,
Model model){
String orderSN = seckillService.kill(killId,code,num);
model.addAttribute("orderSn",orderSN);
return "success";
}
}
service层
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
RedissonClient redissonClient;
@Autowired
RocketMQTemplate rocketMQTemplate;
/**
* 实现秒杀逻辑
* @param killId
* @param code
* @param num
* @return
*/
@Override
public String kill(String killId, String code, Integer num) {
// 1.根据killId获取当前秒杀的商品的信息 Redis中
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
String json = ops.get(killId);
if(StringUtils.isNotBlank(json)){
SeckillSkuRedisDto dto = JSON.parseObject(json, SeckillSkuRedisDto.class);
// 校验合法性 1.校验时效性
Long startTime = dto.getStartTime();
Long endTime = dto.getEndTime();
long now = new Date().getTime();
if(now > startTime && now < endTime){
// 说明是在秒杀活动时间范围内容的请求
// 2.校验 随机和商品 是否合法
String randCode = dto.getRandCode();
Long skuId = dto.getSkuId();
String redisKillId = dto.getPromotionSessionId() + "_" + skuId;
if(randCode.equals(code) && killId.equals(redisKillId)){
// 随机码校验合法
// 3.判断抢购商品数量是否合法
if(num <= dto.getSeckillLimit().intValue()){
// 满足限购的条件
// 4.判断是否满足 幂等性
// 只要抢购成功我们就在Redis中 存储一条信息 userId + sessionID + skuId
MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
Long id = memberVO.getId();
String redisKey = id + "_" + redisKillId;
Boolean aBoolean = redisTemplate.opsForValue()
.setIfAbsent(redisKey, num.toString(), (endTime - now), TimeUnit.MILLISECONDS);
if(aBoolean){
// 表示数据插入成功 是第一次操作
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE+randCode);
try {
boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
if(b){
// 表示秒杀成功
String orderSN = UUID.randomUUID().toString().replace("-", "");
// 继续完成快速下订单操作 --> RocketMQ
SeckillOrderDto orderDto = new SeckillOrderDto() ;
orderDto.setOrderSN(orderSN);
orderDto.setSkuId(skuId);
orderDto.setSeckillPrice(dto.getSeckillPrice());
orderDto.setMemberId(id);
orderDto.setNum(num);
orderDto.setPromotionSessionId(dto.getPromotionSessionId());
// 通过RocketMQ 发送异步消息
rocketMQTemplate.sendOneWay(OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC
,JSON.toJSONString(orderDto));
return orderSN;
}
} catch (InterruptedException e) {
return null;
}
}
}
}
}
}
return null;
}
合法性校验
校验的内容有四块:时效性,随机码是否合法,是否满足限购条件,还有幂等性
信号量处理
通过信号量来控制秒杀的商品数量。降低了对库存商品操作,提升了处理能力
if(aBoolean){
// 表示数据插入成功 是第一次操作
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE+randCode);
try {
boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
if(b){
// 表示秒杀成功
String orderSN = UUID.randomUUID().toString().replace("-", "");
// 继续完成快速下订单操作 --> RocketMQ
SeckillOrderDto orderDto = new SeckillOrderDto() ;
orderDto.setOrderSN(orderSN);
orderDto.setSkuId(skuId);
orderDto.setSeckillPrice(dto.getSeckillPrice());
orderDto.setMemberId(id);
orderDto.setNum(num);
orderDto.setPromotionSessionId(dto.getPromotionSessionId());
// 通过RocketMQ 发送异步消息
rocketMQTemplate.sendOneWay(OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC
,JSON.toJSONString(orderDto));
return orderSN;
}
} catch (InterruptedException e) {
return null;
}
}
MQ异步下单
秒杀成功后给RocketMQ发送消息,订单服务订阅消息,实现异步下单,从而降低了对秒杀系统的影响。
@RocketMQMessageListener(topic = OrderConstant.ROCKETMQ_SECKILL_ORDER_TOPIC,consumerGroup = "test")
@Component
public class SeckillOrderConsumer implements RocketMQListener<String> {
@Autowired
OrderService orderService;
@Override
public void onMessage(String s) {
// 订单关单的逻辑实现
SeckillOrderDto orderDto = JSON.parseObject(s,SeckillOrderDto.class);
orderService.quickCreateOrder(orderDto);
}
}
/**
* 快速完成订单的处理 秒杀活动
* @param orderDto
*/
@Transactional
@Override
public void quickCreateOrder(SeckillOrderDto orderDto) {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderDto.getOrderSN());
orderEntity.setStatus(OrderConstant.OrderStateEnum.FOR_THE_PAYMENT.getCode());
orderEntity.setMemberId(orderDto.getMemberId());
orderEntity.setTotalAmount(orderDto.getSeckillPrice().multiply(new BigDecimal(orderDto.getNum())));
this.save(orderEntity);
OrderItemEntity itemEntity = new OrderItemEntity();
// TODO 根据SKUID查询对应的SKU信息和SPU信息
itemEntity.setOrderSn(orderDto.getOrderSN());
itemEntity.setSkuPrice(orderDto.getSeckillPrice());
itemEntity.setSkuId(orderDto.getSkuId());
itemEntity.setRealAmount(orderDto.getSeckillPrice().multiply(new BigDecimal(orderDto.getNum())));
itemEntity.setSkuQuantity(orderDto.getNum());
orderItemService.save(itemEntity);
}
秒杀成功跳转到成功页面: