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

目录

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_routeuri: lb://gulimall-couponpredicates:- Path=/api/coupon/**,/hellofilters:# 去掉 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-seckillcloud:nacos:discovery:server-addr: 172.xx.xx.10:8848redis: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: 5max-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 {@Autowiredprivate 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 {@Resourceprivate SeckillSkuRelationService seckillSkuRelationService;...@Overridepublic 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 {@Resourceprivate SeckillService seckillService;@Resourceprivate 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 {@Resourceprivate CouponFeignService couponFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate 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:"; // + 商品随机码@Overridepublic 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_routeuri: lb://gulimall-seckillpredicates:# 由以下的主机域名访问转发到会员服务- 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 {@Resourceprivate 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 {@Resourceprivate CouponFeignService couponFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate 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*/@Overridepublic 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 {@Resourceprivate 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 {@Resourceprivate CouponFeignService couponFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate 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*/@Overridepublic SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {// 1. 找到所有需要参与秒杀的商品的keyBoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);// 获取所有的keySet<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_infoprivate SkuInfoEntity info;private boolean hasStock = true;// 获取sku的图片信息 pms_sku_imagesprivate 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.10session: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<>();@Overridepublic 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 {@Resourceprivate LoginUserInterceptor loginUserInterceptor;@Overridepublic 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*/@Beanpublic Queue orderSeckillOrderQueue(){//String name, boolean durable, boolean exclusive, boolean autoDelete,//			@Nullable Map<String, Object> argumentsreturn new Queue("order.seckill.order.queue",true,false,false);}@Beanpublic Binding orderSeckillOrderQueueBinding(){//String destination, DestinationType destinationType, String exchange, String routingKey,//			@Nullable Map<String, Object> argumentsreturn 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.10port: 5672virtual-host: /# 开启发送端确认publisher-confirm-type: correlated# 开启发送端消息抵达队列的确认,默认是falsepublisher-returns: truethymeleaf:# 关闭缓存cache: false
3.4.7.3 配置RabbitMQ序列化方式 

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

@Configuration
public class MyRabbitConfig {@Beanpublic 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 {@Resourceprivate 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 {@Resourceprivate CouponFeignService couponFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;@Resourceprivate 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 上架秒杀商品锁定相关库存,秒杀结束未秒杀完的库存恢复@Overridepublic 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. 校验随机码和商品idString 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消息 10msString 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; // 场次idprivate Long skuId; // 商品idprivate 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 {@Resourceprivate OrderService orderService;/*** 监听秒杀消息* @param message* @param channel* @param seckillOrderTo* @throws IOException*/@RabbitHandlerpublic 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 {...@Overridepublic 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.rhkb.cn/news/407878.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HT97180 125mW免输出耦合电容的立体声线路驱动器1耳机放大器

特点 输出无需隔直流电容 卓越的低音效果 无咔嗒/噼噗声 低THDN:最低0.002% 低噪声&#xff0c;VN:8uV 支持单端输入和全差分输入 1.65V至4.8V较宽的电源工作范围 输出功率:125mW(fIN1kHz,VDD4.2V RL32Ω,THDN0.1%) 无铅封装, QFN16L-PP 3mm*3mm 概述 HT97180(L)是一款差分…

line-height的使用场景

line-height:字面含义为行高&#xff0c;行高有三部分组成&#xff0c;分为内容高度&#xff0c;上间距&#xff0c;下间距。 可以看到文本在div盒子中的默认位置是左上角。此时文字部分的行高只有内容高度在支撑&#xff0c;上间距和下间距都是0。鼠标在字体上滑动时的蓝色部…

超融合/分布式 IT 架构有哪些常见故障类型?如何针对性解决和预防?

本文刊于《中国金融电脑》2024 年第 7 期。 作者&#xff1a;SmartX 金融团队 以超融合为代表的分布式 IT 基础架构凭借其高性能、高可靠和灵活的扩展能力&#xff0c;在满足大规模、高并发、低延迟业务需求等方面展现出显著优势&#xff0c;成为众多金融机构构建 IT 基础设施…

初识模版(C++)

初识模版&#xff08;C&#xff09; 模版是C的一个重大发明&#xff0c;是让C突飞猛进的原因之一。 泛型编程 实现一个通用的交换函数&#xff1f; void Swap(int& left, int& right) {int temp left;left right;right temp; }void Swap(double& left, doubl…

DockerHub解决镜像拉取之困

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

从零开始搭建Aliyun ESC高可用集群 (HaVip+KeepAlived)

从零开始搭建Aliyun ESC高可用集群 (HaVip+KeepAlived) 架构 架构 本设计方案采用两台阿里云ECS服务器搭建Keepalived结合LVS的高可用集群。使用LVS的TUN模式进行负载均衡,同时利用阿里云的弹性IP(EIP)与高可用虚拟HaVIP实现跨服务器的高可用性。架构中,一台ECS服务器作为…

DFS 算法:记忆化搜索

我的个人主页 {\large \mathsf{{\color{Red} 我的个人主页} } } 我的个人主页 往 {\color{Red} {\Huge 往} } 往 期 {\color{Green} {\Huge 期} } 期 文 {\color{Blue} {\Huge 文} } 文 章 {\color{Orange} {\Huge 章}} 章 无 此系列更新频繁&#xff0c;求各位读者点赞 关…

备考计算机二级Python之Day5

第5章 函数和代码 一、函数的基本使用 函数是一段具有特定功能的、可重用的语句组&#xff0c;通过函数名来表示和调用。 函数的使用包括两部分&#xff1a;函数的定义和函数的使用 1、函数的定义 Python语言通过保留字def定义函数&#xff0c;语法形式如下&#xff1a; …

SpringBoot教程(二十四) | SpringBoot集成AOP实现日志记录

SpringBoot教程&#xff08;二十四&#xff09; | SpringBoot集成AOP实现日志记录 &#xff08;一&#xff09;AOP 概要1. 什么是 AOP &#xff1f;2. 为什么要用 AOP&#xff1f;3. AOP一般用来干什么&#xff1f;4. AOP 的核心概念 &#xff08;二&#xff09;Spring AOP1. 简…

CSS3页面布局-三栏-中栏流动布局

三栏-中栏流动布局 用负外边距实现 实现三栏布局且中栏内容区不固定的核心问题就是处理右栏的定位&#xff0c; 并在中栏内容区大小改变时控制右栏与布局的关系。 控制两个外包装容器的外边距&#xff0c;一个包围三栏&#xff0c;一个包围左栏和中栏。 <!DOCTYPE html&…

vllm 部署GLM4模型进行 Zero-Shot 文本分类实验,让大模型给出分类原因,准确率可提高6%

文章目录 简介数据集实验设置数据集转换模型推理评估 简介 本文记录了使用 vllm 部署 GLM4-9B-Chat 模型进行 Zero-Shot 文本分类的实验过程与结果。通过对 AG_News 数据集的测试&#xff0c;研究发现大模型在直接进行分类时的准确率为 77%。然而&#xff0c;让模型给出分类原…

【软件测试面试题】WEB功能测试(持续更新)

Hi&#xff0c;大家好&#xff0c;我是小码哥。最近很多朋友都在说今年的互联网行情不好&#xff0c;面试很难&#xff0c;不知道怎么复习&#xff0c;我最近总结了一份在软件测试面试中比较常见的WEB功能测试面试面试题合集&#xff0c;希望对大家有帮助。 建议点赞收藏再阅读…

AI学习记录 - 怎么理解 torch 的 nn.Conv2d

有用就点个赞 怎么理解 nn.Conv2d 参数 conv_layer nn.Conv2d(in_channels1, out_channels 10 // 2, kernel_size3, stride2, padding0, biasFalse) in_channels in_channels 可以设置成1&#xff0c;2&#xff0c;3&#xff0c;4等等都可以&#xff0c;一般来说做图像识别…

微服务案例搭建

目录 一、案例搭建 1.数据库表 2.服务模块 二、具体代码实现如下&#xff1a; (1) 首先是大体框架为&#xff1a; &#xff08;2&#xff09;父模块中的pom文件配置 &#xff08;3&#xff09;shop_common模块&#xff0c;这个模块里面只需要配置pom.xml&#xff0c;与实体…

MySQL如何判断一个字段里面是否包含汉字

SQL查询中&#xff0c;length() 和 char_length() 都是用来获取字符串长度的函数 在单字节字符集下&#xff08;如ASCII&#xff09;&#xff1a;每个字符通常占用1个字节&#xff0c;因此length()和char_length()在这类字符集中给出的结果是一样 在多字节字符集下&#xff0…

matplotlib绘制子图以及局部放大效果

需求&#xff1a;绘制1*2的子图&#xff0c;子图1显示两个三角函数&#xff0c;子图2显示三个对数函数&#xff0c;子图2中对指定的区域进行放大。 绘图细节&#xff1a; 每个子图中每个函数的数据存放到一个列表中&#xff0c;然后将每个子图的数据统一存到一个列表中&#…

Go 使用Redis安装、实例和基本操作

Go使用Redis&#xff1a;详解go-redis/v9库 引言 Redis作为一个高性能的键值对数据库&#xff0c;广泛应用于缓存、消息队列、实时数据分析等场景。在Go语言中&#xff0c;go-redis/v9库提供了丰富的接口和高效的数据交互能力&#xff0c;使得在Go项目中集成Redis变得简单而高…

接口限流经典算法

文章目录 限流基于计数器的限流基于滑动窗口的限流桶漏斗算法令牌桶算法 限流 为了保证系统的安全性和稳定性&#xff0c;防止恶意流量和突发大量流量短时间内大量请求接口&#xff0c;造成服务器崩溃&#xff0c;接口的限流是有必要的。 以下是四种经典的限流算法。 基于计数…

Python测试框架Pytest的使用

pytest基础功能 pytset功能及使用示例1.assert断言2.参数化3.运行参数4.生成测试报告5.获取帮助6.控制用例的执行7.多进程运行用例8.通过标记表达式执行用例9.重新运行失败的用例10.setup和teardown函数 pytset功能及使用示例 1.assert断言 借助python的运算符号和关键字实现不…

UE5打包iOS运行查看Crash日志

1、查看Crash 1、通过xCode打开设备 2、选择APP打开最近的日志 3、选择崩溃时间点对应的日志 4、选择对应的工程打开 5、就能看到对应的Crash日志 2、为了防止Crash写代码需要注意 1、UObject在RemoveFromRoot之前先判断是否Root if (SelectedImage && Selecte…