秒杀功能、高并发系统关注的问题、秒杀系统设计-59

导读:本篇文章讲解 秒杀功能、高并发系统关注的问题、秒杀系统设计-59,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一:秒杀

1.1 特点

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

1.2 限流方式

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

1.3 秒杀流程

在这里插入图片描述

二:创建秒杀模块

秒杀建议单独写入一个模块里面,这样可以单独部署,及时秒杀模块出现问题,也不会影响其他模块

2.1 pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.sysg.gulimail</groupId>
    <artifactId>gulimail-seckill</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimail-seckill</name>
    <description>谷粒商城-秒杀服务</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.sysg.gulimail</groupId>
            <artifactId>gulimail-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <!--排除掉seata依赖-->
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
                </exclusion>
            </exclusions>
        </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.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2.2 application.properties

# name
spring.application.name=gulimail-seckill
# port
server.port=25000
# nacos
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# redis
spring.redis.host=127.0.0.1

2.3 添加注解

在主启动类添加@EnableDiscoveryClient注解,将服务注册到配置中心
在配置类添加@EnableAsync,表示当前方法异步执行
在配置类添加@EnableScheduling,开启定时任务功能

三:秒杀商品定时上架

秒杀系统一次性上架最近三天所需要的商品

3.1 计算出最近三天的时间

当前时间

/**
 * 当前时间
 * @return
 */
public String startTime(){
    LocalDate now = LocalDate.now();
    LocalTime min = LocalTime.MIN;
    LocalDateTime start = LocalDateTime.of(now, min);
    //格式化时间
    return start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

三天后的时间

/**
 * 结束时间
 * @return
 */
public String endTime(){
    LocalDate now = LocalDate.now();
    LocalDate plus = now.plusDays(2);
    LocalTime max = LocalTime.MAX;
    LocalDateTime end = LocalDateTime.of(plus, max);
    //格式化时间
    return end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

3.2 查询最近三天需要参加秒杀商品的信息

controller

/**
     * 查询最近三天需要参加秒杀商品的信息
     * @return
     */
    @GetMapping(value = "/Lates3DaySession")
    public R getLates3DaySession() {

        List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLates3DaySession();

        return R.ok().setData(seckillSessionEntities);
    }

service

@Override
    public List<SeckillSessionEntity> getLates3DaySession() {

        //计算最近三天
        //查出这三天参与秒杀活动的商品
        QueryWrapper<SeckillSessionEntity> queryWrapper =
                new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime());
        List<SeckillSessionEntity> list = this.baseMapper.selectList(queryWrapper);

        if (list != null && list.size() > 0) {
            List<SeckillSessionEntity> collect = list.stream().map(session -> {
                Long id = session.getId();
                //查出sms_seckill_sku_relation表中关联的skuId
                List<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>()
                        .eq("promotion_session_id", id));
                session.setRelationSkus(relationSkus);
                return session;
            }).collect(Collectors.toList());
            return collect;
        }

        return null;
    }

3.3 将上架的商品缓存到redis里面

@Override
    public void uploadSeckillSkuLatest3Days() {
        //1.扫描最近三天需要参与秒杀的活动
        R session = couponFeignService.getLates3DaySession();
        if(session.getCode() == 0){
            //上架商品
            List<SeckillSessionWithSkusVo> sessionData = session.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            //将上架的商品缓存到redis里面
            //1.缓存活动信息
            saveSessionInfos(sessionData);
            //2.缓存活动关联的商品信息
            saveSessionSkuInfo(sessionData);
        }

    }

3.3.1 缓存活动信息

/**
     * 缓存活动信息
     */
    public void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions){
        sessions.forEach(session ->{
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CATCH_PREFIX + startTime + "_" + endTime;
            List<String> skuIds = session.getRelationSkus().stream().map(item->item.getSkuId().toString()).collect(Collectors.toList());
            //缓存活动信息
            redisTemplate.opsForList().leftPushAll(key,skuIds);
        });
    }

3.3.2 缓存活动关联的商品信息

1)准备hash操作

BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);

2)先查询sku的基本信息,调用远程服务

R info = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                if (info.getCode() == 0) {
                    SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){}
                    );
                    redisTo.setSkuInfo(skuInfo);
                }

3)sku的秒杀信息

BeanUtils.copyProperties(seckillSkuVo,redisTo);

4)设置当前商品的秒杀时间信息

redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());

5)设置商品的随机码(防止恶意攻击)

String token = UUID.randomUUID().toString().replace("-", "");
                redisTo.setRandomCode(token);

6)设置分布式信号量
信号量就是商品的库存,每进来一个,库存就会减一,而且每次都需要携带随机码

  • 获取信号量,信号量作用就是限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
  • 设置信号量的值,设置商品秒杀的数量作为信号值
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());

7)将需要秒杀的商品转化为json

                String seckillValue = JSON.toJSONString(redisTo);
                ops.put(seckillSkuVo.getSkuId(),seckillValue);

3.4 接口幂等性处理

1)加锁:在多台服务器下,要保证只有一个机器的一个方法能去进行秒杀业务

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

    /**
     * 保证幂等性问题
     */
    @Scheduled(cron = "0 0 1/1 * * ? ")
    public void uploadSeckillSkuLatest3Days() {
        //1、重复上架无需处理
        log.info("上架秒杀的商品...");
        //分布式锁
        RLock lock = redissonClient.getLock(upload_lock);
        try {
            //加锁,10秒后就自动释放锁
            lock.lock(10, TimeUnit.SECONDS);
            seckillService.uploadSeckillSkuLatest3Days();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //lock.unlock();
        }
    }

2)判断:通过key在redis里面查询,如果有了就不需要在进行缓存了

/**
     * 缓存活动信息
     */
    public void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions){
        sessions.forEach(session ->{
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CATCH_PREFIX + startTime + "_" + endTime;
            //缓存活动信息
            //通过key在redis里面查询,如果有了就不需要在进行缓存了
            Boolean hasKey = redisTemplate.hasKey(key);
            if(!Boolean.TRUE.equals(hasKey)){
                List<String> skuIds = session.getRelationSkus().stream().map(item->item.getPromotionSessionId()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key,skuIds);
            }
        });
    }

    /**
     * 缓存活动关联的商品信息
     */
    public void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions){
        sessions.forEach(session ->{
            //准备hash操作
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
            session.getRelationSkus().forEach(seckillSkuVo -> {
                //设置商品的随机码(防止恶意攻击)
                String token = UUID.randomUUID().toString().replace("-", "");
                Boolean hasKey = ops.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString());
                if(!Boolean.TRUE.equals(hasKey)){
                    //缓存商品
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                    //1、先查询sku的基本信息,调用远程服务
                    R info = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (info.getCode() == 0) {
                        SkuInfoVo skuInfo = info.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、设置商品的随机码(防止恶意攻击)
                    redisTo.setRandomCode(token);
                    // 将需要秒杀的商品转化为json
                    String seckillValue = JSON.toJSONString(redisTo);
                    ops.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(),seckillValue);

                    //5.设置分布式信号量,信号量就是商品的库存,每进来一个,库存就会减一,而且每次都需要携带随机码
                    //5.1 获取信号量,信号量作用就是限流
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    //5.2 设置信号量的值,设置商品秒杀的数量作为信号值
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            });
        });
    }

四:查询秒杀商品

4.1 controller

/**
     * 当前时间可以参与秒杀的商品信息
     * @return
     */
    @GetMapping(value = "/getCurrentSeckillSkus")
    @ResponseBody
    public R getCurrentSeckillSkus() {
        //获取到当前可以参加秒杀商品的信息
        List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
        return R.ok().setData(vos);
    }

4.2 service

 /**
     * 获取到当前可以参加秒杀商品的信息
     * @return
     */
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        //1.确定当前时间属于哪个秒杀场次
        long time = new Date().getTime();
        //获取所有的keys数据
        Set<String> keys = redisTemplate.keys(SESSIONS_CATCH_PREFIX + "*");
        if(!CollectionUtils.isEmpty(keys)){
            for (String key : keys) {
                //分割后获取时间区间
                String replace = key.replace(SESSIONS_CATCH_PREFIX, "");
                String[] s = replace.split(PREFIX);
                //开始时间
                long start = Long.parseLong(s[0]);
                //结束时间
                long end = Long.parseLong(s[1]);
                //查询当前的场次信息
                if( time >= start && time <= end ){
                    //2.获取这个场次所有的商品信息
                    //range获取-100到100区间的数据
                    List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                    if(!CollectionUtils.isEmpty(range)){
                        //获取绑定的hash值
                        BoundHashOperations<String, String, Object> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                        List<Object> list = hashOps.multiGet(range);
                        if(!CollectionUtils.isEmpty(list)){
                            return list.stream().map(item -> {
                                //不能将随机码字段也返回,所以需要删除掉
                                //redisTo.setRandomCode(null);当前秒杀开始就需要随机码
                                return JSON.parseObject(String.valueOf(item),SeckillSkuRedisTo.class);
                            }).collect(Collectors.toList());
                        }
                        //查询出当前场次以后,后续的就不需要遍历了,直接跳出for循环
                        break;
                    }
                }
            }
        }
        return null;
    }

五:查询商品有没有秒杀信息

5.1 根据skuId查询商品是否参加秒杀活动

  • controller
   /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @GetMapping(value = "/sku/seckill/{skuId}")
    @ResponseBody
    public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
        SeckillSkuRedisTo to = seckillService.getSkuSeckilInfo(skuId);
        return R.ok().setData(to);
    }
  • service
   /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @Override
    public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
        //1.找到所有参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if(!CollectionUtils.isEmpty(keys)){
            //6_4 通过正则表达式去判断
            //d表示匹配一个数字
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                boolean matches = Pattern.matches(regx, key);
                if(matches){
                    String json = hashOps.get(key);
                    SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
                    if(redisTo != null){
                        //随机码
                        Long startTime = redisTo.getStartTime();
                        Long endTime = redisTo.getEndTime();
                        long currentTime = new Date().getTime();
                        //判断当前时间是否在秒杀时间之间,如果是就返回随机码
                        if( currentTime < startTime || currentTime > endTime ){
                            redisTo.setRandomCode(null);
                        }
                        return redisTo;
                    }

                }
            }
        }
        return null;
    }

5.2 新建远程调用的feign接口

@FeignClient(value = "gulimail-seckill")
public interface SeckillFeignService {

    /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @GetMapping(value = "/sku/seckill/{skuId}")
    R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);

}

5.3 远程调用查询当前sku是否参与秒杀优惠活动

//3、远程调用查询当前sku是否参与秒杀优惠活动
        CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
            R skuSeckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
            if (skuSeckillInfo.getCode() == 0) {
                SeckillSkuVo skuSeckillInfoData = skuSeckillInfo.getData("data", new TypeReference<SeckillSkuVo>() {
                });
                skuItemVo.setSeckillSkuVo(skuSeckillInfoData);
                if (skuSeckillInfoData != null) {
                    long currentTime = System.currentTimeMillis();
                    if (currentTime > skuSeckillInfoData.getEndTime()) {
                        skuItemVo.setSeckillSkuVo(null);
                    }
                }
            }
        }, executor);

        //等到所有任务都完成
        CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();

六:高并发系统关注的问题

1) 服务单一职责+独立部署
秒杀服务即使自己扛不住压力,挂掉。不要影响别人
2)秒杀链接加密
防止恶意攻击,模拟秒杀请求,1000次/s攻击。
防止链接暴露,自己工作人员,提前秒杀商品。
3)库存预热+快速扣减
秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求
4)动静分离
nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。使用CDN网络,分担本集群压力
5)恶意请求拦截
识别非法攻击请求并进行拦截,网关层
6)流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车
7)限流&熔断&降级
前端限流+后端限流。限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
8)队列削峰
1万个商品,每个1000件秒杀。双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可。

七:秒杀系统设计-立即抢购

7.1 秒杀流程

在这里插入图片描述

7.2 发送请求

    $(".seckill").click(function () {
        var isLogin = [[${session.loginUser != null}]];     //true
        if (isLogin) {
            var killId = $(this).attr("sessionid") + "-" + $(this).attr("skuid");
            var code = $(this).attr("code");
            var num = $("#productNum").val();
            location.href = "http://seckill.gulimall.com/kill?killId=" + killId + "&key=" + code + "&num=" + num;
        } else {
            alert("秒杀请先登录");
        }
        return false;
    });

7.3 代码实现

  • controller
/**
     * 商品进行秒杀(秒杀开始)
     * @param killId 秒杀id
     * @param key 随机码
     * @param num 秒杀的总数
     * @return
     */
    @GetMapping(value = "/kill")
    public String seckill(@RequestParam("killId") String killId,
                          @RequestParam("key") String key,
                          @RequestParam("num") Integer num,
                          Model model) {

        String orderSn = null;
        try {
            //1、判断是否登录
            orderSn = seckillService.kill(killId,key,num);
            model.addAttribute("orderSn",orderSn);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "success";
    }
  • service
/**
     * 商品进行秒杀(秒杀开始)
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) {
        //1、判断是否登录,拦截器已经处理,此时无需处理
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //2.判断参数的合法性
        //2.1 获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if(!StringUtils.isEmpty(json)){
            SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
            if(redisTo != null){
                //2.2 判断秒杀时间是否过期
                Long startTime = redisTo.getStartTime();
                Long endTime = redisTo.getEndTime();
                long currentTime = new Date().getTime();
                long ttl = endTime - startTime;
                if(startTime <= currentTime && endTime >= currentTime){
                    //2.3 判断随机码是否正确和商品id是否一致
                    String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
                    if(key.equals(redisTo.getRandomCode()) && killId.equals(skuId)){
                        //2.4 验证购物数量是否合理
                        if( num <= redisTo.getSeckillLimit()){
                            //2.5 验证这个用户是否购买过,幂等性处理。只要秒杀成功,就占位
                            String redisKey = memberRespVo.getId() + "_" + skuId;
                            //设置超时时间,只要过了当前秒杀场次就取消,自动过期
                            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                            //如果占位成功就说明这个人从来没买过,就可以买
                            if(Boolean.TRUE.equals(aBoolean)){
                                //2.6 使用信号量减库存
                                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + redisTo.getRandomCode());
                                try {
                                    //等上100毫秒
                                    boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                    //秒杀成功,快速下单
                                    //生成订单号
                                    return IdWorker.getTimeId();
                                } catch (InterruptedException e) {
                                    return null;
                                }
                            }

                        }

                    }
                }
            }
        }
        return null;
    }

7.4 将秒杀成功的商品订单信息发送给MQ队列

1)引入依赖

         <!--引入mq依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

2)配置信息

# RabbitMQ配置
spring.rabbitmq.host=192.168.77.130
spring.rabbitmq.port=5672
# 虚拟主机配置
spring.rabbitmq.virtual-host=/

3)添加配置类

@Configuration
public class MyRabbitMQConfig {

    /**
     * 配置消息为json类型
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

}

4)发送订单号

//2.5 验证这个用户是否购买过,幂等性处理。只要秒杀成功,就占位
                            String redisKey = memberRespVo.getId() + "_" + skuId;
                            //设置超时时间,只要过了当前秒杀场次就取消,自动过期
                            Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                            //如果占位成功就说明这个人从来没买过,就可以买
                            if(Boolean.TRUE.equals(aBoolean)){
                                //2.6 使用信号量减库存
                                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + redisTo.getRandomCode());
                                try {
                                    //等上100毫秒
                                    boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                    if(Boolean.TRUE.equals(semaphoreCount)){
                                        //秒杀成功,快速下单
                                        //生成订单号
                                        String timeId = IdWorker.getTimeId();
                                        SeckillOrderTo orderTo = new SeckillOrderTo();
                                        orderTo.setOrderSn(timeId);
                                        orderTo.setMemberId(memberRespVo.getId());
                                        orderTo.setNum(num);
                                        orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                                        orderTo.setSkuId(redisTo.getSkuId());
                                        orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                                        rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                                        long s2 = System.currentTimeMillis();
                                        log.info("耗时..." + (s2 - s1));
                                        return timeId;
                                    }
                                } catch (InterruptedException e) {
                                    return null;
                                }
                            }

5)新建队列和绑定关系

/**
     * 商品秒杀队列
     * @return
     */
    @Bean
    public Queue orderSecKillOrderQueue() {
        Queue queue = new Queue("order.seckill.order.queue", true, false, false);
        return queue;
    }

    @Bean
    public Binding orderSecKillOrrderQueueBinding() {
        //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;
    }

6)新建秒杀监听器

@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {

    @Autowired
    private OrderService orderService;

    /**
     * 监听订单的entity消息
     * @param seckillOrderTo
     * @param channel
     * @param message
     */
    @RabbitHandler
    public void listener(SeckillOrderTo seckillOrderTo, Channel channel, Message message) throws IOException {
        //关闭OrderEntity订单
        try {
            log.info("准备创建秒杀单的详细信息:{}",seckillOrderTo.toString());
            orderService.createSeckillOrder(seckillOrderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //true重新回到队列里面,不能丢弃
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

}

7)创建秒杀订单

/**
     * 创建秒杀单
     * @param orderTo
     */
    @Override
    public void createSeckillOrder(SeckillOrderTo orderTo) {
        //保存订单信息
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderSn(orderTo.getOrderSn());
        orderEntity.setMemberId(orderTo.getMemberId());
        orderEntity.setCreateTime(new Date());
        BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
        orderEntity.setPayAmount(totalPrice);
        orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        //保存订单
        this.save(orderEntity);
        //保存订单项信息
        OrderItemEntity orderItem = new OrderItemEntity();
        orderItem.setOrderSn(orderTo.getOrderSn());
        orderItem.setRealAmount(totalPrice);
        orderItem.setSkuQuantity(orderTo.getNum());
        //保存商品的spu信息
        R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());
        SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
        });
        orderItem.setSpuId(spuInfoData.getId());
        orderItem.setSpuName(spuInfoData.getSpuName());
        orderItem.setSpuBrand(spuInfoData.getBrandName());
        orderItem.setCategoryId(spuInfoData.getCatalogId());
        //保存订单项数据
        orderItemService.save(orderItem);
    }

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/84079.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!