date
Jul 8, 2022
type
Post
status
Published
slug
msb-mac-mall-seckill
summary
概要:1 商城部分商品业务介绍;2 秒杀活动解决方案;
tags
微服务
spring-cloud-alibaba
商城
category
商城实战
password
Property
Sep 27, 2022 02:57 AM
icon
秒杀服务
一、商品上架
秒杀活动的结构图
通过定时任务触发:
/** * 定时上架秒杀商品信息 */ @Slf4j @Component public class SeckillSkuSchedule { @Autowired SeckillService seckillService; @Autowired RedissonClient redissonClient; /** * */ @Async @Scheduled(cron = "*/5 * * * * *") public void uploadSeckillSku3Days(){ log.info("定时上架秒杀商品执行了...." + new Date()); // 分布式锁 RLock lock = redissonClient.getLock("seckill:upload:lock"); lock.lock(10, TimeUnit.SECONDS); try { // 调用上架商品的方法 seckillService.uploadSeckillSku3Days(); }catch (Exception e){ lock.unlock(); } } }
进入到Service中处理
@Override public void uploadSeckillSku3Days() { // 1. 通过OpenFegin 远程调用Coupon服务中接口来获取未来三天的秒杀活动的商品 R r = couponFeignService.getLates3DaysSession(); if(r.getCode() == 0){ // 表示查询操作成功 String json = (String) r.get("data"); List<SeckillSessionEntity> seckillSessionEntities = JSON.parseArray(json,SeckillSessionEntity.class); // 2. 上架商品 Redis数据保存 // 缓存商品 // 2.1 缓存每日秒杀的SKU基本信息 saveSessionInfos(seckillSessionEntities); // 2.2 缓存每日秒杀的商品信息 saveSessionSkuInfos(seckillSessionEntities); } } /** * 保存每日活动的信息到Redis中 * @param seckillSessionEntities */ private void saveSessionInfos(List<SeckillSessionEntity> seckillSessionEntities) { for (SeckillSessionEntity seckillSessionEntity : seckillSessionEntities) { // 循环缓存每一个活动 key: start_endTime long start = seckillSessionEntity.getStartTime().getTime(); long end = seckillSessionEntity.getEndTime().getTime(); // 生成Key String key = SeckillConstant.SESSION_CHACE_PREFIX+start+"_"+end; Boolean flag = redisTemplate.hasKey(key); if(!flag){// 表示这个秒杀活动在Redis中不存在,也就是还没有上架,那么需要保存 // 需要存储到Redis中的这个秒杀活动涉及到的相关的商品信息的SKUID List<String> collect = seckillSessionEntity.getRelationEntities().stream().map(item -> { // 秒杀活动存储的 VALUE是 sessionId_SkuId return item.getPromotionSessionId()+"_"+item.getSkuId().toString(); }).collect(Collectors.toList()); redisTemplate.opsForList().leftPushAll(key,collect); } } } /** * 存储活动对应的 SKU信息 * @param seckillSessionEntities */ private void saveSessionSkuInfos(List<SeckillSessionEntity> seckillSessionEntities) { seckillSessionEntities.stream().forEach(session -> { // 循环取出每个Session,然后取出对应SkuID 封装相关的信息 BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX); session.getRelationEntities().stream().forEach(item->{ String skuKey = item.getPromotionSessionId()+"_"+item.getSkuId(); Boolean flag = redisTemplate.hasKey(skuKey); if(!flag){ SeckillSkuRedisDto dto = new SeckillSkuRedisDto(); // 1.获取SKU的基本信息 R info = productFeignService.info(item.getSkuId()); if(info.getCode() == 0){ // 表示查询成功 String json = (String) info.get("skuInfoJSON"); dto.setSkuInfoVo(JSON.parseObject(json,SkuInfoVo.class)); } // 2.获取SKU的秒杀信息 /*dto.setSkuId(item.getSkuId()); dto.setSeckillPrice(item.getSeckillPrice()); dto.setSeckillCount(item.getSeckillCount()); dto.setSeckillLimit(item.getSeckillLimit()); dto.setSeckillSort(item.getSeckillSort());*/ BeanUtils.copyProperties(item,dto); // 3.设置当前商品的秒杀时间 dto.setStartTime(session.getStartTime().getTime()); dto.setEndTime(session.getEndTime().getTime()); // 4. 随机码 String token = UUID.randomUUID().toString().replace("-",""); dto.setRandCode(token); // 分布式信号量的处理 限流的目的 RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + token); // 把秒杀活动的商品数量作为分布式信号量的信号量 semaphore.trySetPermits(item.getSeckillCount().intValue()); hashOps.put(skuKey,JSON.toJSONString(dto)); } }); }); }
启动服务,数据会被保存到Redis中
二、秒杀商品查询
通过当前时间获取对应的秒杀活动及对应的SKU信息。
/** * 查询出当前时间内的秒杀活动及对应的商品SKU信息 * @return */ @Override public List<SeckillSkuRedisDto> getCurrentSeckillSkus() { // 1.确定当前时间是属于哪个秒杀活动的 long time = new Date().getTime(); // 从Redis中查询所有的秒杀活动 Set<String> keys = redisTemplate.keys(SeckillConstant.SESSION_CHACE_PREFIX + "*"); for (String key : keys) { //seckill:sessions1656468000000_1656469800000 String replace = key.replace(SeckillConstant.SESSION_CHACE_PREFIX, ""); // 1656468000000_1656469800000 String[] s = replace.split("_"); Long start = Long.parseLong(s[0]); // 活动开始的时间 Long end = Long.parseLong(s[1]); // 活动结束的时间 if(time > start && time < end){ // 说明的秒杀活动就是当前时间需要参与的活动 // 取出来的是SKU的ID 2_9 List<String> range = redisTemplate.opsForList().range(key, -100, 100); BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX); List<String> list = ops.multiGet(range); if(list != null && list.size() > 0){ List<SeckillSkuRedisDto> collect = list.stream().map(item -> { SeckillSkuRedisDto seckillSkuRedisDto = JSON.parseObject(item, SeckillSkuRedisDto.class); return seckillSkuRedisDto; }).collect(Collectors.toList()); return collect; } } } return null; }
然后定义相关的Controller接口就可以访问了
@RestController @RequestMapping("/seckill") public class SeckillController { @Autowired SeckillService seckillService; @GetMapping("/currentSeckillSessionSkus") public R getCurrentSeckillSessionSkus(){ List<SeckillSkuRedisDto> currentSeckillSkus = seckillService.getCurrentSeckillSkus(); return R.ok().put("data", JSON.toJSONString(currentSeckillSkus)); } }
然后对应的访问效果:
三、页面渲染
1. 网关配置
首先在host中配置域名
然后在网关中配置路由信息
然后重启服务访问:
能访问到数据就表示域名配置成功
2.首页配置
通过Ajax来访问获取秒杀的相关信息
$.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp){ if(resp.data.length > 0){ // 说明有秒杀的数据 console.log($.parseJSON(resp.data)) $.parseJSON(resp.data).forEach(function(item){ $("<li></li>").append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>") .append("<p>"+item.skuInfoVo.skuSubtitle+"</p>") .append("<span>"+item.seckillPrice+"</span>") .append("<s>"+item.skuInfoVo.price+"</s>") .appendTo("#seckillSessionContent"); }) /*<li> <img src="/static/index/img/section_second_list_img1.jpg" alt=""> <p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千克) (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千</p> <span>¥83.9</span><s>¥99.9</s> </li>*/ } })
展示的效果
3.商品详情
在购买商品的时候,进入到商品详情页,如果该商品也参与了秒杀活动,那么对应的需要展示相关的信息
首先我们需要在秒杀服务中提供一个根据SKUID查询相关的秒杀活动的接口
/** * 根据SKUID查询秒杀活动对应的信息 * @param skuId * @return */ @Override public SeckillSkuRedisDto getSeckillSessionBySkuId(Long skuId) { // 1.找到所有需要参与秒杀的商品的sku信息 BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX); Set<String> keys = ops.keys(); if(keys != null && keys.size() > 0){ String regx = "\\d_"+ skuId; for (String key : keys) { boolean matches = Pattern.matches(regx, key); if(matches){ // 说明找到了对应的SKU的信息 String json = ops.get(key); SeckillSkuRedisDto dto = JSON.parseObject(json, SeckillSkuRedisDto.class); return dto; } } } return null; }
然后在查询商品详情的时候异步查询出对应的秒杀活动信息
然后在模板页面中展示相关的信息
<div class="box-summary clear"> <ul> <li>京东价</li> <li> <span>¥</span> <span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span> </li> <li style="color: red"> <span th:if="${#dates.createNow().getTime() < item.seckillVO.startTime}"> 商品将在:[[${#dates.format(new java.util.Date(item.seckillVO.startTime),'yyyy-MM-dd HH:mm:ss')}]] 开始秒杀 </span> <span th:if="${#dates.createNow().getTime() > item.seckillVO.startTime && #dates.createNow().getTime() < item.seckillVO.endTime }"> 秒杀价: [[${#numbers.formatDecimal(item.seckillVO.seckillPrice,1,2)}]] </span> </li> <li> <a href="/static/item/"> 预约说明 </a> </li> </ul> </div>
首页调整到商品详情页
function goItem(skuId){ location.href="http://item.msb.com/"+skuId+".html" } $.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp){ if(resp.data.length > 0){ // 说明有秒杀的数据 console.log($.parseJSON(resp.data)) $.parseJSON(resp.data).forEach(function(item){ $("<li onclick='goItem("+item.skuId+")'></li>") .append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>") .append("<p>"+item.skuInfoVo.skuSubtitle+"</p>") .append("<span>"+item.seckillPrice+"</span>") .append("<s>"+item.skuInfoVo.price+"</s>") .appendTo("#seckillSessionContent"); }) /*<li> <img src="/static/index/img/section_second_list_img1.jpg" alt=""> <p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千克) (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千</p> <span>¥83.9</span><s>¥99.9</s> </li>*/ } })
四、秒杀活动
1.秒杀活动关注点
秒杀活动的最大特点就是高并发而且是短时间内的高并发,那么对我们的服务要求就非常高,针对这种情况所产生的共性问题,对应的解决方案:
2. 秒杀服务前端
当我们点击
秒杀抢购
按钮后,对应我们需要把当前的商品信息提交到后端服务。活动编号+"_"+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; });
访问测试:
3.后端逻辑处理
前端提交的秒杀请求,在后端具体的处理
3.1 登录校验
秒杀活动必须是在登录状态下进行的,如果没有认证就不让秒杀。这时我们需要整合进来SpringSession。
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </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(); } }
最后在启动类中开启
3.2 秒杀活动流程
登录校验
通过拦截器处理:在秒杀活动中并不是所有的请求都是需要在登录状态下的,所有这个拦截器应该只需要拦截部分的请求。
/** * 秒杀活动的拦截器 确认是杂登录的状态下操作的 */ 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"); } }
合法性校验
校验的内容有四块:时效性,随机码是否合法,是否满足限购条件,还有幂等性
信号量处理
通过信号量来控制秒杀的商品数量。降低了对库存商品操作,提升了处理能力
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); } }
秒杀成功跳转到成功页面:
<!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/cart/js/jquery-3.1.1.min.js"></script> <script type="text/javascript" src="/static/cart/bootstrap/js/bootstrap.js"></script> <script type="text/javascript" src="/static/cart/js/swiper.min.js"></script> <script src="js/swiper.min.js"></script> <link rel="stylesheet" type="text/css" href="/static/cart/css/swiper.min.css"/> <link rel="stylesheet" type="text/css" href="/static/cart/bootstrap/css/bootstrap.css"/> <link rel="stylesheet" type="text/css" href="/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://mall.msb.com/home">马士兵商城首页</a> </li> </ul> <ul class="hd_wrap_right"> <li th:if="${session.loginUser == null}"><a href="http://auth.msb.com/login.html" style="color: red;">你好,请登录</a></li> <li th:if="${session.loginUser != null}"><span style="color: red;">[[${session.loginUser.nickname}]]</span></li> <li class="spacer"></li> <li> <a href="/javascript:;">我的订单</a> </li> </ul> </div> </div> <div class="nav-tabs-justified"> <div class="nav_wrap"> <div class="nav_top"> <div class="nav_top_one"> <a href="http://mall.msb.com/home"><img src="/static/cart/img/logo1.jpg" style="height: 60px;width:180px;"/></a> </div> <div class="nav_top_two"><input type="text"/> <button>搜索</button> </div> </div> </div> </div> <div class="main"> <div class="success-wrap"> <div class="w" id="result"> <div class="m succeed-box"> <div class="mc success-cont" th:if="${orderSn!=null}"> <h1>恭喜您!秒杀成功,订单号[[${orderSn}]]</h1> <h2>正在准备订单数据...10秒后跳转到支付页面</h2> <a href="#">去支付</a> </div> <div class="mc success-cont" th:if="${orderSn==null}"> <h1>很遗憾~没有抢购到!欢迎下次再来参与....</h1> </div> </div> </div> </div> </div> </body> <script type="text/javascript" src="/static/cart/js/success.js"></script> </html>
好了~到这儿秒杀活动搞定!