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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一秒杀

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);
                }

3sku的秒杀信息

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);
    }
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

“秒杀功能、高并发系统关注的问题、秒杀系统设计-59” 的相关文章