0
点赞
收藏
分享

微信扫一扫

谷粒商城--秒杀服务--高级篇笔记十二

哈哈我是你爹呀 2022-03-18 阅读 67

谷粒商城–秒杀服务–高级篇笔记十二

1.后台添加秒杀商品

image-20220106102656034

1.1 配置网关

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

1.2 配置网关完成,添加场次

image-20220106104012325

image-20220106104028583

image-20220106105141275

1.3 上架秒杀商品

image-20220106104057588

image-20220106105207108

1.4 上架商品小bug,在任意一个场次可以查询的所有场次的上架商品

image-20220106105701893

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
        String promotionSessionId = (String) params.get("promotionSessionId");
        // 2、封装活动场次id,关联
        if (!StringUtils.isEmpty(promotionSessionId)) {
            queryWrapper.eq("promotion_session_id", promotionSessionId);
        }
        IPage<SeckillSkuRelationEntity> page = this.page(
                new Query<SeckillSkuRelationEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

2. 定时任务

2.1 cron 表达式

在线Cron表达式生成器 (qqe2.com)

2.1.1 cron表达式语法

语法:秒 分 时 日 月 周 年 (spring 不支持年,所以可以不写)

quartz表达式格式介绍

Cron Trigger Tutorial (quartz-scheduler.org)

A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:

Field NameMandatoryAllowed ValuesAllowed Special Characters
SecondsYES0-59, - * /
MinutesYES0-59, - * /
HoursYES0-23, - * /
Day of monthYES1-31, - * ? / L W
MonthYES1-12 or JAN-DEC, - * /
Day of weekYES1-7 or SUN-SAT, - * ? / L #
YearNOempty, 1970-2099, - * /

So cron expressions can be as simple as this: * * * * ? *

or more complex, like this: 0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010

2.1.2 cron表达式特殊字符

,:枚举;
(cron="7,9,23****?"):任意时刻的7,923秒启动这个任务;
-:范围:
(cron="7-20****?""):任意时刻的7-20秒之间,每秒启动一次
*:任意;
指定位置的任意时刻都可以
/:步长;
(cron="7/5****?"):7秒启动,每5秒一次;
(cron="*/5****?"):任意秒启动,每5秒一次;

? :(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使用?
(cron="***1*?"):每月的1号,而且必须是周二然后启动这个任务;

L:(出现在日和周的位置)”,
last:最后一个
(cron="***?*3L"):每月的最后一个周二

W:Work Day:工作日
(cron="***W*?"):每个月的工作日触发
(cron="***LW*?"):每个月的最后一个工作日触发
#:第几个
(cron="***?*5#2"):每个月的 第2个周4

2.1.3 示例

ExpressionMeaning
0 0 12 * * ?每天中午12点触发
0 15 10 ? * *每天的10点15分触发
0 15 10 * * ?每天的10点15分触发
0 15 10 * * ? *每天的10点15分触发
0 15 10 * * ? 20052005年的10点15分触发
0 * 14 * * ?每天的14:00-14:59 每分钟触发一次
0 0/5 14 * * ?每天的14:00-14:59 每五分钟触发一次
0 0/5 14,18 * * ?每天的14:00-14:59 和18:00-18:59 每五分钟触发一次
0 0-5 14 * * ?每天的14:00-14:05每分钟执行一次
0 10,44 14 ? 3 WED3月的每个星期三的14:10:00和14:44:00触发一次
0 15 10 ? * MON-FRI星期一到星期五的10:15:00触发
0 15 10 15 * ?每个月的15号10:15:00触发
0 15 10 L * ?每个月的最后一天10:15:00触发
0 15 10 L-2 * ?每个月的倒数第二天10:15:00触发
0 15 10 ? * 6L每个月的最后一个星期五的10:15:00触发
0 15 10 ? * 6L每个月的最后一个星期五10:15:00触发
0 15 10 ? * 6L 2002-20052002年到2005年的每个月的最后一个星期五的10:15:00触发
0 15 10 ? * 6#3每个月的第3个星期五的10:15:00触发
0 0 12 1/5 * ?每个月的1号开始每五天12:00:00触发
0 11 11 11 11 ?十一月的11号的11:11:00触发

3. 秒杀模块

3.1 创建模块

image-20220106141630820

image-20220106141156627

<?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.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zhourui.gulimall</groupId>
    <artifactId>gulimall-seckill</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-seckill</name>
    <description>秒杀</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.zhourui.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</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>
            </plugin>
        </plugins>
    </build>

</project>

server:
  port: 25000
spring:
  application:
    name: gulimall-seckill
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  redis:
    host: 192.168.157.128
    port: 6379


@EnableFeignClients
@EnableDiscoveryClient

3.2 springboot整合定时任务异步任务(使用异步任务 + 定时任务来完成定时任务不阻塞的功能)

3.2.1整合定时任务

3.2.1.0 自动配置类TaskSchedulingAutoConfiguration
@Component
@EnableScheduling
@Scheduled(cron = "* * * * * ? ")
package site.zhourui.gulimall.seckill.scheduled;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author zr
 * @date 2022/1/6 14:29
 */
@Slf4j
@Component
@EnableScheduling
public class HelloScheduled {
    @Scheduled(cron = "* * * * * ? ")
    public void hello(){
        log.info("hello world");
    }
}

image-20220106144957968

注意事项

1、在Spring中表达式是6位组成,不允许第七位的年份
2、在周几的的位置,1-7代表周一到周日
3、定时任务不该阻塞。默认是阻塞的

3.2.2 整合异步任务

3.2.2.0 自动配置类TaskExecutionProperties
3.2.2.1 整合异步任务原因

定时任务不该阻塞。默认是阻塞的

image-20220106150637292

3.2.2.2 步骤
@EnableAsync
@Async

image-20220106150949750

3.2.2.3 设置线程池大小
spring:
  task:
    execution:
      pool:
        core-size: 8  #默认大小为8
        max-size: 50  #默认最大为int

3.2.3 开启异步任务的方式

3.2.3.1 自己异步执行CompletableFuture.runAsync
//可以让业务以异步的方式,自己提交到线程池
      CompletableFuture.runAsync(() -> {
   },execute);
3.2.3.2 设置定时任务的线程池线程数量(默认为1)

定时任务开启后其实也是有线程池的,通过更改配置修改线程池大小

spring:
  task:
    scheduling:
      pool:
        size: 2  #默认为1,就会阻塞
3.2.3.3 异步开启任务3.2章节

3.3 秒杀商品定时上架

3.3.1 秒杀上架流程图

image-20220109174707852

3.3.2 秒杀(高并发)系统关注的问题

image-20220106154048181

image-20220106154101407

3.3.3 查询从今天开始三天之内的秒杀场次信息

3.3.3.1 时间如期处理
3.3.3.1.1获取当天0点的时间
    /**
     * 当前时间
     * @return
     */
    private String startTime() {
        LocalDate now = LocalDate.now();
        LocalTime min = LocalTime.MIN;
        LocalDateTime start = LocalDateTime.of(now, min);

        //格式化时间
        String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return startFormat;
    }
3.3.3.1.2 获取加今天三天后的最后时间
    /**
     * 结束时间
     * @return
     */
    private String endTime() {
        LocalDate now = LocalDate.now();
        LocalDate plus = now.plusDays(2);
        LocalTime max = LocalTime.MAX;
        LocalDateTime end = LocalDateTime.of(plus, max);

        //格式化时间
        String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return endFormat;
    }

image-20220106172820279

3.3.4 获取三天之内的秒杀商品信息

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

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

        return R.ok().setData(seckillSessionEntities);
    }
    @Override
    public List<SeckillSessionEntity> getLates3DaySession() {

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

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

    /**
     * 当前时间
     * @return
     */
    private String startTime() {
        LocalDate now = LocalDate.now();
        LocalTime min = LocalTime.MIN;
        LocalDateTime start = LocalDateTime.of(now, min);

        //格式化时间
        String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return startFormat;
    }

    /**
     * 结束时间
     * @return
     */
    private String endTime() {
        LocalDate now = LocalDate.now();
        LocalDate plus = now.plusDays(2);
        LocalTime max = LocalTime.MAX;
        LocalDateTime end = LocalDateTime.of(plus, max);

        //格式化时间
        String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return endFormat;
    }
package site.zhourui.gulimall.seckill.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import site.zhourui.common.utils.R;

/**
 * @author zr
 * @date 2022/1/6 17:34
 */
@FeignClient("gulimall-coupon")
public interface CouponFeignService {

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

}

3.3.5 秒杀商品定时上架

3.3.5.1 定时任务的方法查询最近三天场次的商品上架
package site.zhourui.gulimall.seckill.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * @author zr
 * @date 2022/1/8 16:11
 */
@EnableAsync
@EnableScheduling
@Configuration
public class ScheduledConfig {
}

package site.zhourui.gulimall.seckill.scheduled;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.seckill.service.SeckillService;

import java.util.concurrent.TimeUnit;

/**
 * @author zr
 * @date 2022/1/8 16:08
 */

/**
 * 秒杀商品定时上架
 *  每天晚上3点,上架最近三天需要三天秒杀的商品
 *  当天00:00:00 - 23:59:59
 *  明天00:00:00 - 23:59:59
 *  后天00:00:00 - 23:59:59
 */
@Slf4j
@Service
public class SeckillScheduled {

    @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 * * ? ")
    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();
        }
    }
}

package site.zhourui.gulimall.seckill.service;

/**
 * @author zr
 * @date 2022/1/6 18:14
 */
public interface SeckillService {
    /**
     * 上架三天需要秒杀的商品
     */
    void uploadSeckillSkuLatest3Days();
}

package site.zhourui.gulimall.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.seckill.feign.CouponFeignService;
import site.zhourui.gulimall.seckill.feign.ProductFeignService;
import site.zhourui.gulimall.seckill.service.SeckillService;
import site.zhourui.gulimall.seckill.to.SeckillSkuRedisTo;
import site.zhourui.gulimall.seckill.vo.SeckillSessionWithSkusVo;
import site.zhourui.gulimall.seckill.vo.SkuInfoVo;

import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * @author zr
 * @date 2022/1/6 18:14
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    CouponFeignService couponFeignService;
    @Autowired
    private ProductFeignService productFeignService;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private StringRedisTemplate redisTemplate;
    private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
    private final String SECKILL_CHARE_PREFIX = "seckill:skus";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码

    @Override
    public void uploadSeckillSkuLatest3Days() {

        //1、扫描最近三天的商品需要参加秒杀的活动
        R lates3DaySession = couponFeignService.getLates3DaySession();
        if (lates3DaySession.getCode() == 0) {
            //上架商品
            List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            //缓存到Redis
            //1、缓存活动信息
            saveSessionInfos(sessionData);

            //2、缓存活动的关联商品信息
            saveSessionSkuInfo(sessionData);
        }

    }
    /**
     * 缓存秒杀活动信息
     * @param sessions
     */
    private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
        if (!CollectionUtils.isEmpty(sessions))
            sessions.stream().forEach(session -> {

                //获取当前活动的开始和结束时间的时间戳
                long startTime = session.getStartTime().getTime();
                long endTime = session.getEndTime().getTime();

                //存入到Redis中的key
                String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;

                //判断Redis中是否有该信息,如果没有才进行添加
                Boolean hasKey = redisTemplate.hasKey(key);
                //缓存活动信息
                if (!hasKey) {
                    //获取到活动中所有商品的skuId
                    List<String> skuIds = session.getRelationSkus().stream()
                            .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
                    redisTemplate.opsForList().leftPushAll(key,skuIds);
                }
            });

    }

    /**
     * 缓存秒杀活动所关联的商品信息
     */
    private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
        if (!CollectionUtils.isEmpty(sessions))
            sessions.stream().forEach(session -> {
                //准备hash操作,绑定hash
                BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                    //生成随机码
                    String token = UUID.randomUUID().toString().replace("-", "");
                    String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                    if (!operations.hasKey(redisKey)) {

                        //缓存我们商品信息
                        SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                        Long skuId = seckillSkuVo.getSkuId();
                        //1、先查询sku的基本信息,调用远程服务
                        R info = productFeignService.getSkuInfo(skuId);
                        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格式存入Redis中
                        String seckillValue = JSON.toJSONString(redisTo);
                        operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);

                        //如果当前这个场次的商品库存信息已经上架就不需要上架
                        //5、使用库存作为分布式Redisson信号量(限流)
                        // 使用库存作为分布式信号量
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                        // 商品可以秒杀的数量作为信号量
                        semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                    }
                });
            });
    }
}

3.3.2.2 定时任务分布式场景下的问题

分布式情况下,定时任务会启动多次【因为场次信息在redis中是List类型,会重复添加】

image-20220108175035350

image-20220108175215450

3.3.2.3 同一场次的同一商品重复上架(幂等性问题)

如果上架之前没有对本次上架的商品验证是否上架,那么就会重复上架

在商品上架时检查已上架商品是否与本次上架的商品的场次信息与商品信息重复

image-20220108180127831

3.3.6 首页展示上架商品

3.3.6.1 前端代码
<script type="text/javascript">
  function search() {
    var keyword=$("#searchText").val()
    window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;
  }

  $.get("http://seckill.gulimall.com/getCurrentSeckillSkus", 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";
  }

</script>
3.3.6.2 新增当天场次秒杀商品查询接口
package site.zhourui.gulimall.seckill.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.seckill.service.SeckillService;
import site.zhourui.gulimall.seckill.to.SeckillSkuRedisTo;

import java.util.List;

/**
 * @author zr
 * @date 2022/1/8 18:07
 */
@Controller
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    /**
     * 当前时间可以参与秒杀的商品信息
     */
    @GetMapping(value = "/getCurrentSeckillSkus")
    @ResponseBody
    public R getCurrentSeckillSkus() {

        //获取到当前可以参加秒杀商品的信息
        List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();

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

}

    List<SeckillSkuRedisTo> getCurrentSeckillSkus();
    /**
     * 获取到当前可以参加秒杀商品的信息
     * @return
     */

    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {


            //1、确定当前属于哪个秒杀场次
            long currentTime = System.currentTimeMillis();

            //从Redis中查询到所有key以seckill:sessions开头的所有数据
            Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
            for (String key : keys) {
                //seckill:sessions:1594396764000_1594453242000
                String replace = key.replace(SESSION_CACHE_PREFIX, "");
                String[] s = replace.split("_");
                //获取存入Redis商品的开始时间
                long startTime = Long.parseLong(s[0]);
                //获取存入Redis商品的结束时间
                long endTime = Long.parseLong(s[1]);

                //判断是否是当前秒杀场次
                if (currentTime >= startTime && currentTime <= endTime) {
                    //2、获取这个秒杀场次需要的所有商品信息
                    List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                    BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                    assert range != null;
                    List<String> listValue = hasOps.multiGet(range);
                    if (listValue != null && listValue.size() >= 0) {

                        List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {
                            String items = (String) item;
                            SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);
                            // redisTo.setRandomCode(null);当前秒杀开始需要随机码
                            return redisTo;
                        }).collect(Collectors.toList());
                        return collect;
                    }
                    break;
                }
            }

        return null;
    }

image-20220109170741161

3.3.7 秒杀页面渲染

image-20220109172330083

3.3.7.1 前端页面

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-product/src/main/resources/templates/item.html

3.3.7.2 根据skuId查询商品是否参加秒杀活动
    /**
     * 根据skuId查询商品是否参加秒杀活动
     */
    @GetMapping(value = "/sku/seckill/{skuId}")
    @ResponseBody
    public R getSkuSeckilInfo(@PathVariable("skuId") Long skuId) {
        SeckillSkuRedisTo to = seckillService.getSkuSeckilInfo(skuId);
        return R.ok().setData(to);
    }
    /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    SeckillSkuRedisTo getSkuSeckilInfo(Long skuId);
   /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @Override
    public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {

        //1、找到所有需要秒杀的商品的key信息---seckill:skus
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);

        //拿到所有的key
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() > 0) {
            //4-45 正则表达式进行匹配
            String reg = "\\d-" + skuId;
            for (String key : keys) {
                //如果匹配上了
                if (Pattern.matches(reg,key)) {
                    //从Redis中取出数据来
                    String redisValue = hashOps.get(key);
                    //进行序列化
                    SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);

                    //随机码
                    Long currentTime = System.currentTimeMillis();
                    Long startTime = redisTo.getStartTime();
                    Long endTime = redisTo.getEndTime();
                    //如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间
                    if (currentTime >= startTime && currentTime <= endTime) {
                        return redisTo;
                    }
                    redisTo.setRandomCode(null);
                    return redisTo;
                }
            }
        }
        return null;
    }
3.3.7.3 在查询商品详情信息时带上秒杀验证该商品此时是否处于秒杀
        // 3、查询当前sku是否参与秒杀活动
        CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
            //3、远程调用查询当前sku是否参与秒杀优惠活动
            R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
            if (skuSeckilInfo.getCode() == 0) {
                //查询成功
                SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
                });
                skuItemVo.setSeckillSkuVo(seckilInfoData);

                if (seckilInfoData != null) {
                    long currentTime = System.currentTimeMillis();
                    if (currentTime > seckilInfoData.getEndTime()) {
                        skuItemVo.setSeckillSkuVo(null);
                    }
                }
            }
        }, executor);


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

image-20220109172330083

image-20220109174142699

image-20220109174317349

3.4 秒杀

3.4.1 秒杀架构

image-20220106153619253

3.4.2 秒杀(高并发)系统关注的问题

image-20220106154048181

image-20220106154101407

3.4.3 秒杀流程一(加入购物车秒杀-弃用)

优点:加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好

缺点:秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单

image-20220110094159911

3.4.4 秒杀流程二(独立秒杀业务来处理)

优点:从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息

缺点:如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款

解决方案:不使用订单服务处理秒杀消息,需要一套独立的业务来处理

image-20220109174746252

3.4.5 创建秒杀队列

image-20220110144228078

    /**
     * 商品秒杀队列
     * 作用:削峰,创建订单
     */
    @Bean
    public Queue orderSecKillOrderQueue() {
        Queue queue = new Queue("order.seckill.order.queue", true, false, false);
        return queue;
    }

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

image-20220110145145750

3.4.6 整合rabbitmq,thymeleaf

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

        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
server:
  port: 25000
spring:
  rabbitmq:
    host: 192.168.157.128
    port: 5672
    virtual-host: /
    #开发环境关闭缓存
  thymeleaf:
    cache: false
package site.zhourui.gulimall.seckill.config;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zr
 * @date 2022/1/10 15:27
 */
@Configuration
public class MyRabbitMQConfig {
    /**
     * 以json序列化的格式发送消息
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

3.4.7 秒杀成功页面

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-seckill/src/main/resources/templates/success.html

        - id: gulimall_seckill_route
          uri: lb://gulimall-seckill
          predicates:
            - Host=seckill.gulimall.com

image-20220110161444372

# gulimall
192.168.157.128 gulimall.com
# search
192.168.157.128 search.gulimall.com
# item 商品详情
192.168.157.128 item.gulimall.com
#商城认证
192.168.157.128 auth.gulimall.com
#购物车
192.168.157.128 cart.gulimall.com
#订单
192.168.157.128 order.gulimall.com
#会员
192.168.157.128 member.gulimall.com
#秒杀
192.168.157.128 seckill.gulimall.com
#单点登录
127.0.0.1 ssoserver.com

127.0.0.1 client1.com

127.0.0.1 client2.com

3.4.8 配置拦截器(未登录请求拦截)

package site.zhourui.gulimall.seckill.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.vo.MemberResponseVo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;

import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;

/**
 * @author zr
 * @date 2022/1/10 15:13
 */
@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uri = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/kill", uri);
        // 只有秒杀需要拦截,其他直接放行
        if (match) {
            HttpSession session = request.getSession();
            //获取登录的用户信息
            MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);
            if (attribute != null) {
                //把登录后用户的信息放在ThreadLocal里面进行保存
                loginUser.set(attribute);
                return true;
            } else {
                //未登录,返回登录页面
                response.setContentType("text/html;charset=UTF-8");
                PrintWriter out = response.getWriter();
                out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
                // session.setAttribute("msg", "请先进行登录");
                // response.sendRedirect("http://auth.gulimall.com/login.html");
                return false;
            }
        }
        return true;
    }
}

package site.zhourui.gulimall.seckill.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.seckill.interceptor.LoginUserInterceptor;

/**
 * @author zr
 * @date 2022/1/10 15:12
 */
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginUserInterceptor loginUserInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
    }
}

3.4.9 秒杀接口

    /**
     * 商品进行秒杀(秒杀开始)
     * 查看表 oms_order_item
     */
    @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";
    }
    /**
     * 当前商品进行秒杀(秒杀开始)
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String kill(String killId, String key, Integer num) throws InterruptedException {

        long s1 = System.currentTimeMillis();
        //获取当前用户的信息
        MemberResponseVo user = LoginUserInterceptor.loginUser.get();

        //1、获取当前秒杀商品的详细信息从Redis中获取
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String skuInfoValue = hashOps.get(killId);
        if (StringUtils.isEmpty(skuInfoValue)) {
            return null;
        }
        //(合法性效验)
        SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTime = System.currentTimeMillis();
        //判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
        if (currentTime >= startTime && currentTime <= endTime) {

            //2、效验随机码和商品id
            String randomCode = redisTo.getRandomCode();
            String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
            if (randomCode.equals(key) && killId.equals(skuId)) {
                //3、验证购物数量是否合理和库存量是否充足
                Integer seckillLimit = redisTo.getSeckillLimit();

                //获取信号量
                String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
                Integer count = Integer.valueOf(seckillCount);
                //判断信号量是否大于0,并且买的数量不能超过库存
                if (count > 0 && num <= seckillLimit && count > num ) {
                    //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
                    //SETNX 原子性处理
                    String redisKey = user.getId() + "-" + skuId;
                    //设置自动过期(活动结束时间-当前时间)
                    Long ttl = endTime - currentTime;
                    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (aBoolean) {
                        //占位成功说明从来没有买过,分布式锁(获取信号量-1)【分布式锁-1】
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        //TODO 秒杀成功,快速下单
                        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                        //保证Redis中还有商品库存
                        if (semaphoreCount) {
                            //创建订单号和订单信息发送给MQ
                            // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
                            String timeId = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(timeId);
                            orderTo.setMemberId(user.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                            orderTo.setSkuId(redisTo.getSkuId());
                            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                            rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2 - s1));
                            return timeId;
                        }
                    }
                }
            }
        }
        long s3 = System.currentTimeMillis();
        log.info("耗时..." + (s3 - s1));
        return null;
    }
3.4.9.1 限制同一用户重复秒杀

image-20220110161838342

 //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
                    //SETNX 原子性处理
                    String redisKey = user.getId() + "-" + skuId;
                    //设置自动过期(活动结束时间-当前时间)
                    Long ttl = endTime - currentTime;
                    Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (aBoolean) {
                        //占位成功说明从来没有买过,分布式锁(获取信号量-1)【分布式锁-1】
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        //TODO 秒杀成功,快速下单tryAcquire尝试扣减信号量,超时也会失败
                        boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                    }

image-20220110154324233

image-20220110155557781

3.4.10 秒杀消息消费

package site.zhourui.gulimall.order.listener;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import site.zhourui.common.to.mq.SeckillOrderTo;
import site.zhourui.gulimall.order.service.OrderService;

import java.io.IOException;

/**
 * @author zr
 * @date 2022/1/10 15:52
 */
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {

    @Autowired
    private OrderService orderService;

    @RabbitHandler
    public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
        log.info("准备创建秒杀单的详细信息...");
        try {
            orderService.createSeckillOrder(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

    /**
     * 创建秒杀单
     * @param orderTo
     */
    void createSeckillOrder(SeckillOrderTo orderTo);
    /**
     * 创建秒杀单
     * @param orderTo
     */
    @Override
    public void createSeckillOrder(SeckillOrderTo orderTo) {

        //TODO 保存订单信息
        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);
    }

image-20220110155535310

3.4.11 秒杀总结

3.4.11.1 服务单一职责+独立部署
	秒杀服务即使自己扛不住压力,挂掉。不要影响别人
	
	解决:新增秒杀服务
3.4.11.2 秒杀链接加密
	防止恶意攻击,模拟秒杀请求,1000次ls攻击。
	防止链接暴露.自己工作人员,提前秒杀商品。
	
	解决:请求需要随机码,在秒杀开始时随机码才会放在商品信息中
3.4.11.3 库存预热+快速扣减【限流,并发信号量】
	秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求
	
	解决:库存放入redis中,使用分布式信号量扣减+限流
3.4.11.4 动静分离
	nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
	使用CDN网络,分担本集群压力
	
	解决:nginx

例:10万个人来访问商品详情页,这个详情页会发送63个请求,但是只有3个请求到达后端,60个请求是前端的。一共30万请求到达后端,600万个请求到达nginx或cdn
3.4.11.5 恶意请求拦截
识别非法攻击请求并进行拦截,网关层拦截,放行到后太服务的请求都是正常请求
	在网关层拦截:一些不带令牌的请求循环发送
	
	解决:使用网关拦截
    	本系统做了登录拦截器【在各微服务创建的,未登录跳转登录页面】
3.4.11.6 流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车
	1、输入验证码需要时间,将流量错开了【速度有快有慢】
	2、加入购物车,然后再结算【速度有快有慢】
	
	解决:使用购物车逻辑
3.4.11.7 限流&熔断&降级
前踹限流+后端限流
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩

	前端限流:1、每次点击1s后才能再次点击
			2、验证登录
	后端限流:1、网关限流,例如访问秒杀的流量到达10W等2S再将请求传过去【其中10W是集群的峰值】
			2、就算是合理的10次也只放行1-23、熔断:远程访问失败,快速返回,并且下次不要再请求这个节点【防止请求长时间等待】
			4、降级:请求量太大了,直接将请求转发到一个错误页面

    出现一种情况:集群的处理能力是10W,网关放行了10W的请求,但此时秒杀服务掉线了2台,处理能力下降导致请求堆积,最后资源耗尽服务器全崩了
    
	解决:spring alibaba sentinel
    以前是Hystrix,现在不更新了就不用了
3.4.11.8 队列削峰
100万个商品,每个商品的秒杀库存是100,会产生1亿的流量到后台,全部放入队列中,然后订单监听队列一个个创建订单扣减库存

	解决:秒杀服务将创建订单的请求存入mq,订单服务监听mq。
    优点:要崩只会崩秒杀服务,不会打垮其他服务【商品服务、订单服务、购物车服务】【第一套实现逻辑会导致这些问题】【看秒杀请求的两种实现】
举报

相关推荐

0 条评论