谷粒商城篇章11--P311-P325--秒杀服务【分布式高级篇八】

news2024/11/23 23:45:51

目录

1 后台添加秒杀商品

1.1 配置优惠券服务网关

1.2 添加秒杀场次

1.3 上架秒杀商品

2 定时任务

2.1 cron 表达式

2.2 cron表达式特殊字符

2.3 cron示例

3 秒杀服务

3.1 创建秒杀服务模块

3.1.1 pom.xml

3.1.2 application.yml配置

3.1.3 bootstrap.yml配置

3.1.4 启动类上添加注解

3.2 SpringBoot整合定时任务与异步任务

3.2.1 整合定时任务

3.2.2 整合异步任务

3.2.2.1 定时任务阻塞

3.2.2.2 解决定时任务阻塞的方式

3.2.2.3 整合异步任务步骤

3.3 秒杀商品上架

3.3.1 秒杀商品上架流程

3.3.2 时间日期处理

3.3.2.1 获取当天0点整的时间

3.3.2.2 获取含今天的三天后的最后时间

3.3.3 获取三天内要开始的秒杀活动及秒杀商品信息

3.3.4 秒杀商品定时上架

3.3.4.1 使用定时任务上架最近三天需要秒杀的商品

3.3.4.2 定时任务分布式情况下的问题

3.3.4.3 解决同一活动同一商品重复上架(幂等性保证)

3.3.5 首页展示上架的秒杀商品

3.3.5.1 配置网关

3.3.5.2 SwitchHosts增加配置

3.3.5.3 获取当前时间可以参与秒杀的商品信息

3.3.5.4 首页代码

3.3.5.5 测试

3.3.6 秒杀页面渲染

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

3.3.6.2 查询商品详情时验证当前商品是否参与秒杀活动

3.3.6.3 商品详情页代码

3.4 秒杀

3.4.1 秒杀架构

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

3.4.3 登录检查(配置登录拦截器) 

3.4.3.1 商品详情页登录拦截

3.4.3.2 秒杀服务配置登录拦截器

3.4.3.2.1 引入依赖

3.4.3.2.2 SpringSession 相关配置

3.4.3.2.3 yml 配置

3.4.3.2.4 启用Redis会话管理

3.4.3.2.5 配置登录拦截器

3.4.5 秒杀流程

3.4.5.1 流程一(加入购物车秒杀——弃用)

3.4.5.2 流程二(独立秒杀业务处理——推荐)

3.4.6 创建秒杀队列、绑定关系

3.4.7 整合rabbitmq、thymeleaf

3.4.7.1 引入依赖

3.4.7.2 yml配置

3.4.7.3 配置RabbitMQ序列化方式 

3.4.8 秒杀成功页面 

3.4.9 秒杀接口

3.4.9.1 (幂等性)限制同一用户重复秒杀

3.4.10 秒杀消息监听消费

3.5 秒杀总结

3.5.1 服务单一职责+独立部署

3.5.2 秒杀连接加密

3.5.3 库存预热+快速扣减

3.5.4 动静分离

3.5.5 恶意请求拦截

3.5.6 流量错峰

3.5.7 限流+熔断+降级

3.5.8 队列削峰


1 后台添加秒杀商品

复制优惠券前端代码到 src\views\modules路径下,如下:

1.1 配置优惠券服务网关

未配置优惠券服务网关之前,如下:

网关配置如下:

gulimall-gateway/src/main/resources/application.yml 

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

1.2 添加秒杀场次

1.3 上架秒杀商品

场次id对应数据库中的promotion_session_id字段。

 上架秒杀商品bug,在任意一个场次可以查询所有场次的上架商品。如下:点击2号场次关联商品可以看到场次1关联的商品。

解决方案:修改场次关联商品查询接口,添加查询条件场次id.

gulimall-coupon/src/main/java/com/wen/gulimall/coupon/service/impl/SeckillSkuRelationServiceImpl.java

2 定时任务

2.1 cron 表达式

Cron - 在线Cron表达式生成器

2.1.1 cron表达式语法

语法:秒 分 时 日 月 周 年(Spring不支持年)

https://www.quartz-scheduler.org/documentation/

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 #
YearYESempty, 1970-2099, - * /

2.2 cron表达式特殊字符

(1),  :枚举

        (cron="7,9,23 * * * * ?"):任意时刻的7,9,23秒启动这个任务;

(2)-  :范围

        (cron="7-20 * * * * ?"):任意时刻的7-20秒之间,每秒启动一次;

(3)*  :任意

        指定位置的任意时刻都可以;

(4)/  :步长

        (cron="7/5 * * * * ?"):第7秒启动,每5秒一次;

        (cron="*/5 * * * * ?"):任意秒启动,每5秒一次;

(5)?  :(出现在日或周几的位置)为了防止日和周冲突,在周和日上如果要写通配符使用? 

        (cron="* * * 1 * ?"):每个月的1号,启动这个任务;

(6)L  :出现在日和周的位置

        last:最后一个

        (cron="* * * ? * 3L"):每个月的最后一个周二,周日是1;

(7)W  :

        Work Day:工作日

        (cron="* * * W * ?"):每个月的工作日触发;

        (cron="* * * LW * ?"):每个月的最后一个工作日触发;

(8)#  :第几个

        (cron="* * * ? * 5#2"):每个月的第2个周4。

2.3 cron示例

Expression

Meaning

0 0 12 * * ?

每天中午12点触发

0 15 10 ? * *

每天的10点15分触发

0 15 10 * * ?

每天的10点15分触发

0 15 10 * * ? *

每天的10点15分触发

0 15 10 * * ? 2005

2005年的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 WED

3月的每个星期三的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 2002-2005

2002年到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 创建秒杀服务模块

3.1.1 pom.xml

gulimall-seckill/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.7.8</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.wen.gulimall</groupId>
	<artifactId>gulimall-seckill</artifactId>
	<version>1.0</version>
	<name>gulimall-seckill</name>
	<description>秒杀服务</description>

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>2021.0.5</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>com.wen.gulimall</groupId>
			<artifactId>gulimall-common</artifactId>
			<version>1.0</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>

		<dependency>
			<groupId>org.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>

3.1.2 application.yml配置

端口、应用名、nacos发现中心、redis

gulimall-seckill/src/main/resources/application.yml

server:
  port: 25000
spring:
  application:
    name: gulimall-seckill
  cloud:
    nacos:
      discovery:
        server-addr: 172.xx.xx.10:8848
  redis:
    host: 172.xx.xx.10

3.1.3 bootstrap.yml配置

nacos配置中心

gulimall-seckill/src/main/resources/bootstrap.yml

spring:
  cloud:
    nacos:
      config:
        server-addr: 172.xx.xx.10:8848

3.1.4 启动类上添加注解

启动类添加Feign远程调用、服务发现、排除数据库自动配置类。

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/GulimallSeckillApplication.java

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

3.2 SpringBoot整合定时任务与异步任务

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

3.2.1 整合定时任务

自动配置类TaskSchedulingAutoConfiguration

属性TaskSchedulingProperties

在类上使用注解开启定时任务功能,如下:

@Component // 注入容器中
@EnableScheduling // 开启定时任务

  在需要开启定时任务的方法上使用注解,为该方法开启定时任务,根据cron表达式定时执行,如下:

@Scheduled(cron = "* * * ? * 1")

注意:

(1)Spring中cron由6位组成,不允许第7位的年

(2)在周几的位置,1-7代表周一到周日;MON-SUN

(3)定时任务不应该阻塞。默认是阻塞的

示例:

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/scheduled/HelloSchedule.java

@Slf4j
@Component
@EnableScheduling // 开启定时任务
public class HelloSchedule {
 
    @Scheduled(cron = "* * * ? * 2")
    public void hello() throws InterruptedException {
        log.info("hello ...");
    }
}

测试结果,每秒执行一次,如下:

3.2.2 整合异步任务

整合异步任务为了解决定时任务不应该阻塞,默认是阻塞的。

自动配置类TaskExecutionAutoConfiguration

属性TaskExecutionProperties

3.2.2.1 定时任务阻塞

模拟业务处理时间较长,查看定时任务的执行情况,如下:

@Slf4j
@Component
@EnableScheduling // 开启定时任务
public class HelloSchedule {
    
    @Scheduled(cron = "* * * ? * 2")
    public void hello() throws InterruptedException {
        log.info("hello ...");
        Thread.sleep(3000);
    }
}

测试结果,日志打印间隔4秒打印一次,说明定时任务阻塞,执行结果如下:

3.2.2.2 解决定时任务阻塞的方式

方式一:使用异步编排,可以业务以异步的方式运行,自己提交到线程池,如下:

CompletableFuture.runAsync(()->{
    xxxService.hello();
},executor);

不生效方式二:支持定时任务线程池,设置 TaskSchedulingProperties,线程池大小默认是1,修改线程池大小,如下:

spring:
  task:
    scheduling:
      pool:
        size: 5

方式三:异步任务,实现过程见3.2.2.3 

3.2.2.3 整合异步任务步骤

1. 在类上标注注解开启异步功能

@EnableAsync

2. 在需要异步执行的方法上标注注解,开启异步任务

@Async

3. 测试

@Slf4j
@Component
@EnableAsync
@EnableScheduling // 开启定时任务
public class HelloSchedule {
 
    @Async
    @Scheduled(cron = "* * * ? * 2")
    public void hello() throws InterruptedException {

        log.info("hello ...");
        Thread.sleep(3000);
    }
}

日志每秒打印一次,定时任务没有阻塞了,如下:

4. 设置线程池大小

异步任务的线程池最大线程数是Integer的最大值,项目中要对其进行限制。

gulimall-seckill/src/main/resources/application.yml 

spring:
  task:
    execution:
      pool:
        core-size: 5
        max-size: 50

3.3 秒杀商品上架

3.3.1 秒杀商品上架流程

3.3.2 时间日期处理

3.3.2.1 获取当天0点整的时间
/**
 * 开始日期:今天 00:00:00
 * @return
 */
private String startTime(){
    LocalDate now = LocalDate.now();
    LocalTime min = LocalTime.MIN;
    LocalDateTime of = LocalDateTime.of(now, min);
    return of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
3.3.2.2 获取含今天的三天后的最后时间
/**
 * 结束日期:含今天的三天后的最后时间 23:59:59
 * @return
 */
private String endTime(){
    LocalDate now = LocalDate.now();
    LocalDate localDate = now.plusDays(2);
    LocalDateTime of = LocalDateTime.of(localDate, LocalTime.MAX);
    return of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

测试结果,如下:

3.3.3 获取三天内要开始的秒杀活动及秒杀商品信息

gulimall-coupon/src/main/java/com/wen/gulimall/coupon/controller/SeckillSessionController.java

@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {
    @Autowired
    private SeckillSessionService seckillSessionService;

    /**
     * 查询最近三天要开始的秒杀活动
     * @return
     */
    @GetMapping("/latest3DaySession")
    public R getLatest3DaySession(){
        List<SeckillSessionEntity> sessionEntities =  seckillSessionService.getLatest3DaySession();
        return R.ok().setData(sessionEntities);
    }

    ...
}

gulimall-coupon/src/main/java/com/wen/gulimall/coupon/service/SeckillSessionService.java

/**
 * 秒杀活动场次
 *
 * @author wen
 */
public interface SeckillSessionService extends IService<SeckillSessionEntity> {

    ...

    List<SeckillSessionEntity> getLatest3DaySession();
}

gulimall-coupon/src/main/java/com/wen/gulimall/coupon/service/impl/SeckillSessionServiceImpl.java 

@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {
    @Resource
    private SeckillSkuRelationService seckillSkuRelationService;

    ...

    @Override
    public List<SeckillSessionEntity> getLatest3DaySession() {
        // 查询最近三天要开始的秒杀活动
        List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
        if(CollUtil.isNotEmpty(list)) {
            List<SeckillSessionEntity> collect = list.stream().map(session -> {
                Long id = session.getId();
                List<SeckillSkuRelationEntity> relations = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
                session.setRelationSkus(relations);
                return session;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

    /**
     * 开始日期:今天 00:00:00
     * @return
     */
    private String startTime(){
        LocalDate now = LocalDate.now();
        LocalTime min = LocalTime.MIN;
        LocalDateTime of = LocalDateTime.of(now, min);
        return of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

    /**
     * 结束日期:含今天的三天后的最后时间 23:59:59
     * @return
     */
    private String endTime(){
        LocalDate now = LocalDate.now();
        LocalDate localDate = now.plusDays(2);
        LocalDateTime of = LocalDateTime.of(localDate, LocalTime.MAX);
        return of.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}

秒杀服务远程调用优惠券服务的feign接口

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/feign/CouponFeignService.java

/**
 * 远程调用优惠服务
 *
 * @author w
 * @date 2024/07/23 14:16
 */
@FeignClient("gulimall-coupon")
public interface CouponFeignService {

    @GetMapping("/coupon/seckillsession/latest3DaySession")
    R getLatest3DaySession();
}

3.3.4 秒杀商品定时上架

3.3.4.1 使用定时任务上架最近三天需要秒杀的商品

定时任务+异步任务配置

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/config/ScheduledConfig.java

/**
 * 定时任务配置类
 *      异步任务+定时任务
 *
 * @author w
 * @date 2024/07/23 14:02
 */
@EnableAsync // 开启异步任务
@EnableScheduling // 开启定时任务
@Configuration
public class ScheduledConfig {
}

秒杀商品的定时上架

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/scheduled/SeckillSkuScheduled.java

/**
 * 秒杀商品的定时上架
 *      每天晚上3点,上架最近三天需要秒杀的商品。
 *      当天00:00:00 - 23:59:59
 *      明天00:00:00 - 23:59:59
 *      后天00:00:00 - 23:59:59
 *
 * @author w
 * @date 2024/07/23 13:58
 */
@Slf4j
@Service
public class SeckillSkuScheduled {
    @Resource
    private SeckillService seckillService;
    @Resource
    private RedissonClient redissonClient;
    private final String upload_lock = "seckill:upload:lock";
    //todo幂等性上架
    @Scheduled(cron="0 * * * * ?")
    public void uploadSeckillSkuLatest3Days(){
        // 1. 重复上架无需处理
        log.info("上架秒杀商品的信息.....");
        // 分布式锁。锁的业务执行完成,状态已更新完成。释放锁以后,其他人获取到就会拿到最新的状态。
        // 加锁保证原子性,直接判断无法保证原子性
        RLock lock = redissonClient.getLock(upload_lock);
        lock.lock(10, TimeUnit.SECONDS);
        try {
            seckillService.uploadSeckillSkuLatest3Days();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

上架最近三天参与秒杀活动的商品

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/SeckillService.java 

/**
 * 秒杀业务层
 *
 * @author w
 * @date 2024/07/23 14:09
 */
public interface SeckillService {
    /**
     * 上架最近三天参与秒杀活动的商品
     */
    void uploadSeckillSkuLatest3Days();
}

 gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/impl/SeckillServiceImpl.java

@Service
public class SeckillServiceImpl implements SeckillService {
    @Resource
    private CouponFeignService couponFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:sku:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // + 商品随机码
    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 1. 数据库查询最近三天需要参与秒杀的活动
        R session = couponFeignService.getLatest3DaySession();
        if(session.getCode() == 0){
            // 上架商品
            List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
            });
            if(CollUtil.isNotEmpty(sessionData)) {
                // 缓存到redis
                // 1.缓存活动信息
                saveSessionInfos(sessionData);
                // 2.缓存活动的关联商品信息
                saveSessionSkuInfos(sessionData);
            }
        }
    }

    private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions){
        sessions.stream().forEach(session->{
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            Boolean hasKey = stringRedisTemplate.hasKey(key);
            List<SeckillSkuVo> relationSkus = session.getRelationSkus();
            // 幂等性保证
            if(Boolean.FALSE.equals(hasKey) && CollUtil.isNotEmpty(relationSkus)) {
                List<String> collect = relationSkus.stream().map(item -> item.getPromotionSessionId().toString()+"_"+item.getSkuId().toString()).collect(Collectors.toList());
                // 缓存活动信息
                stringRedisTemplate.opsForList().leftPushAll(key, collect);
            }
        });

    }
    private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions){
        sessions.forEach(session -> {
            // 准备hash操作
            BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                // 4. 商品的随机码(防止恶意攻击、公平秒杀)
                String token = UUID.randomUUID().toString().replaceAll("-","");
                if(!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString())) {
                    // 缓存商品
                    SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
                    // 1. sku的基本数据
                    R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (skuInfo.getCode() == 0) {
                        SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        seckillSkuRedisTo.setSkuInfo(info);
                    }
                    // 2. sku的秒杀信息
                    BeanUtil.copyProperties(seckillSkuVo, seckillSkuRedisTo);
                    // 3. 设置当前商品的秒杀时间信息
                    seckillSkuRedisTo.setStartTime(session.getStartTime().getTime());
                    seckillSkuRedisTo.setEndTime(session.getEndTime().getTime());

                    seckillSkuRedisTo.setRandomCode(token);
                    String jsonString = JSON.toJSONString(seckillSkuRedisTo);
                    ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(),jsonString);

                    // 5.使用库存作为分布式的信号量 限流
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                    // 商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
                }
            });
        });
    }
}
3.3.4.2 定时任务分布式情况下的问题

问题:分布式情况下,定时任务会执行多次,活动信息在redis中以list的方式存储,会重复添加。

解决方案:使用分布式锁。

3.3.4.3 解决同一活动同一商品重复上架(幂等性保证)

上架之前没有对上架的商品进行校验是否已上架,就会重复上架。解决方案如下:

缓存活动信息幂等性保证:

缓存活动关联商品信息幂等性保证:

3.3.5 首页展示上架的秒杀商品

3.3.5.1 配置网关

gulimall-gateway/src/main/resources/application.yml

- id: gulimall_seckill_route
  uri: lb://gulimall-seckill
  predicates:
    # 由以下的主机域名访问转发到会员服务
    - Host=seckill.gulimall.com
3.3.5.2 SwitchHosts增加配置

添加秒杀服务的域名与ip映射:xxx.xxx.11.10 seckill.gulimall.com 

3.3.5.3 获取当前时间可以参与秒杀的商品信息

秒杀商品信息实体

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/to/SeckillSkuRedisTo.java

@Data
public class SeckillSkuRedisTo {
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 商品秒杀随机码
     */
    private String randomCode;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;
    // sku的详细信息
    private SkuInfoVo skuInfo;

    // 当前商品秒杀的开始时间
    private Long startTime;
    // 当前商品秒杀的结束时间
    private Long endTime;
}

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/controller/SeckillController.java

@Controller
public class SeckillController {
    @Resource
    private SeckillService seckillService;

    /**
     * 返回当前时间可以参与的秒杀商品信息
     * @return
     */
    @ResponseBody
    @GetMapping("/currentSeckillSkus")
    public R getCurrentSeckillSkus(){
        List<SeckillSkuRedisTo> skus = seckillService.getCurrentSeckillSkus();
        return R.ok().setData(skus);
    }

    ...
}

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/SeckillService.java

public interface SeckillService {
    
    ...

    List<SeckillSkuRedisTo> getCurrentSeckillSkus();
}

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/impl/SeckillServiceImpl.java

@Service
public class SeckillServiceImpl implements SeckillService {
    @Resource
    private CouponFeignService couponFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:sku:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // + 商品随机码

    ...

    /**
     * 返回当前时间可以参与的秒杀商品信息
     * @return
     */
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        // 1. 确定当前时间属于哪个秒杀场次
        long time = System.currentTimeMillis();
        Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        for(String key:keys) {
            String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            Long startTime = Long.parseLong(s[0]);
            Long endTime = Long.parseLong(s[1]);
            if (time >= startTime && time <= endTime) {
                // 2. 获取这个秒杀场次需要的所有商品信息
                List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<String> list = ops.multiGet(range);
                if (list != null && list.size() > 0) {
                    List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
                        SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(item.toString(), SeckillSkuRedisTo.class);
                        //seckillSkuRedisTo.setRandomCode(null); 当前秒杀开始就需要随机码,预告不需要
                        return seckillSkuRedisTo;
                    }).collect(Collectors.toList());
                    return collect;
                }
                break;
            }
        }
        return null;
    }

    ...
}
3.3.5.4 首页代码

gulimall-product/src/main/resources/templates/index.html

<script type="text/javascript">
  function search() {
    var keyword=$("#searchText").val()
    window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;
  }
  function to_href(skuId){
    location.href = "http://item.gulimall.com/"+skuId+".html";
  }
  $.get("http://seckill.gulimall.com/currentSeckillSkus",function (resp){
    if(resp.data.length>0){
      resp.data.forEach(item=>{
        $("<li onclick='to_href("+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");
      })
    }
  });

</script>
3.3.5.5 测试

访问商城首页。

3.3.6 秒杀页面渲染

如果商品正在秒杀中,“加入购物车” 变为 “立即抢购”。

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

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/controller/SeckillController.java

@Controller
public class SeckillController {
    @Resource
    private SeckillService seckillService;

    ...

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

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/SeckillService.java

public interface SeckillService {
    ...

    /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    SeckillSkuRedisTo getSkuSeckillInfo(Long skuId);
}

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/impl/SeckillServiceImpl.java 

@Service
public class SeckillServiceImpl implements SeckillService {
    @Resource
    private CouponFeignService couponFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:sku:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // + 商品随机码
   
    ...

    /**
     * 根据skuId查询商品是否参加秒杀活动
     * @param skuId
     * @return
     */
    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        // 1. 找到所有需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        // 获取所有的key
        Set<String> keys = hashOps.keys();
        if(keys!=null && keys.size()>0){
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                if(Pattern.matches(regx,key)) {
                    String json = hashOps.get(key);
                    SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
                    // 随机码
                    long current = System.currentTimeMillis();
                    if (current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()) {
                        // 正在参与秒杀活动
                    } else {
                        skuRedisTo.setRandomCode(null);
                    }
                    return skuRedisTo;
                }
            }
        }
        return null;
    }
    
    ...
}
3.3.6.2 查询商品详情时验证当前商品是否参与秒杀活动

远程调用秒杀服务根据skuId查询当前商品是否参与秒杀活动。

gulimall-product/src/main/java/com/wen/gulimall/product/feign/SeckillFeignService.java

@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
    @GetMapping("/sku/seckill/{skuId}")
    R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}

商品服务秒杀信息vo,复制秒杀服务SeckillSkuRedisTo实体。

gulimall-product/src/main/java/com/wen/gulimall/product/vo/SeckillInfoVo.java 

@Data
public class SeckillInfoVo {
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 商品秒杀随机码
     */
    private String randomCode;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private Integer seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;

    // 当前商品秒杀的开始时间
    private Long startTime;
    // 当前商品秒杀的结束时间
    private Long endTime;
}

SkuItemVo添加秒杀商品信息属性seckillInfo

gulimall-product/src/main/java/com/wen/gulimall/product/vo/SkuItemVo.java 

@Data
public class SkuItemVo {
    // 获取sku的基本信息 pms_sku_info
    private SkuInfoEntity info;

    private boolean hasStock = true;

    // 获取sku的图片信息 pms_sku_images
    private List<SkuImagesEntity> images;

    // 获取spu的销售属性组合
    private List<SkuItemSaleAttrVo> saleAttr;

    // 获取spu的介绍
    private SpuInfoDescEntity desc;

    // 获取spu的规格参数信息
    private List<SpuItemAttrGroupVo> groupAttrs;

    // 当前商品的秒杀优惠信息
    private SeckillInfoVo seckillInfo;


}

查询商品详情业务层添加查询当前sku是否参与秒杀活动。

gulimall-product/src/main/java/com/wen/gulimall/product/service/impl/SkuInfoServiceImpl.java

        // 6.查询当前sku是否参与秒杀活动
        CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
            R skuSeckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
            if (skuSeckillInfo.getCode() == 0) {
                SeckillInfoVo data = skuSeckillInfo.getData(new TypeReference<SeckillInfoVo>() {
                });
                skuItemVo.setSeckillInfo(data);
            }
        }, threadPoolExecutor);

        // 等待所有任务都完成,不用写infoFuture,因为saleAttrFuture/descFuture/baseAttrFuture他们依赖infoFuture完成的结果
        CompletableFuture.anyOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,secKillFuture).get();
3.3.6.3 商品详情页代码

gulimall-product/src/main/resources/templates/item.html

<div class="box-summary clear">
	<ul>
		<li>京东价</li>
		<li>
			<span>¥</span>
			<span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>
		</li>
		<li style="color: red" th:if="${item.seckillInfo!=null}">
			<span th:if="${#dates.createNow().getTime()<item.seckillInfo.startTime}">
				商品将会在[[${#dates.format(new java.util.Date(item.seckillInfo.startTime),"yyyy-MM-dd HH:mm:ss")}]]进行秒杀
			</span>
			<span th:if="${#dates.createNow().getTime()>=item.seckillInfo.startTime && #dates.createNow().getTime()<=item.seckillInfo.endTime}">
				秒杀价:[[${#numbers.formatDecimal(item.seckillInfo.seckillPrice,1,2)}]]
			</span>

		</li>
		<li>
			<a href="">
				预约说明
			</a>
		</li>
	</ul>
</div>
<div class="box-btns-two" th:if="${#dates.createNow().getTime()>=item.seckillInfo.startTime && #dates.createNow().getTime()<=item.seckillInfo.endTime}">
	<a href="#" id="secKillA" th:attr="skuId=${item.info.skuId}">
		立即抢购
	</a>
</div>
<div class="box-btns-two" th:if="${#dates.createNow().getTime()<item.seckillInfo.startTime || #dates.createNow().getTime()>item.seckillInfo.endTime}">
	<a href="#" id="addToCartA" th:attr="skuId=${item.info.skuId}">
		加入购物车
	</a>
</div>

3.4 秒杀

3.4.1 秒杀架构

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

3.4.3 登录检查(配置登录拦截器) 

登录后,才能进行秒杀。

3.4.3.1 商品详情页登录拦截

正在秒杀的商品,点击“立即抢购”,登录了才能进行秒杀。

<script>
    ...

    $("#secKillA").click(function (){
	    var isLogin = [[${session.loginUser!=null}]]
	    if(isLogin){
		    var killId = $(this).attr("sessionId")+"_"+$(this).attr("skuId");
		    var key = $(this).attr("code");
		    var num = $('#numInput').val();
		    location.href = "http://seckill.gulimall.com/kill?killId="+killId+"&key="+key+"&num="+num;
	    }else {
		    alert("秒杀请先登录");
	    }
    });
</script>
3.4.3.2 秒杀服务配置登录拦截器
3.4.3.2.1 引入依赖

添加 redis 依赖,SpringSession相关依赖在公共模块,已引入公共模块。

gulimall-seckill/pom.xml

<!--	lettuce有问题,引入jedis 	-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<exclusions>
		<exclusion>
			<groupId>io.lettuce</groupId>
			<artifactId>lettuce-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
</dependency>
3.4.3.2.2 SpringSession 相关配置

GulimallSessionConfig.java在公共模块gulimall-common。

3.4.3.2.3 yml 配置

登录信息存储在redis

spring:
  redis:
    host: 172.xxx.xxx.10
  session:
    store-type: redis
3.4.3.2.4 启用Redis会话管理

@EnableRedisHttpSession

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/GulimallSeckillApplication.java

@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {

	public static void main(String[] args) {
		SpringApplication.run(GulimallSeckillApplication.class, args);
	}

}
3.4.3.2.5 配置登录拦截器

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/interceptor/LoginUserInterceptor.java

/**
 * @author W
 * @createDate 2024/02/27 16:58
 * @description: 登录拦截器
 * 从session中(redis中)获取了登录信息,封装到ThreadLocal
 * 自定义拦截器需要添加到webmvc中,否则不起作用
 */
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    // 同一个线程共享数据
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/kill", requestURI);
        if(match) {
            MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
            if (attribute != null) {
                // 登录成功
                loginUser.set(attribute);
                return true;
            } else {
                // 没登录,去登录
                request.getSession().setAttribute("msg", "请先进行登录");
                response.sendRedirect("http://auth.gulimall.com/login.html");
                return false;
            }
        }
        return true;
    }
}

 gulimall-seckill/src/main/java/com/wen/gulimall/seckill/config/SeckillWebConfig.java

@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
    @Resource
    private LoginUserInterceptor loginUserInterceptor;

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

3.4.5 秒杀流程

3.4.5.1 流程一(加入购物车秒杀——弃用)

(1)优点:天然的流量错峰,与正常购物流程一致,价格为秒杀价,数据模型与正常下单流程一致。

(2)缺点:秒杀流量级联映射到其他服务,比如:购物车服务、订单服务,秒杀服务高并发下,可能会拖垮购物车等服务,导致非秒杀商品无法正常加入购物车下单。

3.4.5.2 流程二(独立秒杀业务处理——推荐)

(1)优点:从用户下单到返回没有对数据库进行任何操作,只做了一些合法性校验,校验通过生成订单号并发送消息。

(2)缺点:如果订单服务挂了,无法消费消息,订单一直创建不好导致用户无法支付。

(3)解决方案:不使用订单服务处理秒杀消息,使用独立的业务进行秒杀处理,保证高并发秒杀不影响拖垮其他服务。

3.4.6 创建秒杀队列、绑定关系

gulimall-order/src/main/java/com/wen/gulimall/order/config/MyMQConfig.java

/**
     * 商品秒杀队列
     * 作用:流量削峰、监听创建订单
     * @return
     */
    @Bean
    public Queue orderSeckillOrderQueue(){
        //String name, boolean durable, boolean exclusive, boolean autoDelete,
        //			@Nullable Map<String, Object> arguments
        return new Queue("order.seckill.order.queue",true,false,false);
    }

    @Bean
    public Binding orderSeckillOrderQueueBinding(){
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        //			@Nullable Map<String, Object> arguments
        return new Binding("order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);
    }

3.4.7 整合rabbitmq、thymeleaf

rabbitmq:用于秒杀校验等通过订单的创建。

thymeleaf:用于秒杀成功页面。

3.4.7.1 引入依赖

gulimall-seckill/pom.xml

<!-- 模板引擎 :thymeleaf -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 消息队列amqp -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.4.7.2 yml配置

gulimall-seckill/src/main/resources/application.yml

spring:
  rabbitmq:
    host: 172.1.11.10
    port: 5672
    virtual-host: /
    # 开启发送端确认
    publisher-confirm-type: correlated
    # 开启发送端消息抵达队列的确认,默认是false
    publisher-returns: true
  thymeleaf:
    # 关闭缓存
    cache: false
3.4.7.3 配置RabbitMQ序列化方式 

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/config/MyRabbitConfig.java

@Configuration
public class MyRabbitConfig {

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

3.4.8 秒杀成功页面 

复制加入购物车的成功页代码,修改<div class="m succeed-box"></div>中的内容即可。

gulimall-seckill/src/main/resources/templates/success.html

<div class="m succeed-box">
    <div th:if="${orderSn != null}" class="mc success-cont">
        <h1>恭喜,秒杀成功,订单号:[[${orderSn}]]</h1>
        <h2>正在准备订单数据,10s以后自动跳转支付 <a style="color: red" th:href="${'http://order.gulimall.com/payOrder?orderSn='+orderSn}">去支付</a></h2>
    </div>
    <div th:if="${orderSn == null}">
        <h1>手气不好,秒杀失败,下次再来</h1>
    </div>
</div>

3.4.9 秒杀接口

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/controller/SeckillController.java

@Controller
public class SeckillController {
    @Resource
    private SeckillService seckillService;

    ...

    /**
     * 秒杀:立即抢购
     * @param killId 场次id_skuId
     * @param key 商品随机码
     * @param num 秒杀数量
     * @param model
     * @return
     */
    @GetMapping("/kill")
    public String kill(String killId, String key, Integer num, Model model){
        String orderSn = seckillService.kill(killId,key,num);
        model.addAttribute("orderSn",orderSn);
        return "success";
    }

}

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/SeckillService.java

public interface SeckillService {
    ...

    /**
     * 秒杀
     * @param killId
     * @param key
     * @param num
     * @return
     */
    String kill(String killId, String key, Integer num);
}

gulimall-seckill/src/main/java/com/wen/gulimall/seckill/service/impl/SeckillServiceImpl.java

@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
    @Resource
    private CouponFeignService couponFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private RabbitTemplate rabbitTemplate;

    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
    private final String SKUKILL_CACHE_PREFIX = "seckill:sku:";
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; // + 商品随机码
    
    ...

    // TODO 上架秒杀商品的时候,每个数据都有过期时间
    // TODO 秒杀后续流程,简化了收货地址等信息
    // TODO 上架秒杀商品锁定相关库存,秒杀结束未秒杀完的库存恢复
    @Override
    public String kill(String killId, String key, Integer num) {
        long l1 = System.currentTimeMillis();
        // 获取当前登录用户信息
        MemberRespVo memberVo = LoginUserInterceptor.loginUser.get();
        // 获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if (StrUtil.isBlank(json)) {
            return null;
        } else {
            SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
            // 校验合法性
            // 1. 校验时间的合法性 上线可以给数据过期时间
            long currentTime = System.currentTimeMillis();
            Long startTime = seckillSkuRedisTo.getStartTime();
            Long endTime = seckillSkuRedisTo.getEndTime();
            if (currentTime >= startTime && currentTime <= endTime) {
                //2. 校验随机码和商品id
                String randomCode = seckillSkuRedisTo.getRandomCode();
                String skuId = seckillSkuRedisTo.getPromotionSessionId() + "_" + seckillSkuRedisTo.getSkuId();
                if (randomCode.equals(key) && killId.equals(skuId)) {
                    // 3. 验证购买数量是否合理
                    if (num <= seckillSkuRedisTo.getSeckillLimit()) {
                        // 4. 验证这个人是否已经购买过。幂等性;如果秒杀成功,就去redis占位。userId_sessionId_skuId
                        // SETNX 占位,没有才占位 原子性操作
                        String redisKey = memberVo.getId() + "_" + skuId;
                        long ttl = endTime - currentTime;
                        // 自动过期
                        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            // 占位成功,说明从来没有买过,分布式锁(获取信号量-1)
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            //boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                            boolean b = semaphore.tryAcquire(num);
                            if (b) {
                                // 秒杀成功;
                                // 快速下单。发送MQ消息 10ms
                                String timeId = IdWorker.getTimeId();
                                SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
                                seckillOrderTo.setOrderSn(timeId);
                                seckillOrderTo.setMemberId(memberVo.getId());
                                seckillOrderTo.setPromotionSessionId(seckillSkuRedisTo.getPromotionSessionId());
                                seckillOrderTo.setSkuId(seckillSkuRedisTo.getSkuId());
                                seckillOrderTo.setSeckillPrice(seckillSkuRedisTo.getSeckillPrice());
                                seckillOrderTo.setNum(num);
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", seckillOrderTo);
                                long l2 = System.currentTimeMillis();
                                log.info("秒杀接口耗时......"+(l2-l1));
                                return timeId;
                            }
                        }
                    }
                }
            }
        }
        long l3 = System.currentTimeMillis();
        log.info("秒杀接口耗时......"+(l3-l1));
        return null;
    }

    ...
}

秒杀消息内容实体

gulimall-common/src/main/java/com/wen/common/to/mq/SeckillOrderTo.java

@Data
public class SeckillOrderTo {
    private String orderSn; // 订单号
    private Long promotionSessionId; // 场次id
    private Long skuId; // 商品id
    private BigDecimal seckillPrice; // 秒杀价格
    private Integer num; // 购买数量
    private Long memberId; // 会员id
}
3.4.9.1 (幂等性)限制同一用户重复秒杀

使用SETNX占位,没有才占位,以用户id_场次id_skuId为key,value为秒杀数量。

// 4. 验证这个人是否已经购买过。幂等性;如果秒杀成功,就去redis占位。userId_sessionId_skuId
// SETNX 占位,没有才占位 原子性操作
String redisKey = memberVo.getId() + "_" + skuId;
long ttl = endTime - currentTime;
// 自动过期
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
    // 占位成功,说明从来没有买过,分布式锁(获取信号量-1)
    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
    //boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
    boolean b = semaphore.tryAcquire(num);

立即抢购,秒杀测试结果,如下:

刷新浏览器,模拟重复秒杀,结果如下:

3.4.10 秒杀消息监听消费

秒杀下单监听

gulimall-order/src/main/java/com/wen/gulimall/order/listener/OrderSeckillListener.java

@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
    @Resource
    private OrderService orderService;
    /**
     * 监听秒杀消息
     * @param message
     * @param channel
     * @param seckillOrderTo
     * @throws IOException
     */
    @RabbitHandler
    public void listen(SeckillOrderTo seckillOrderTo, Message message, Channel channel) throws IOException {
        log.info("准备创建秒杀单...");
        try {
            // 确认收到消息
            orderService.createSeckillOrder(seckillOrderTo);
            // 手动调用支付宝收单
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (IOException e) {
            // 重回队列
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

创建秒杀订单

gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java

public interface OrderService extends IService<OrderEntity> {

    ...

    /**
     * 创建秒杀订单
     * @param seckillOrderTo
     */
    void createSeckillOrder(SeckillOrderTo seckillOrderTo);
}

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    ...

        @Override
    public void createSeckillOrder(SeckillOrderTo seckillOrderTo) {
        // TODO 保存订单信息
        OrderEntity order = new OrderEntity();
        order.setOrderSn(seckillOrderTo.getOrderSn());
        order.setMemberId(seckillOrderTo.getMemberId());
        order.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        // 收货地址
        BigDecimal multiply = seckillOrderTo.getSeckillPrice().multiply(new BigDecimal("" + seckillOrderTo.getNum()));
        order.setPayAmount(multiply);
        this.save(order);

        // TODO 保存订单项信息
        OrderItemEntity orderItemEntity = new OrderItemEntity();
        orderItemEntity.setOrderSn(seckillOrderTo.getOrderSn());
        orderItemEntity.setSkuId(seckillOrderTo.getSkuId());
        orderItemEntity.setRealAmount(multiply);
        // TODO 获取当前spu的详细信息进行设置
        R spuInfoBySkuId = productFeignService.getSpuInfoBySkuId(seckillOrderTo.getSkuId());
        SpuInfoVo spuInfo = spuInfoBySkuId.getData(new TypeReference<SpuInfoVo>() {
        });
        orderItemEntity.setSpuId(spuInfo.getId());
        orderItemEntity.setSpuName(spuInfo.getSpuName());
        orderItemEntity.setSpuBrand(spuInfo.getBrandName());

        orderItemEntity.setSkuQuantity(seckillOrderTo.getNum());
        orderItemService.save(orderItemEntity);
    }

    ...
}

消费结果:

 

3.5 秒杀总结

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

3.5.1 服务单一职责+独立部署

要求:秒杀服务即使自己扛不住压力,挂掉。不要影像别的服务。

解决方案:新建秒杀服务。

3.5.2 秒杀连接加密

目的:(1)防止恶意攻击,模拟秒杀请求,1000次/s攻击。

           (2)防止链接暴露,自己工作人员,提前秒杀商品。

解决方案:这里使用商品随机码,当秒杀开始时随机码才会在商品信息中。

3.5.3 库存预热+快速扣减

秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求。

解决方案:使用定时任务将近三天需要秒杀的商品放到redis中,使用redission信号量完成秒杀库存扣减+限流。

3.5.4 动静分离

nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。使用CDN网络,分担本集群压力。

解决方案:将所有模块的静态资源放到nginx缓解集群压力。页面静态请求较多,以商品详情页为例,总共60多个请求到达后台的只有1个。

3.5.5 恶意请求拦截

识别非法攻击请求并进行拦截,网关层。

解决方案:未在网关层拦截,在秒杀模块配置登录拦截器。

3.5.6 流量错峰

使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车【每个用户速度有快有慢】,将流量分散。
解决方案:可以使用秒杀流程的第一种方案加入购物车。

3.5.7 限流+熔断+降级

前端限流+后端限流

限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩

(1)前端限流:

                        1)点击1后才能进行下次点击;

                        2)验证码设计。

(2)后端限流:

                        1)nginx限流降级:直接负载部分请求到错误的静态页面: 令牌算法 漏斗算法;

                        2)网关限流;

                        3)redission分布式型号量;

                        4)RabbitMQ限流;

                        5)熔断:当远程服务出现异常时快速中断调用并返回错误响应,方式服务级联失败。

解决方案:Sentinel

3.5.8 队列削峰

1万个商品,每个1000件秒杀。双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可。

解决方案:秒杀发送需要创建订单的MQ消息,订单服务监听秒杀队列,创建秒杀订单。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2069376.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【赵渝强老师】Docker三剑客

在Docker容器中提供了三个非常有用的工具&#xff0c;它们分别是&#xff1a;Docker Compose、Docker Machine和Docker Swarm。下面分别进行介绍。 视频讲解如下&#xff1a; Docker三剑客 【赵渝强老师】Docker的三剑客 一、容器编排工具Docker Compose 在使用Docker部署应用…

如何在 Nuxt 中动态设置页面布局

title: 如何在 Nuxt 中动态设置页面布局 date: 2024/8/24 updated: 2024/8/24 author: cmdragon excerpt: 摘要:本文介绍如何在Nuxt框架中通过设置setPageLayout函数动态调整页面布局,包括安装Nuxt、创建不同布局文件及中间件,并通过示例演示如何根据不同路径设置相应布局…

Transformer模型-1-概述、核心部件及应用场景

Transformer概述 什么是Transformer Transformer模型是由谷歌公司提出的一种基于自注意力机制的神经网络模型&#xff0c;用于处理序列数据。相比于传统的循环神经网络模型&#xff0c;Transformer模型具有更好的并行性能和更短的训练时间&#xff0c;因此在自然语言处理领域…

设计模式—工厂模式

文章目录 工厂模式1、没有使用工厂2、简单工厂模式3、工厂方法模式4、抽象工厂模式5、工厂模式小结 工厂模式 1、没有使用工厂 需求 看一个披萨的项目&#xff1a;要便于披萨种类的扩展&#xff0c;要便于维护 披萨的种类很多(比如 GreekPizz、CheesePizz 等)披萨的制作有 pr…

谷粒商城实战笔记-252~254-商城业务-消息队列-Exchange-三种type的使用

文章目录 一&#xff0c;252-商城业务-消息队列-Direct-Exchange1&#xff0c;创建4个队列2&#xff0c;exchange绑定queue3&#xff0c;发送消息 二&#xff0c;253-商城业务-消息队列-Fanout-Exchange1&#xff0c;创建一个type为fanout的exchange2&#xff0c;给这个exchang…

通过C# 读取PDF页面大小、方向、旋转角度

在处理PDF文件时&#xff0c;了解页面的大小、方向和旋转角度等信息对于PDF的显示、打印和布局设计至关重要。本文将介绍如何使用免费.NET 库通过C#来读取PDF页面的这些属性。 文章目录 C# 读取PDF页面大小&#xff08;宽度、高度&#xff09;C# 判断PDF页面方向C# 检测PDF页面…

VMWare中添加Ubuntu20.04.06镜像

一、下载Ubuntu镜像 Ubuntu20.04&#xff1a; 官方下载地址https://releases.ubuntu.com/20.04.6/ 进入官网 点击下图红框位置&#xff0c;下载镜像镜像名为ubuntu-20.04.6-desktop-amd64.iso 也可点击下面链接直接下载&#xff1a;https://releases.ubuntu.com/20.04.6/ubu…

安科瑞ACREL-7000能源管控平台在综合能耗监测系统在大型园区的应用

摘要&#xff1a;大型综合园区已经成为多种能源消耗的重要区域&#xff0c;为了探索适用于大型综合园区的综合能耗监测系统&#xff0c;建立了综合能耗监测系统整体框架&#xff0c;提出了综合能耗网络、能耗关系集合、能耗均衡度等概念&#xff0c;并以某大型综合园区为例对综…

【三维深度补全模型】PENet

【版权声明】本文为博主原创文章&#xff0c;未经博主允许严禁转载&#xff0c;我们会定期进行侵权检索。 参考书籍&#xff1a;《人工智能点云处理及深度学习算法》 本文为专栏《Python三维点云实战宝典》系列文章&#xff0c;专栏介绍地址“【python三维深度学习】python…

java结合Ai

Spring AI Spring AI提供的API支持跨人工智能提供商的 聊天,文本到图像,和嵌入模型等,同时支持同步和流API选项; 介绍 Spring AI 是 AI 工程的应用框架。其目标是将Spring生态系统的设计原则(如可移植性和模块化设计)应用于AI领域,并促进使用POJO作为应用程序的构建块…

大数据-100 Spark 集群 Spark Streaming DStream转换 黑名单过滤的三种实现方式

喜大普奔&#xff01;破百了&#xff01; 点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&a…

【精选】基于django柚子校园影院(咨询+解答+辅导)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

[240824] 微软更新导致部分 Linux 用户无法启动系统,谁之过?| Chrome 稳定版更新(128.0.6613.84)

目录 微软更新导致部分 Linux 用户无法启动系统&#xff0c;谁之过&#xff1f;Chrome 稳定版更新 (128.0.6613.84) 微软更新导致部分 Linux 用户无法启动系统&#xff0c;谁之过&#xff1f; 最近&#xff0c;微软推送的一项 Windows 更新导致部分 Linux 用户无法启动系统&am…

基于Springboot + vue + mysql 藏区特产销售平台 设计实现

目录 &#x1f4da; 前言 &#x1f4d1;摘要 1.1 研究背景 &#x1f4d1;操作流程 &#x1f4da; 系统架构设计 &#x1f4da; 数据库设计 &#x1f4ac; E-R表 系统功能模块 系统首页 特产信息 ​编辑 个人中心 购物车 用户注册 管理员功能模块 管理员登录 管…

Stable diffusion模型如何区分?通俗易懂,入门必看!

在Stable Diffusion的基础学习中&#xff0c;很多小伙伴们可能看到繁杂的大模型就蒙圈了&#xff0c;那么多的模型后缀&#xff0c;究竟代表什么呢&#xff1f;如何区分呢&#xff1f;今天就带大家来学习一下&#xff5e; 不同后缀模型介绍 在Stable diffusion中&#xff0c;…

【Tomact源码解析】——组件介绍

目录 一、简介 二、组件和体系架构简介 三、组件详情 Server Service Connector Engine ​编辑Host Context Wrapper 四、容器详情 生命周期机制 监听器机制 管道机制 五、补充内容 一、简介 Tomcat 服务器是一个免费的开放源代码的 Web 应用服务器,属于…

支持在线编辑的文件管理系统MxsDoc

DocSys是一个基于Web的文件管理系统&#xff08;全平台支持:Linux&#xff0c;Windows&#xff0c;Mac&#xff09;&#xff0c;它提供了丰富的功能和特性&#xff0c;以满足不同用户在不同场景下的需求。 开源地址&#xff1a;DocSys: MxsDoc是基于Web的文件管理系统&#xff…

校友林小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;树木管理管理&#xff0c;所属科管理&#xff0c;树木领取管理&#xff0c;树跟踪状态管理&#xff0c;用户信息统计管理&#xff0c;树木捐款管理&#xff0c;留言板管理 微信端…

【芯片往事】陈大同-展讯和TD

前言&#xff1a;几年前&#xff08;2012&#xff09;&#xff0c;应邀为校友刊物《水木清华》写了一年创业专栏&#xff0c;其中有几期回忆了当年先后创办硅谷豪威科技&#xff08;OmniVision&#xff09;和上海展讯通信&#xff08;SpreadTrum&#xff09;的经历&#xff0c;…

ZMQ发布订阅模型

案例一 发布者Publisher(server) // server.cpp #include <zmq.hpp> #include <string> #include <iostream> #include <chrono> #include <thread> using namespace std; using namespace zmq; int main() {context_t context(1);socket_t so…