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

一:秒杀

1.1 特点

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

1.2 限流方式

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

1.3 秒杀流程

二:创建秒杀模块

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

2.1 pom文件

xml 复制代码
<?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

xml 复制代码
# 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 计算出最近三天的时间

当前时间

ini 复制代码
/**
 * 当前时间
 * @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"));
}

三天后的时间

ini 复制代码
/**
 * 结束时间
 * @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

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

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

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

service

java 复制代码
@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里面

java 复制代码
@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 缓存活动信息

java 复制代码
/**
     * 缓存活动信息
     */
    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操作

java 复制代码
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);

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

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

3)sku的秒杀信息

java 复制代码
BeanUtils.copyProperties(seckillSkuVo,redisTo);

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

java 复制代码
redisTo.setStartTime(session.getStartTime().getTime());
                redisTo.setEndTime(session.getEndTime().getTime());

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

java 复制代码
String token = UUID.randomUUID().toString().replace("-", "");
                redisTo.setRandomCode(token);

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

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

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

java 复制代码
                String seckillValue = JSON.toJSONString(redisTo);
                ops.put(seckillSkuVo.getSkuId(),seckillValue);

3.4 接口幂等性处理

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

java 复制代码
/**
     * 秒杀商品上架功能的锁
     */
    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里面查询,如果有了就不需要在进行缓存了

java 复制代码
/**
     * 缓存活动信息
     */
    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

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

4.2 service

java 复制代码
 /**
     * 获取到当前可以参加秒杀商品的信息
     * @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
java 复制代码
   /**
     * 根据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
java 复制代码
   /**
     * 根据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接口

java 复制代码
@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是否参与秒杀优惠活动

java 复制代码
//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 发送请求

javascript 复制代码
    $(".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
java 复制代码
/**
     * 商品进行秒杀(秒杀开始)
     * @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
java 复制代码
/**
     * 商品进行秒杀(秒杀开始)
     * @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)引入依赖

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

2)配置信息

xml 复制代码
# RabbitMQ配置
spring.rabbitmq.host=192.168.77.130
spring.rabbitmq.port=5672
# 虚拟主机配置
spring.rabbitmq.virtual-host=/

3)添加配置类

java 复制代码
@Configuration
public class MyRabbitMQConfig {

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

}

4)发送订单号

java 复制代码
//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)新建队列和绑定关系

java 复制代码
/**
     * 商品秒杀队列
     * @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)新建秒杀监听器

java 复制代码
@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)创建秒杀订单

java 复制代码
/**
     * 创建秒杀单
     * @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);
    }
相关推荐
苏三的开发日记2 分钟前
linux端进行kafka集群服务的搭建
后端
苏三的开发日记21 分钟前
windows系统搭建kafka环境
后端
爬山算法31 分钟前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai31 分钟前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌1 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量1 小时前
AQS抽象队列同步器原理与应用
后端
9号达人1 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337982 小时前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso2 小时前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net
追逐时光者2 小时前
精选 8 个 .NET 开发实用的类库,效率提升利器!
后端·.net