未配置秒杀服务相关网关
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
promotion_session_id代表场次id
解决方案:按照场次查询上架商品,修改关联商品接口查询逻辑
gulimall-coupon/src/main/java/site/zhourui/gulimall/coupon/service/impl/SeckillSkuRelationServiceImpl.java
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
String promotionSessionId = (String) params.get("promotionSessionId");
// 2、封装活动场次id,关联
if (!StringUtils.isEmpty(promotionSessionId)) {
queryWrapper.eq("promotion_session_id", promotionSessionId);
}
IPage<SeckillSkuRelationEntity> page = this.page(
new Query<SeckillSkuRelationEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
此时重启优惠服务,场次二不能查询到场次一的上架商品,场次一只能查询到自己的上架商品
在线Cron表达式生成器 (qqe2.com)
语法:秒 分 时 日 月 周 年 (spring 不支持年,所以可以不写)
quartz表达式格式介绍
Cron Trigger Tutorial (quartz-scheduler.org)
A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:
Field Name | Mandatory | Allowed Values | Allowed Special Characters |
---|---|---|---|
Seconds | YES | 0-59 | , - * / |
Minutes | YES | 0-59 | , - * / |
Hours | YES | 0-23 | , - * / |
Day of month | YES | 1-31 | , - * ? / L W |
Month | YES | 1-12 or JAN-DEC | , - * / |
Day of week | YES | 1-7 or SUN-SAT | , - * ? / L # |
Year | NO | empty, 1970-2099 | , - * / |
So cron expressions can be as simple as this: * * * * ? *
or more complex, like this: 0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010
,:枚举;
(cron="7,9,23****?"):任意时刻的7,9,23秒启动这个任务;
-:范围:
(cron="7-20****?""):任意时刻的7-20秒之间,每秒启动一次
*:任意;
指定位置的任意时刻都可以
/:步长;
(cron="7/5****?"):第7秒启动,每5秒一次;
(cron="*/5****?"):任意秒启动,每5秒一次;
? :(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使用?
(cron="***1*?"):每月的1号,而且必须是周二然后启动这个任务;
L:(出现在日和周的位置)”,
last:最后一个
(cron="***?*3L"):每月的最后一个周二
W:Work Day:工作日
(cron="***W*?"):每个月的工作日触发
(cron="***LW*?"):每个月的最后一个工作日触发
#:第几个
(cron="***?*5#2"):每个月的 第2个周4
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 |
每个月的最后一个星期五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触发 |
pom
gulimall-seckill/pom.xml
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.3.RELEASEversion>
<relativePath/>
parent>
<groupId>com.zhourui.gulimallgroupId>
<artifactId>gulimall-seckillartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gulimall-seckillname>
<description>秒杀description>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR6spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>com.zhourui.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
exclusion>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
配置秒杀模块
gulimall-seckill/src/main/resources/application.yml
server:
port: 25000
spring:
application:
name: gulimall-seckill
cloud:
nacos:
discovery:
server-addr: localhost:8848
redis:
host: 192.168.157.128
port: 6379
主启动类上启动注册发现与feign调用
@EnableFeignClients
@EnableDiscoveryClient
在类上标注注解开启定时任务功能
@Component
@EnableScheduling
在需要开启定时任务的方法标注注解,为该方法开启定时任务,并填上cron表达式
@Scheduled(cron = "* * * * * ? ")
整合示例
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/scheduled/HelloScheduled.java
package site.zhourui.gulimall.seckill.scheduled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author zr
* @date 2022/1/6 14:29
*/
@Slf4j
@Component
@EnableScheduling
public class HelloScheduled {
@Scheduled(cron = "* * * * * ? ")
public void hello(){
log.info("hello world");
}
}
测试结果,每秒执行一次
1、在Spring中表达式是6位组成,不允许第七位的年份
2、在周几的的位置,1-7代表周一到周日
3、定时任务不该阻塞。默认是阻塞的
定时任务不该阻塞。默认是阻塞的
示范
模拟业务处理时间较长,发现日志打印时间间隔为4秒,说明定时任务是阻塞的
在类上标注注解开启异步任务功能
@EnableAsync
在需要开启异步任务的方法标注注解,为该方法开启异步任务
@Async
日志打印的时间间隔为1秒,任务没有阻塞了
spring:
task:
execution:
pool:
core-size: 8 #默认大小为8
max-size: 50 #默认最大为int
//可以让业务以异步的方式,自己提交到线程池
CompletableFuture.runAsync(() -> {
},execute);
定时任务开启后其实也是有线程池的,通过更改配置修改线程池大小
spring:
task:
scheduling:
pool:
size: 2 #默认为1,就会阻塞
/**
* 当前时间
* @return
*/
private String startTime() {
LocalDate now = LocalDate.now();
LocalTime min = LocalTime.MIN;
LocalDateTime start = LocalDateTime.of(now, min);
//格式化时间
String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return startFormat;
}
/**
* 结束时间
* @return
*/
private String endTime() {
LocalDate now = LocalDate.now();
LocalDate plus = now.plusDays(2);
LocalTime max = LocalTime.MAX;
LocalDateTime end = LocalDateTime.of(plus, max);
//格式化时间
String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return endFormat;
}
测试效果
gulimall-coupon/src/main/java/site/zhourui/gulimall/coupon/controller/SeckillSessionController.java
/**
* 查询最近三天需要参加秒杀商品的信息
* @return
*/
@GetMapping(value = "/Lates3DaySession")
public R getLates3DaySession() {
List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLates3DaySession();
return R.ok().setData(seckillSessionEntities);
}
gulimall-coupon/src/main/java/site/zhourui/gulimall/coupon/service/impl/SeckillSessionServiceImpl.java
@Override
public List<SeckillSessionEntity> getLates3DaySession() {
//计算最近三天
//查出这三天参与秒杀活动的商品
List<SeckillSessionEntity> list = this.baseMapper.selectList(new QueryWrapper<SeckillSessionEntity>()
.between("start_time", startTime(), endTime()));
if (list != null && list.size() > 0) {
List<SeckillSessionEntity> collect = list.stream().map(session -> {
Long id = session.getId();
//查出sms_seckill_sku_relation表中关联的skuId
List<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>()
.eq("promotion_session_id", id));
session.setRelationSkus(relationSkus);
return session;
}).collect(Collectors.toList());
return collect;
}
return null;
}
/**
* 当前时间
* @return
*/
private String startTime() {
LocalDate now = LocalDate.now();
LocalTime min = LocalTime.MIN;
LocalDateTime start = LocalDateTime.of(now, min);
//格式化时间
String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return startFormat;
}
/**
* 结束时间
* @return
*/
private String endTime() {
LocalDate now = LocalDate.now();
LocalDate plus = now.plusDays(2);
LocalTime max = LocalTime.MAX;
LocalDateTime end = LocalDateTime.of(plus, max);
//格式化时间
String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return endFormat;
}
feign接口
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/feign/CouponFeignService.java
package site.zhourui.gulimall.seckill.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import site.zhourui.common.utils.R;
/**
* @author zr
* @date 2022/1/6 17:34
*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
/**
* 查询最近三天需要参加秒杀商品的信息
* @return
*/
@GetMapping(value = "/coupon/seckillsession/Lates3DaySession")
R getLates3DaySession();
}
配置定时任务
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/config/ScheduledConfig.java
package site.zhourui.gulimall.seckill.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author zr
* @date 2022/1/8 16:11
*/
@EnableAsync
@EnableScheduling
@Configuration
public class ScheduledConfig {
}
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/scheduled/SeckillScheduled.java
package site.zhourui.gulimall.seckill.scheduled;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.seckill.service.SeckillService;
import java.util.concurrent.TimeUnit;
/**
* @author zr
* @date 2022/1/8 16:08
*/
/**
* 秒杀商品定时上架
* 每天晚上3点,上架最近三天需要三天秒杀的商品
* 当天00:00:00 - 23:59:59
* 明天00:00:00 - 23:59:59
* 后天00:00:00 - 23:59:59
*/
@Slf4j
@Service
public class SeckillScheduled {
@Autowired
private SeckillService seckillService;
@Autowired
private RedissonClient redissonClient;
//秒杀商品上架功能的锁
private final String upload_lock = "seckill:upload:lock";
//TODO 保证幂等性问题
@Scheduled(cron = "*/5 * * * * ? ")
// @Scheduled(cron = "0 0 3 * * ? ")
public void uploadSeckillSkuLatest3Days() {
//1、重复上架无需处理
log.info("上架秒杀的商品...");
//分布式锁
RLock lock = redissonClient.getLock(upload_lock);
try {
//加锁
lock.lock(10, TimeUnit.SECONDS);
seckillService.uploadSeckillSkuLatest3Days();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
上架三天需要秒杀的商品
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/SeckillService.java
package site.zhourui.gulimall.seckill.service;
/**
* @author zr
* @date 2022/1/6 18:14
*/
public interface SeckillService {
/**
* 上架三天需要秒杀的商品
*/
void uploadSeckillSkuLatest3Days();
}
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java
package site.zhourui.gulimall.seckill.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.seckill.feign.CouponFeignService;
import site.zhourui.gulimall.seckill.feign.ProductFeignService;
import site.zhourui.gulimall.seckill.service.SeckillService;
import site.zhourui.gulimall.seckill.to.SeckillSkuRedisTo;
import site.zhourui.gulimall.seckill.vo.SeckillSessionWithSkusVo;
import site.zhourui.gulimall.seckill.vo.SkuInfoVo;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author zr
* @date 2022/1/6 18:14
*/
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
private ProductFeignService productFeignService;
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
private final String SECKILL_CHARE_PREFIX = "seckill:skus";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码
@Override
public void uploadSeckillSkuLatest3Days() {
//1、扫描最近三天的商品需要参加秒杀的活动
R lates3DaySession = couponFeignService.getLates3DaySession();
if (lates3DaySession.getCode() == 0) {
//上架商品
List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
});
//缓存到Redis
//1、缓存活动信息
saveSessionInfos(sessionData);
//2、缓存活动的关联商品信息
saveSessionSkuInfo(sessionData);
}
}
/**
* 缓存秒杀活动信息
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
if (!CollectionUtils.isEmpty(sessions))
sessions.stream().forEach(session -> {
//获取当前活动的开始和结束时间的时间戳
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
//存入到Redis中的key
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
//判断Redis中是否有该信息,如果没有才进行添加
Boolean hasKey = redisTemplate.hasKey(key);
//缓存活动信息
if (!hasKey) {
//获取到活动中所有商品的skuId
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,skuIds);
}
});
}
/**
* 缓存秒杀活动所关联的商品信息
*/
private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
if (!CollectionUtils.isEmpty(sessions))
sessions.stream().forEach(session -> {
//准备hash操作,绑定hash
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//生成随机码
String token = UUID.randomUUID().toString().replace("-", "");
String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
if (!operations.hasKey(redisKey)) {
//缓存我们商品信息
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
Long skuId = seckillSkuVo.getSkuId();
//1、先查询sku的基本信息,调用远程服务
R info = productFeignService.getSkuInfo(skuId);
if (info.getCode() == 0) {
SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
redisTo.setSkuInfo(skuInfo);
}
//2、sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo,redisTo);
//3、设置当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
//4、设置商品的随机码(防止恶意攻击)
redisTo.setRandomCode(token);
//序列化json格式存入Redis中
String seckillValue = JSON.toJSONString(redisTo);
operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);
//如果当前这个场次的商品库存信息已经上架就不需要上架
//5、使用库存作为分布式Redisson信号量(限流)
// 使用库存作为分布式信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
});
});
}
}
分布式情况下,定时任务会启动多次【因为场次信息在redis中是List类型,会重复添加】
解决方案,加分布式锁
如果上架之前没有对本次上架的商品验证是否上架,那么就会重复上架
解决方案
在商品上架时检查已上架商品是否与本次上架的商品的场次信息与商品信息重复
<script type="text/javascript">
function search() {
var keyword=$("#searchText").val()
window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;
}
$.get("http://seckill.gulimall.com/getCurrentSeckillSkus", function (res) {
if (res.data.length > 0) {
res.data.forEach(function (item) {
$(" + item.skuId + ")'> ").append($(""))
.append($(""
+item.skuInfo.skuTitle+""))
.append($("" + item.seckillPrice + ""))
.append($("" + item.skuInfo.price + ""))
.appendTo("#seckillSkuContent");
})
}
})
function toDetail(skuId) {
location.href = "http://item.gulimall.com/" + skuId + ".html";
}
</script>
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/controller/SeckillController.java
package site.zhourui.gulimall.seckill.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.seckill.service.SeckillService;
import site.zhourui.gulimall.seckill.to.SeckillSkuRedisTo;
import java.util.List;
/**
* @author zr
* @date 2022/1/8 18:07
*/
@Controller
public class SeckillController {
@Autowired
private SeckillService seckillService;
/**
* 当前时间可以参与秒杀的商品信息
*/
@GetMapping(value = "/getCurrentSeckillSkus")
@ResponseBody
public R getCurrentSeckillSkus() {
//获取到当前可以参加秒杀商品的信息
List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
}
获取到当前可以参加秒杀商品的信息
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/SeckillService.java
List<SeckillSkuRedisTo> getCurrentSeckillSkus();
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java
/**
* 获取到当前可以参加秒杀商品的信息
* @return
*/
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
//1、确定当前属于哪个秒杀场次
long currentTime = System.currentTimeMillis();
//从Redis中查询到所有key以seckill:sessions开头的所有数据
Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
for (String key : keys) {
//seckill:sessions:1594396764000_1594453242000
String replace = key.replace(SESSION_CACHE_PREFIX, "");
String[] s = replace.split("_");
//获取存入Redis商品的开始时间
long startTime = Long.parseLong(s[0]);
//获取存入Redis商品的结束时间
long endTime = Long.parseLong(s[1]);
//判断是否是当前秒杀场次
if (currentTime >= startTime && currentTime <= endTime) {
//2、获取这个秒杀场次需要的所有商品信息
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
assert range != null;
List<String> listValue = hasOps.multiGet(range);
if (listValue != null && listValue.size() >= 0) {
List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {
String items = (String) item;
SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);
// redisTo.setRandomCode(null);当前秒杀开始需要随机码
return redisTo;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
return null;
}
测试效果
https://gitee.com/zhourui815/gulimall/blob/master/gulimall-product/src/main/resources/templates/item.html
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/controller/SeckillController.java
/**
* 根据skuId查询商品是否参加秒杀活动
*/
@GetMapping(value = "/sku/seckill/{skuId}")
@ResponseBody
public R getSkuSeckilInfo(@PathVariable("skuId") Long skuId) {
SeckillSkuRedisTo to = seckillService.getSkuSeckilInfo(skuId);
return R.ok().setData(to);
}
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/SeckillService.java
/**
* 根据skuId查询商品是否参加秒杀活动
* @param skuId
* @return
*/
SeckillSkuRedisTo getSkuSeckilInfo(Long skuId);
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java
/**
* 根据skuId查询商品是否参加秒杀活动
* @param skuId
* @return
*/
@Override
public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
//1、找到所有需要秒杀的商品的key信息---seckill:skus
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
//拿到所有的key
Set<String> keys = hashOps.keys();
if (keys != null && keys.size() > 0) {
//4-45 正则表达式进行匹配
String reg = "\\d-" + skuId;
for (String key : keys) {
//如果匹配上了
if (Pattern.matches(reg,key)) {
//从Redis中取出数据来
String redisValue = hashOps.get(key);
//进行序列化
SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);
//随机码
Long currentTime = System.currentTimeMillis();
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
//如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间
if (currentTime >= startTime && currentTime <= endTime) {
return redisTo;
}
redisTo.setRandomCode(null);
return redisTo;
}
}
}
return null;
}
gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/SkuInfoServiceImpl.java
// 3、查询当前sku是否参与秒杀活动
CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
//3、远程调用查询当前sku是否参与秒杀优惠活动
R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
if (skuSeckilInfo.getCode() == 0) {
//查询成功
SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
});
skuItemVo.setSeckillSkuVo(seckilInfoData);
if (seckilInfoData != null) {
long currentTime = System.currentTimeMillis();
if (currentTime > seckilInfoData.getEndTime()) {
skuItemVo.setSeckillSkuVo(null);
}
}
}
}, executor);
//等到所有任务都完成(注意添加seckillFuture)
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();
重启测试
清除redis再次测试
优点:加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好
缺点:秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单
优点:从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息
缺点:如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款
解决方案:不使用订单服务处理秒杀消息,需要一套独立的业务来处理
gulimall-order/src/main/java/com/atguigu/gulimall/order/config/MyRabbitMQConfig.java
/**
* 商品秒杀队列
* 作用:削峰,创建订单
*/
@Bean
public Queue orderSecKillOrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
导入依赖
rabbitmy用于发送消息
thymeleaf用于展示秒杀成功页面
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
配置
server:
port: 25000
spring:
rabbitmq:
host: 192.168.157.128
port: 5672
virtual-host: /
#开发环境关闭缓存
thymeleaf:
cache: false
配置rabbitmq系列化方式
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/config/MyRabbitMQConfig.java
package site.zhourui.gulimall.seckill.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zr
* @date 2022/1/10 15:27
*/
@Configuration
public class MyRabbitMQConfig {
/**
* 以json序列化的格式发送消息
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
页面链接
https://gitee.com/zhourui815/gulimall/blob/master/gulimall-seckill/src/main/resources/templates/success.html
配置网关
gulimall-gateway/src/main/resources/application.yml
- id: gulimall_seckill_route
uri: lb://gulimall-seckill
predicates:
- Host=seckill.gulimall.com
配置host
# gulimall
192.168.157.128 gulimall.com
# search
192.168.157.128 search.gulimall.com
# item 商品详情
192.168.157.128 item.gulimall.com
#商城认证
192.168.157.128 auth.gulimall.com
#购物车
192.168.157.128 cart.gulimall.com
#订单
192.168.157.128 order.gulimall.com
#会员
192.168.157.128 member.gulimall.com
#秒杀
192.168.157.128 seckill.gulimall.com
#单点登录
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/interceptor/LoginUserInterceptor.java
package site.zhourui.gulimall.seckill.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.vo.MemberResponseVo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;
/**
* @author zr
* @date 2022/1/10 15:13
*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/kill", uri);
// 只有秒杀需要拦截,其他直接放行
if (match) {
HttpSession session = request.getSession();
//获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("");
// session.setAttribute("msg", "请先进行登录");
// response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
return true;
}
}
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/config/SeckillWebConfig.java
package site.zhourui.gulimall.seckill.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.seckill.interceptor.LoginUserInterceptor;
/**
* @author zr
* @date 2022/1/10 15:12
*/
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/controller/SeckillController.java
/**
* 商品进行秒杀(秒杀开始)
* 查看表 oms_order_item
*/
@GetMapping(value = "/kill")
public String seckill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num,
Model model) {
String orderSn = null;
try {
//1、判断是否登录
orderSn = seckillService.kill(killId,key,num);
model.addAttribute("orderSn",orderSn);
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
当前商品进行秒杀(各种校验,成功后发送消息)
gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java
/**
* 当前商品进行秒杀(秒杀开始)
* @param killId
* @param key
* @param num
* @return
*/
@Override
public String kill(String killId, String key, Integer num) throws InterruptedException {
long s1 = System.currentTimeMillis();
//获取当前用户的信息
MemberResponseVo user = LoginUserInterceptor.loginUser.get();
//1、获取当前秒杀商品的详细信息从Redis中获取
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
String skuInfoValue = hashOps.get(killId);
if (StringUtils.isEmpty(skuInfoValue)) {
return null;
}
//(合法性效验)
SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long currentTime = System.currentTimeMillis();
//判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
if (currentTime >= startTime && currentTime <= endTime) {
//2、效验随机码和商品id
String randomCode = redisTo.getRandomCode();
String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
//3、验证购物数量是否合理和库存量是否充足
Integer seckillLimit = redisTo.getSeckillLimit();
//获取信号量
String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
Integer count = Integer.valueOf(seckillCount);
//判断信号量是否大于0,并且买的数量不能超过库存
if (count > 0 && num <= seckillLimit && count > num ) {
//4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
//SETNX 原子性处理
String redisKey = user.getId() + "-" + skuId;
//设置自动过期(活动结束时间-当前时间)
Long ttl = endTime - currentTime;
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功说明从来没有买过,分布式锁(获取信号量-1)【分布式锁-1】
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//TODO 秒杀成功,快速下单
boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
//保证Redis中还有商品库存
if (semaphoreCount) {
//创建订单号和订单信息发送给MQ
// 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(user.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSkuId(redisTo.getSkuId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
long s2 = System.currentTimeMillis();
log.info("耗时..." + (s2 - s1));
return timeId;
}
}
}
}
}
long s3 = System.currentTimeMillis();
log.info("耗时..." + (s3 - s1));
return null;
}
//4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
//SETNX 原子性处理
String redisKey = user.getId() + "-" + skuId;
//设置自动过期(活动结束时间-当前时间)
Long ttl = endTime - currentTime;
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功说明从来没有买过,分布式锁(获取信号量-1)【分布式锁-1】
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//TODO 秒杀成功,快速下单tryAcquire尝试扣减信号量,超时也会失败
boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
}
测试结果
gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderSeckillListener.java
package site.zhourui.gulimall.order.listener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import site.zhourui.common.to.mq.SeckillOrderTo;
import site.zhourui.gulimall.order.service.OrderService;
import java.io.IOException;
/**
* @author zr
* @date 2022/1/10 15:52
*/
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
log.info("准备创建秒杀单的详细信息...");
try {
orderService.createSeckillOrder(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
创建秒杀订单
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
/**
* 创建秒杀单
* @param orderTo
*/
void createSeckillOrder(SeckillOrderTo orderTo);
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 创建秒杀单
* @param orderTo
*/
@Override
public void createSeckillOrder(SeckillOrderTo orderTo) {
//TODO 保存订单信息
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderTo.getOrderSn());
orderEntity.setMemberId(orderTo.getMemberId());
orderEntity.setCreateTime(new Date());
BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
orderEntity.setPayAmount(totalPrice);
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
//保存订单
this.save(orderEntity);
//保存订单项信息
OrderItemEntity orderItem = new OrderItemEntity();
orderItem.setOrderSn(orderTo.getOrderSn());
orderItem.setRealAmount(totalPrice);
orderItem.setSkuQuantity(orderTo.getNum());
//保存商品的spu信息
R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());
SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
});
orderItem.setSpuId(spuInfoData.getId());
orderItem.setSpuName(spuInfoData.getSpuName());
orderItem.setSpuBrand(spuInfoData.getBrandName());
orderItem.setCategoryId(spuInfoData.getCatalogId());
//保存订单项数据
orderItemService.save(orderItem);
}
测试
秒杀服务即使自己扛不住压力,挂掉。不要影响别人
解决:新增秒杀服务
防止恶意攻击,模拟秒杀请求,1000次ls攻击。
防止链接暴露.自己工作人员,提前秒杀商品。
解决:请求需要随机码,在秒杀开始时随机码才会放在商品信息中
秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求
解决:库存放入redis中,使用分布式信号量扣减+限流
nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。
使用CDN网络,分担本集群压力
解决:nginx
例:10万个人来访问商品详情页,这个详情页会发送63个请求,但是只有3个请求到达后端,60个请求是前端的。一共30万请求到达后端,600万个请求到达nginx或cdn
识别非法攻击请求并进行拦截,网关层拦截,放行到后太服务的请求都是正常请求
在网关层拦截:一些不带令牌的请求循环发送
解决:使用网关拦截
本系统做了登录拦截器【在各微服务创建的,未登录跳转登录页面】
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车
1、输入验证码需要时间,将流量错开了【速度有快有慢】
2、加入购物车,然后再结算【速度有快有慢】
解决:使用购物车逻辑
前踹限流+后端限流
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
前端限流:1、每次点击1s后才能再次点击
2、验证登录
后端限流:1、网关限流,例如访问秒杀的流量到达10W等2S再将请求传过去【其中10W是集群的峰值】
2、就算是合理的10次也只放行1-2次
3、熔断:远程访问失败,快速返回,并且下次不要再请求这个节点【防止请求长时间等待】
4、降级:请求量太大了,直接将请求转发到一个错误页面
出现一种情况:集群的处理能力是10W,网关放行了10W的请求,但此时秒杀服务掉线了2台,处理能力下降导致请求堆积,最后资源耗尽服务器全崩了
解决:spring alibaba sentinel
以前是Hystrix,现在不更新了就不用了
100万个商品,每个商品的秒杀库存是100,会产生1亿的流量到后台,全部放入队列中,然后订单监听队列一个个创建订单扣减库存
解决:秒杀服务将创建订单的请求存入mq,订单服务监听mq。
优点:要崩只会崩秒杀服务,不会打垮其他服务【商品服务、订单服务、购物车服务】【第一套实现逻辑会导致这些问题】【看秒杀请求的两种实现】