# gulimall **Repository Path**: he-junyang/gulimall ## Basic Information - **Project Name**: gulimall - **Description**: 谷粒商城 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-07-15 - **Last Updated**: 2023-06-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1. 谷粒商城 ![](./media/img.png) ![](./media/img_1.png) 使用技术: ```text Nacos 配置中心, 服务发现 GateWay 网关 JSR303 数据校验 ElasticSearch 搜索 (倒排索引机制) redis 缓存 Redisson 分布式下的锁 SpringCache 方便地使用 Redis 线程池 Nginx 服务器, 1. 正向代理与反向代理 2. 域名访问环境 3. 动静分离 第三方平台 阿里云 OSS JMeter 压力测试 feign远程调用 CompletableFuture异步编排 RabbitMQ 消息队列 Seata 分布式事务框架 支付宝沙箱环境 Sentinel 限流降级 1. 监控应用流量的QPS或并发线程数,当达到指定的阈值时对流量进行控制 2. 开启feign对sentinel的支持后,在控制台上就可以看到所有feign的远程调用接口 ``` # 2. 秒杀模块 ## 2.1 秒杀服务主要表结构 **sms_seckill_promotion:秒杀活动表** 活动标题,活动名称,开始时间,结束时间,启用状态 这张表没有用到,可以不看 **sms_seckill_session:每日秒杀活动表** 活动标题,场次名称,每日开始时间,每日结束时间,启用状态 在这个项目中每日秒杀活动表被当成秒杀活动表用了 **sms_seckill_sku_relation:秒杀活动商品关联表** 活动场次id,每日活动场次id,skuid,秒杀价格,秒杀总量,每人限购数量, 本项目中,只关联了每日活动场次id ## 2.2 秒杀架构思路 - 项目独立部署,独立秒杀模块gulimall-seckill - 库存预热,使用定时任务每天三点上架最新秒杀商品,削减高峰期压力 - 秒杀链接加密,为秒杀商品添加唯一商品随机码,在开始秒杀时才暴露接口,防止恶意攻击 - 快速扣减+控制流量,先从数据库中扣除一部分库存以redisson信号量的形式存储在redis中 - 队列削峰,秒杀成功后立即返回,然后以发送消息的形式创建订单(消息队列) ## 2.3 定时提前缓存秒杀活动 配置类开启异步定时任务 ```java @EnableAsync @EnableScheduling @Configuration public class ScheduledConfig { } ``` 配置异步任务线程池 ```yaml spring: task: execution: pool: core-size: 5 max-size: 50 ``` 每天凌晨三点自动上架最近三天的秒杀活动和商品,由于在分布式情况下该方法可能同时被调用多次,因此加入分布式锁,同时只有一个服务可以调用该方法 ```java @Scheduled(cron = "0 0 3 * * ? ") public void uploadSeckillSkuLatest3Days() { RLock lock = redissonClient.getLock(upload_lock); try { // 加锁,10秒释放 lock.lock(10, TimeUnit.SECONDS); seckillService.uploadSeckillSkuLatest3Days(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } ``` **redis中秒杀活动场次缓存结构:** ```text key: seckill:sessions:开始时间_结束时间 value: 场地id_商品skuId(list结构) ``` **redis中秒杀商品缓存结构:** ```text key: seckill:skus value: key:场地id_商品skuId value:sku所有详细信息+商品秒杀表信息+秒杀开始结束时间+随机码JSON(hash结构) ``` 随机码在秒杀活动开始后才可以返回给接口 **redisson信号量限流:** ```java // 每个商品将可以秒杀的数量以redisson信号量的形式存储在redis中 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); ``` ```text redis中缓存结构 key: seckill:stock:随机码 value: 库存数(string) ``` ## 2.4 首页获取当前可秒杀商品 ```java /getCurrentSeckillSkus ``` 从Redis中查询到所有key以seckill:sessions开头的所有数据 ```java Set keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*"); ``` 遍历这些秒杀场次,如果在当前时间内,获取这个秒杀场次关联的所有商品信息(场地id-skuid) ```java if (currentTime >= startTime && currentTime <= endTime) { List range = redisTemplate.opsForList().range(key, -100, 100); } ``` 在seckill:skus开头的缓存中,依次根据场次id-skuid这个key获取value ```java BoundHashOperations hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); List listValue = hasOps.multiGet(range); ``` ## 2.5 商品详情页展示秒杀信息 /sku/seckill/{skuId} ```java /** * 根据skuId查询商品是否参加秒杀活动 */ @Override public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) { // 找到所有需要秒杀的商品的key信息,前缀seckill:skus BoundHashOperations hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); // 拿到所有的key,场次id-skuid Set keys = hashOps.keys(); if (keys != null && keys.size() > 0) { // 正则表达式进行匹配 String reg = "\\d-" + skuId; for (String key : keys) { // 如果匹配上了 if (Pattern.matches(reg,key)) { // 从Redis中取出数据来 String 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 && currentTime <= endTime) { return redisTo; } // 随机码不能提前暴露 redisTo.setRandomCode(null); return redisTo; } } } return null; } ``` ```java CompletableFuture seckillFuture = CompletableFuture.runAsync(() -> { // 远程调用查询当前sku是否参与秒杀优惠活动 R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId); if (skuSeckilInfo.getCode() == 0) { // 查询成功 SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference() { }); skuItemVo.setSeckillSkuVo(seckilInfoData); }}, executor); ``` ## 2.6 秒杀下单 ### 1./kill请求配置登录拦截器 拦截器配置都类似,完整参考购物车拦截器配置 ```java @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(); // 获取登录的用户信息 MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(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(""); return false; } } return true; } ``` ### 2.点击按钮开始秒杀 - 点击立即抢购时,会发送请求 - 秒杀请求会对请求校验时效、商品随机码、当前用户是否已经抢购过当前商品、库存和购买量,通过校验的则秒杀成功,发送消息创建订单 返回页面,根据有无订单号判断是否成功 ```java

恭喜秒杀成功 订单号 [[${orderSn}]]

正在准备订单数据 10秒钟后自动跳转支付 点击支付

手气不好 秒杀失败 下次再来

``` 秒杀请求 ```java @GetMapping(value = "/kill") public String seckill(@RequestParam("killId") String killId, @RequestParam("key") String key, @RequestParam("num") Integer num, Model model) { String orderSn = null; try { orderSn = seckillService.kill(killId,key,num); model.addAttribute("orderSn",orderSn); } catch (Exception e) { e.printStackTrace(); } return "success"; } ``` 验证活动时效,随机码,商品id,数量后,请求合法,根据userid-skuid在redis中占位,占位成功才可以获取信号量,占位失败说明之前已经秒杀过了 ```java String redisKey = user.getId() + "-" + skuId; // 设置自动过期(活动结束时间-当前时间) Long ttl = endTime - currentTime; Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS); if (aBoolean) { // 占位成功说明从来没有买过,分布式锁(获取信号量-1) RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode); boolean semaphoreCount = semaphore.tryAcquire(num); // 获取信号量成功即为秒杀成功 if (semaphoreCount) { // 创建订单号和订单信息发送给MQ,整个操作时间在10ms左右 String timeId = IdWorker.getTimeId(); SeckillOrderTo 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()); // 主启动类不用@EnableRabbit, 因为我们只用来发送消息,不接收消息 rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo); // 返回订单号 return timeId; } } ``` ### 3.订单服务监听消息创建秒杀单 秒杀队列和绑定关系 ```java @Bean public Queue orderSecKillOrrderQueue() { Queue queue = new Queue("order.seckill.order.queue", true, false, false); return queue; } @Bean public Binding orderSecKillOrrderQueueBinding() { Binding binding = new Binding( "order.seckill.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.seckill.order", null); return binding; } ``` 订单服务监听秒杀队列的消息,收到消息只需要根据订单号和订单信息创建订单 ```java @Slf4j @Component @RabbitListener(queues = "order.seckill.order.queue") public class OrderSeckillListener { @Autowired private OrderService orderService; @RabbitHandler public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException { try { orderService.createSeckillOrder(orderTo); channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } catch (Exception e) { channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); } } } ```