商城业务-商品秒杀服务

一、秒杀介绍

1、秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)

  • 独立部署

限流方式:

  • 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
  • nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
  • 网关限流,限流的过滤器
  • 代码中使用分布式信号量
  • rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。

2、秒杀流程

见秒杀流程图

3、限流

参照 Alibaba Sentinel

二、秒杀

京东秒杀

1.启动前端项目

IDEA启动renren-fast项目,打开vscode,npm run dev

1).修改coupon的网关

        - id: coupon_route
          uri: lb://gulimall-coupon
          predicates:
            - Path=/api/coupon/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

2).vscode代码

coupon-seckillsession

2.1).查询、新增秒杀场次

查询秒杀场次
http://localhost:88/api/coupon/seckillskurelation/list?t=1623417281403&page=1&limit=10&key=&promotionSessionId=1

新增秒杀场次在这里插入图片描述

2.2).关联商品

http://localhost:88/api/coupon/seckillskurelation/list?t=1623417281403&page=1&limit=10&key=&promotionSessionId=1
在这里插入图片描述
查询已经关联的商品:sms_seckill_sku_relation
coupon的seckillSkuRelationServiceImpl

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        //list?t=1623417281403&page=1&limit=10&key=&promotionSessionId=1
        QueryWrapper<SeckillSkuRelationEntity> wrapper = new QueryWrapper<SeckillSkuRelationEntity>();
        String promotionSessionId = (String) params.get("promotionSessionId");
        if (!StringUtils.isEmpty(promotionSessionId)) {
            wrapper.eq("promotion_session_id", promotionSessionId);
        }
        IPage<SeckillSkuRelationEntity> page = this.page(
                new Query<SeckillSkuRelationEntity>().getPage(params),
                wrapper
        );

        return new PageUtils(page);
    }

三、秒杀服务-定时任务&cron表达式

1.秒杀微服务

1.0 创建 秒杀微服务

在这里插入图片描述

1.1 添加依赖

<dependency>
            <groupId>com.gulimall</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

1.2 配置文件application.properties

spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=192.168.56.10:8848
spring.redis.host=192.168.56.10

1.3 启动类

@EnableDiscoveryClient //开启nacos

2.秒杀业务流程

把秒杀场次放入session、库存页放入session

2.1 秒杀流程图

逻辑:秒杀商品定时上架、秒杀
在这里插入图片描述

2.2 定时任务笔记

定时任务与分布式调度笔记:百度云地址 提取码1111
定时任务使用quartz-schedule。 http://www.quartz-scheduler.org/

1、cron 表达式

语法:秒 分 时 日 月 周 年(Spring 不支持)
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
在这里插入图片描述
cron表达式
在线Cron表达式生成器

2、cron 示例

在这里插入图片描述

3、SpringBoot 整合

@EnableScheduling
@Scheduled

2.3、测试 定时任务-异步执行

/**
 * 定时任务
 *      1、@EnableScheduling 开启定时任务
 *      2、@Scheduled开启一个定时任务
 *
 * 异步任务
 *      1、@EnableAsync:开启异步任务
 *      2、@Async:给希望异步执行的方法标注
 */

@Slf4j
@Component //放入容器中
@EnableScheduling //开启定时任务
@EnableAsync // 开启异步任务
public class HelloScheduled {
     @Scheduled(cron = "* * * * * ?")
     @Async //异步执行的方法
     public void hello() {
         log.info("hello...");
     }
}
 * 秒  分  时  日  月  周  年(Spring 不支持)
 * 1、在Spring中表达式是6位组成,不允许第七位的年份
 * 2、在周几的的位置,1-7代表周一到周日
 * 3、定时任务不该阻塞。默认是阻塞的
 *      1)、可以让业务以异步的方式,自己提交到线程池
 *              CompletableFuture.runAsync(() -> {
 *         },execute);
 *
 *      2)、支持定时任务线程池;设置 TaskSchedulingProperties
 *        spring.task.scheduling.pool.size: 5
 *
 *      3)、让定时任务异步执行
 *          异步任务
 *
 *      解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
 *
 */

3.秒杀商品定时上架

3.1远程coupon

3.1.1 coupon的feign接口
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
    @GetMapping("/coupon/seckillsession/lates3DaySession")
      R getLates3DaySession();
}
3.1.2 coupon秒杀的商品
    @GetMapping("/lates3DaySession")
    public R getLates3DaySession() {
     List<SeckillSessionEntity> sessions= seckillSessionService.getLates3DaySession();
        return R.ok().setData(sessions);
    }
3.1.3 coupon秒杀的商品实现类
    @Override
    public List<SeckillSessionEntity> getLates3DaySession() {
        //计算最近三天
        String startTime = startTime();
        String endTime = endTime();
        //获取最近三天的秒杀场次
        QueryWrapper<SeckillSessionEntity> wrapper = new QueryWrapper<>();
        wrapper.between("start_time", startTime, endTime);

        List<SeckillSessionEntity> list = this.list(wrapper);
        if (list != null && list.size() > 0) {
            List<SeckillSessionEntity> collect = list.stream().map(session -> {
                Long id = session.getId();
                QueryWrapper<SeckillSkuRelationEntity> wrapper1 = new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id);
                List<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list(wrapper1);
                session.setRelationSkus(relationSkus);
                return session;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

    //获取开始、结束时间
    private String startTime() {
        LocalDate now = LocalDate.now();
        LocalTime min = LocalTime.MIN;
        LocalDateTime start = LocalDateTime.of(now, min);
        String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return format;
    }

    private String endTime() {
        LocalDate now = LocalDate.now();
        LocalDate plus2 = now.plusDays(2);
        LocalTime max = LocalTime.MAX;
        LocalDateTime end = LocalDateTime.of(plus2, max);
        String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

        return format;
    }

3.2 秒杀的控制器-SeckillSkuScheduled

3.2.1 秒杀商品定时上架
    //上架最近三天需要三天秒杀的商品
    @Scheduled(cron = "0 0 3 * * ?")
    public void uploadSeckillSkuLatest3Days() {
        //1、重复上架无需处理
        log.info("上架秒杀的商品...");
        seckillService.uploadSeckillSkuLatest3Days();
    }
3.2.2 秒杀商品定时上架的实现类

扫描最近三天需要参与秒杀的活动 分 上架商品缓存到redis

1).上架商品
      @Autowired
    CouponFeignService couponFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    RedissonClient redissonClient;

    private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码
    
    @Override
    public void uploadSeckillSkuLatest3Days() {
        //1.扫描最近三天需要参与秒杀的活动。远程coupon的seckill-session
        R r = couponFeignService.getLates3DaySession();
        if (r.getCode() == 0) {
            //1.1上架商品
            List<SeckillSessionWithSkus> sessionData = r.getData(new TypeReference<List<SeckillSessionWithSkus>>() {});
            //1.2.缓存到redis
            //1.2.1.缓存秒杀活动的信息
            saveSessionInfos(sessionData);
            //1.2.2.缓存秒杀活动的关联商品信息
            saveSessionSkuInfos(sessionData);
        }
    }
2).缓存到redis

保存数据到redis,包括缓存秒杀活动的信息缓存秒杀活动的关联商品信息 在这里插入图片描述

a.缓存秒杀活动的信息
    private void saveSessionInfos(List<SeckillSessionWithSkus> sessions) {
        sessions.stream().forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();
            String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            List<String> collect = session.getRelationSkus().stream().map(item -> session.getId().toString()).collect(Collectors.toList());
            //缓存活动信息
            redisTemplate.opsForList().leftPushAll(key, collect);
        });
    }
b.缓存秒杀活动的关联商品信息

分sku基本信息、sku秒杀信息、设置当前商品的秒杀时间信息、设置随机码、使用库存作为分布式的信号量。限流:

 private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions) {
        BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        //准备hash操作
        sessions.stream().forEach(session -> {
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                //缓存商品信息。缓存为Hash结构
                SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                //1.sku基本信息。
                // 远程product服务的SkuInfo

                R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if (r.getCode() == 0) {
                    SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    redisTo.setSkuInfo(skuInfo);
                }
                //2.sku秒杀信息
                BeanUtils.copyProperties(seckillSkuVo, redisTo);
                //3.设置当前商品的秒杀时间信息
                redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());
                //4.设置随机码。seckill?skuId=1&key=1234r5498r5
                String token = UUID.randomUUID().toString().replace("_", "");
                redisTo.setRandomCode(token);
                //5.使用库存作为分布式的信号量。限流:
                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                //商品可以秒杀的数量作为信号量
                semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

                //为了好保存,把数据变成JSON数据
                String jsonString = JSON.toJSONString(redisTo);
                ops.put(seckillSkuVo.getSkuId().toString(), jsonString);
            });

        });
    }

导入Redisson依赖、Redisson配置

        <!--使用redisson作为所有分布式锁,分布式对象等功能性框架-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>
        
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
 * @Description:
 **/

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redissonClient() throws IOException {
        //1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        //Redis url 应该以 redis:// 或 redis:// 开头

        //2、根据Config创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}
        

远程商品服务,获取sku信息。feign接口

    //信息
    @RequestMapping("/info/{skuId}")
    public R info(@PathVariable("skuId") Long skuId) {
        SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
        return R.ok().put("skuInfo", skuInfo);
    }
    
@FeignClient("gulimall-product")
public interface ProductFeignService {
    //获取sku商品信息
    @RequestMapping("/product/skuinfo/info/{skuId}")
     R getSkuInfo(@PathVariable("skuId") Long skuId);
}
3.2.3. 秒杀上架幂等性问题

上架过的商品重复上架,咋办??。保证幂等性问题
不管多少服务,商品只能上架一次。使用分布式锁 在这里插入图片描述

1).修改定时上架SeckillSkuScheduled-分布式锁

使用分布式锁,实现一次商品上架
锁的业务执行完成,状态已经更新完成。其他人会获取到最新状态

@Slf4j
@Service
public class SeckillSkuScheduled {

    @Autowired
    private SeckillService seckillService;

    @Autowired
    private RedissonClient redissonClient;

    //秒杀商品上架功能的锁
    private final String upload_lock = "seckill:upload:lock";

    //TODO 保证幂等性问题。上架后就不用再上架了
    //@Scheduled(cron = "*/5 * * * * ? ")

    //上架最近三天需要三天秒杀的商品
//    @Scheduled(cron = "0 0 3 * * ?")
    @Scheduled(cron = "0 * * * * ?")
    public void uploadSeckillSkuLatest3Days() {
        //1、重复上架无需处理
        log.info("上架秒杀的商品...");

        //分布式锁
        //锁的业务执行完成,状态已经更新完成。其他人会获取到最新状态
        RLock lock = redissonClient.getLock(upload_lock);
        try {
            //加锁
            lock.lock(10, TimeUnit.SECONDS);
            seckillService.uploadSeckillSkuLatest3Days();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
2).修改SeckillServiceImpl实现类

判断redis是否有数据key,没有就缓存数据到redis
在这里插入图片描述在这里插入图片描述

3.2.4. 同一商品多次被秒杀,怎办?

.同一商品,在不同场次秒杀,怎么处理?修改保存的redis的key
在这里插入图片描述
在这里插入图片描述
redis秒杀信息为sessionId_skuId,商品信息为sessionId_skuIds
在这里插入图片描述
在这里插入图片描述

4.秒杀商品页面渲染

4.1 查询当前时秒杀的商品

1).控制器controller
@RestController
public class SeckillController {
    @Autowired
    SeckillService seckillService;

    //返回当前时间可以参与的秒杀商品信息
    @GetMapping("/currentSeckillSkus")
    public R getCurrentSeckillSkus() {
        List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
        System.out.println("当前时间秒杀场次的商品数据vos: "+ vos);
        return R.ok().setData(vos);
    }
}
2).实现类SeckillServiceImpl
    //返回当前时间可以参与的秒杀商品信息
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        //1.确定当前时间属于哪个秒杀场次
        long time = new Date().getTime();
        //从redis获取缓存数据。获取开始、结束时间
        Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
        for (String key : keys) {
            //seckill:sessions:1624068000000_1624075200000
            String replace = key.replace(SESSION_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            Long startTime = Long.parseLong(s[0]);
            Long endTime = Long.parseLong(s[1]);

            if (time >= startTime && time <= endTime) {
                //2.获取这个秒杀场次 所有的商品信息
                List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                //获得操作redis的ops
                BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<String> list = ops.multiGet(range);
                if (list != null) {
                    List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
                        SeckillSkuRedisTo redis = JSON.parseObject((String) item, SeckillSkuRedisTo.class);
                        //redis.setRandomCode(null);//当前秒杀开始需要随机码
                        return redis;
                    }).collect(Collectors.toList());
                    return collect;
                }
                break;
            }
        }
        return null;
    }

4.2 页面渲染-product

1).修改网关
        - id: gulimall_seckill_route
          uri: lb://gulimall-seckill
          predicates:
            - Host=seckill.gulimall.com
2).配置域名

使用SwitchHosts.exe在这里插入图片描述
测试http://seckill.gulimall.com/currentSeckillSkus
在这里插入图片描述

3).页面渲染

商品项目的index.html

                <div class="swiper-slide">
                    <ul id="seckillSkuContent">
                    </ul>
                </div>
    $.get("http://seckill.gulimall.com/currentSeckillSkus", function (res) {
        if (res.data.length > 0) {
            res.data.forEach(function (item) {
                $("<li οnclick='toDetail(" + item.skuId + ")'></li>")
                    .append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />"))
                    .append($("<p>"+item.skuInfo.skuTitle+"</p>"))
                    .append($("<span>" + item.seckillPrice + "</span>"))
                    .append($("<s>" + item.skuInfo.price + "</s>"))
                    .appendTo("#seckillSkuContent");
            })
        }
    })

    function toDetail(skuId) {
        location.href = "http://item.gulimall.com/" + skuId + ".html";
    }
4).获得秒杀的商品
4.1).秒杀的控制器
    @GetMapping("/sku/seckill/{skuId}")
    public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
        SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
        System.out.println("当前时间秒杀的商品to: " + to);
        return R.ok().setData(to);
    }
4.2).秒杀的实现类
    //获得秒杀的商品
    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        //1.找到所有需要参与秒杀的商品key---
        BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = ops.keys();
        if (keys != null && keys.size() > 0) {
            String regx = "\\d_" + skuId;//正则表达式
            for (String key : keys) {
                //7_9,8_1。匹配成功,说明有此商品参与秒杀
                if (Pattern.matches(regx, key)) {
                    String s = ops.get(key);
                    SeckillSkuRedisTo redisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);
                    //随机码
                    long current = new Date().getTime();
                    if (current >= redisTo.getStartTime() && current <= redisTo.getEndTime()) {
                    } else redisTo.setRandomCode(null);
                    System.out.println("redisTo: "+redisTo);
                    return redisTo;
                }
            }
        }
        return null;
    }
4.3).商品product项目秒杀的feign接口
@FeignClient("gulimall-seckill")
public interface SecKillFeignService {
    //查询秒杀的商品sku信息
    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
4.4).修改展示当前sku商品详情–SkuInfoServiceImpl
    @Autowired
    SecKillFeignService secKillFeignService;
   public SkuItemVo item(Long skuId){
        //3.查询当前sku商品是否参与秒杀优惠
        CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
            R r = secKillFeignService.getSkuSeckillInfo(skuId);
            if (r.getCode() == 0) {
                SeckillInfoVo data = r.getData(new TypeReference<SeckillInfoVo>() {
                });
                skuItemVo.setSeckillInfo(data);
            }
        }, executor);
            CompletableFuture<Void> allOf = CompletableFuture.allOf( saleAttrFuture, descFuture, baseAttrFuture, imageFuture,secKillFuture);
 }

4.5).渲染商品详情页面-item.html
    <li style="color: red" th:if="${item.seckillInfo!=null}">
		<span th:if="${#dates.createNow().getTime() < item.seckillInfo.startTime}">
              商品将会在[[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀
		</span>
		<span th:if="${#dates.createNow().getTime() >= item.seckillInfo.startTime && #dates.createNow().getTime() <= item.seckillInfo.endTime}">
              秒杀价:  [[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
        </span>
  </li>

四、秒杀系统设计

秒杀系统设计

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值