秒杀系统设计
一、秒杀系统设计
1.秒杀系统介绍
1.1 秒杀(高并发)系统关注的问题
二、登录检查
1.前端限流
秒杀必须登录,进行前端限流。
<div class="box-btns-two"
th:if="${#dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
<a id="seckillA"
th:attr="skuId=${item.info.skuId},sessionId=${item.seckillInfo.promotionSessionId},code=${item.seckillInfo.randomCode}">
立即抢购
</a>
</div>
<div class="box-btns-two"
th:if="${#dates.createNow().getTime() < item.seckillInfo.startTime || #dates.createNow().getTime() > item.seckillInfo.endTime}">
<a id="addCart" th:attr="skuId=${item.info.skuId}">
加入购物车
</a>
</div>
//立即抢购
$("#seckillA").click(function () {
var isLogin = [[${session.loginUser!=null}]];//true
if (isLogin) {
var killId = $(this).attr("sessionid") + "_" + $(this).attr("skuid");
var key = $(this).attr("code");
var num = $("#numInput").val();
location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + key + "&num=" + num;
}else {
alert("秒杀前请先登录")
}
return false;
})```
2.控制器请求拦截
2.1).依赖、配置
1).引入redis,session依赖
<!--整合spring-session完成Session共享问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--spring整合redis依赖,lettuce容易内存溢出,使用jedis不会-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--导入jedis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2).引入redis,session配置
#session保存到redis
spring.session.store-type=redis
#配置redis
spring.redis.host=192.168.56.10
spring.redis.port=6379
3).引入session配置类
/**
* @Description: spring Session配置类
**/
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
4).引入拦截器
/**
* @Description: 登录拦截器
**/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/kill", uri);
if (match) {
HttpSession session = request.getSession();
//获取登录的用户信息
MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
// session.setAttribute("msg", "请先进行登录");
// response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
5).使用拦截器-web Config
/**
* @Description: web配置
**/
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
三、秒杀流程
1.流程一:添加购物车、支付(不采用)
像京东一样,秒杀就是优惠,添加购物车、支付。
缺点:秒杀需要级联其他服务、页面。流量大,每个页面可能扛不住压力
优点:整个业务是统一的、数据模型需要设计兼容。普通商品的信息与秒杀商品的信息,就差优惠、库存。
2.流程二、秒杀作为信号量(采用)
面对高流量,使用信号量。
优点:很少访问数据库。
缺点:极限下,订单服务宕机,会一直无法消费订单,造成一直支付不成功。
2.1 代码1-创建快速秒杀
1).秒杀操作-controller
//立即抢购
//http://seckill.gulimall.com/kill?killId=8_1&key=e965f89b-dd6a-4324-bb20-7634919edbba&num=1
@GetMapping("/kill")
public R kill(@RequestParam("killId") String killId, @RequestParam("key") String key, @RequestParam("num") Integer num) {
//1.判断是否登录
String orderSn = seckillService.kill(killId,key,num);
System.out.println("秒杀商品的订单号: " + orderSn);
return R.ok().setData(orderSn);
}
2). 秒杀操作-实现类
//秒杀
@Override
public String kill(String killId, String key, Integer num) {
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//0.登录检查。已经前端、拦截器已判断
//1.获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String json = ops.get(killId);
if (StringUtils.isEmpty(json)) {
return null;
} else {
SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
//1.1校验合法性(秒杀时间、随机码、对应关系、幂等性)
//校验秒杀时间
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long current = new Date().getTime();
long ttl = startTime - endTime;
if (current >= startTime && startTime <= endTime) {
System.out.println("秒杀时间验证合法!");
//校验随机码与商品id
String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
String randomCode = redisTo.getRandomCode();
if (randomCode.equals(key) && skuId.equals(killId)) {
System.out.println("随机码与商品id验证合法!");
//验证购物数量是否合理
if (num <= redisTo.getSeckillLimit()) {
System.out.println("验证购物数量 合理");
//幂等性:如果只要秒杀成功,就去redis占位,userId_sessionId_skuId
// 验证此人是否已经购买过
String redisKey = memberRespVo.getId() + "_" + redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
//自动过期(秒杀场次结束,就过期)
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功,此人未买过
//1.2 获取信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
try {
boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
//1.3 秒杀成功,快速下单(发送MQ消息)
String orderNo = IdWorker.getTimeId();
return orderNo;
} catch (InterruptedException e) {
return null;
}
} else {
//此人已经买过
return null;
}
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
}
}
2.2 代码2-发送MQ到订单服务
1).MQ依赖、配置
<!--MQ依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10
2).MyRabbitMQConfig
@Configuration
public class MyRabbitMQConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
3).秒杀订单的数据-common项目
@Data
public class SeckillOrderTo {
// 订单号
private String orderSn;
// 活动场次id
private Long promotionSessionId;
//商品id
private Long skuId;
//秒杀价格
private BigDecimal seckillPrice;
//购买数量
private Integer num;
// 会员ID
private Long memberId;
}
4).发送MQ消息
代码一2)实现类处,添加下面代码
@Autowired
RabbitTemplate rabbitTemplate;
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(orderSn);
orderTo.setMemberId(memberRespVo.getId());
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
orderTo.setSkuId(redisTo.getSkuId());
orderTo.setNum(num);
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order", orderTo );
2.3.订单项目
1).MyRabbitMQConfig 添加队列、绑定
// 商品秒杀队列
@Bean
public Queue orderSecKillOrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
//商品秒杀队列的绑定
@Bean
public Binding orderSecKillOrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
2).订单秒杀监听器
/**
* @Description: 订单秒杀监听器
**/
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
log.info("准备创建秒杀单的详细信息...");
try {
orderService.createSeckillOrder(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
3).订单实现类-createSeckillOrder方法
//创建秒杀订单
@Override
public void createSeckillOrder(SeckillOrderTo seckillOrder) {
//TODO 保存订单信息
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(seckillOrder.getOrderSn());
orderEntity.setMemberId(seckillOrder.getMemberId());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
BigDecimal multiply = seckillOrder.getSeckillPrice().multiply(new BigDecimal("" + seckillOrder.getNum()));
orderEntity.setPayAmount(multiply);
this.save(orderEntity);
//TODO 保存订单项信息
OrderItemEntity itemEntity = new OrderItemEntity();
itemEntity.setOrderSn(seckillOrder.getOrderSn());
itemEntity.setSkuId(seckillOrder.getSkuId());
itemEntity.setRealAmount(multiply);
itemEntity.setSkuQuantity(seckillOrder.getNum());
//todo 获取当前Sku详细信息进行设置
// productFeignService.getSpuInfoBySkuId(seckillOrder.getSkuId());
orderItemService.save(itemEntity);
}
2.4.渲染商品页面-秒杀项目success.html
1).导入thymeleaf依赖、配置
<!-- thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring.thymeleaf.cache=false
2).把cart项目的success.html复制到seckill中
a.修改静态资源,指向cart
src="http://cart.gulimall.com/static/cart/js/jquery-3.1.1.min.js"
b.支付成功,页面跳转支付
<div class="m succeed-box">
<div th:if="${orderSn !=null}" class="mc success-cont">
<h1>恭喜,秒杀成功,订单号[[${orderSn}]]</h1>
<h2>正在准备订单数据,10s以后自动跳转支付页面
<a style="color: red" th:href="${'http://order.gulimall.com/payOrder?orderSn=' + orderSn}">点击支付</a>
</h2>
</div>
<div th:if="${orderSn==null}" class="mc success-cont">
<h1>秒杀失败,手气不好,下次再来!!</h1>
</div>
</div>
c.修改秒杀的控制器–抢购方法
@Controller
public class SeckillController {
@Autowired
SeckillService seckillService;
//立即抢购
//http://seckill.gulimall.com/kill?killId=8_1&key=e965f89b-dd6a-4324-bb20-7634919edbba&num=1
@GetMapping("/kill")
public String kill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num, Model model) {
//1.判断是否登录
String orderSn = seckillService.kill(killId,key,num);
model.addAttribute("orderSn",orderSn);
return "success";
}
}