商城业务-商品秒杀服务
一、秒杀介绍
1、秒杀业务
秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)
- 独立部署
限流方式:
- 前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
- nginx 限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法
- 网关限流,限流的过滤器
- 代码中使用分布式信号量
- rabbitmq 限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。
2、秒杀流程
见秒杀流程图
3、限流
参照 Alibaba Sentinel
二、秒杀
1.启动前端项目
IDEA启动renren-fast项目,打开vscode,npm run dev
1).修改coupon的网关
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
2).vscode代码
coupon-seckillsession
2.1).查询、新增秒杀场次
查询秒杀场次
http://localhost:88/api/coupon/seckillskurelation/list?t=1623417281403&page=1&limit=10&key=&promotionSessionId=1
新增秒杀场次
2.2).关联商品
http://localhost:88/api/coupon/seckillskurelation/list?t=1623417281403&page=1&limit=10&key=&promotionSessionId=1
查询已经关联的商品:sms_seckill_sku_relation
coupon的seckillSkuRelationServiceImpl
@Override
public PageUtils queryPage(Map<String, Object> params) {
//list?t=1623417281403&page=1&limit=10&key=&promotionSessionId=1
QueryWrapper<SeckillSkuRelationEntity> wrapper = new QueryWrapper<SeckillSkuRelationEntity>();
String promotionSessionId = (String) params.get("promotionSessionId");
if (!StringUtils.isEmpty(promotionSessionId)) {
wrapper.eq("promotion_session_id", promotionSessionId);
}
IPage<SeckillSkuRelationEntity> page = this.page(
new Query<SeckillSkuRelationEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
三、秒杀服务-定时任务&cron表达式
1.秒杀微服务
1.0 创建 秒杀微服务
1.1 添加依赖
<dependency>
<groupId>com.gulimall</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1.2 配置文件application.properties
spring.application.name=gulimall-seckill
server.port=25000
spring.cloud.nacos.discovery.server-addr=192.168.56.10:8848
spring.redis.host=192.168.56.10
1.3 启动类
@EnableDiscoveryClient //开启nacos
2.秒杀业务流程
把秒杀场次放入session、库存页放入session
2.1 秒杀流程图
逻辑:秒杀商品定时上架、秒杀
2.2 定时任务笔记
定时任务与分布式调度笔记:百度云地址 提取码1111
定时任务使用quartz-schedule。 http://www.quartz-scheduler.org/
1、cron 表达式
语法:秒 分 时 日 月 周 年(Spring 不支持)
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
cron表达式
在线Cron表达式生成器
2、cron 示例
3、SpringBoot 整合
@EnableScheduling
@Scheduled
2.3、测试 定时任务-异步执行
/**
* 定时任务
* 1、@EnableScheduling 开启定时任务
* 2、@Scheduled开启一个定时任务
*
* 异步任务
* 1、@EnableAsync:开启异步任务
* 2、@Async:给希望异步执行的方法标注
*/
@Slf4j
@Component //放入容器中
@EnableScheduling //开启定时任务
@EnableAsync // 开启异步任务
public class HelloScheduled {
@Scheduled(cron = "* * * * * ?")
@Async //异步执行的方法
public void hello() {
log.info("hello...");
}
}
* 秒 分 时 日 月 周 年(Spring 不支持)
* 1、在Spring中表达式是6位组成,不允许第七位的年份
* 2、在周几的的位置,1-7代表周一到周日
* 3、定时任务不该阻塞。默认是阻塞的
* 1)、可以让业务以异步的方式,自己提交到线程池
* CompletableFuture.runAsync(() -> {
* },execute);
*
* 2)、支持定时任务线程池;设置 TaskSchedulingProperties
* spring.task.scheduling.pool.size: 5
*
* 3)、让定时任务异步执行
* 异步任务
*
* 解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
*
*/
3.秒杀商品定时上架
3.1远程coupon
3.1.1 coupon的feign接口
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@GetMapping("/coupon/seckillsession/lates3DaySession")
R getLates3DaySession();
}
3.1.2 coupon秒杀的商品
@GetMapping("/lates3DaySession")
public R getLates3DaySession() {
List<SeckillSessionEntity> sessions= seckillSessionService.getLates3DaySession();
return R.ok().setData(sessions);
}
3.1.3 coupon秒杀的商品实现类
@Override
public List<SeckillSessionEntity> getLates3DaySession() {
//计算最近三天
String startTime = startTime();
String endTime = endTime();
//获取最近三天的秒杀场次
QueryWrapper<SeckillSessionEntity> wrapper = new QueryWrapper<>();
wrapper.between("start_time", startTime, endTime);
List<SeckillSessionEntity> list = this.list(wrapper);
if (list != null && list.size() > 0) {
List<SeckillSessionEntity> collect = list.stream().map(session -> {
Long id = session.getId();
QueryWrapper<SeckillSkuRelationEntity> wrapper1 = new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id);
List<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list(wrapper1);
session.setRelationSkus(relationSkus);
return session;
}).collect(Collectors.toList());
return collect;
}
return null;
}
//获取开始、结束时间
private String startTime() {
LocalDate now = LocalDate.now();
LocalTime min = LocalTime.MIN;
LocalDateTime start = LocalDateTime.of(now, min);
String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
private String endTime() {
LocalDate now = LocalDate.now();
LocalDate plus2 = now.plusDays(2);
LocalTime max = LocalTime.MAX;
LocalDateTime end = LocalDateTime.of(plus2, max);
String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
3.2 秒杀的控制器-SeckillSkuScheduled
3.2.1 秒杀商品定时上架
//上架最近三天需要三天秒杀的商品
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days() {
//1、重复上架无需处理
log.info("上架秒杀的商品...");
seckillService.uploadSeckillSkuLatest3Days();
}
3.2.2 秒杀商品定时上架的实现类
扫描最近三天需要参与秒杀的活动 分 上架商品、缓存到redis
1).上架商品
@Autowired
CouponFeignService couponFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ProductFeignService productFeignService;
@Autowired
RedissonClient redissonClient;
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码
@Override
public void uploadSeckillSkuLatest3Days() {
//1.扫描最近三天需要参与秒杀的活动。远程coupon的seckill-session
R r = couponFeignService.getLates3DaySession();
if (r.getCode() == 0) {
//1.1上架商品
List<SeckillSessionWithSkus> sessionData = r.getData(new TypeReference<List<SeckillSessionWithSkus>>() {});
//1.2.缓存到redis
//1.2.1.缓存秒杀活动的信息
saveSessionInfos(sessionData);
//1.2.2.缓存秒杀活动的关联商品信息
saveSessionSkuInfos(sessionData);
}
}
2).缓存到redis
保存数据到redis,包括缓存秒杀活动的信息、缓存秒杀活动的关联商品信息
a.缓存秒杀活动的信息
private void saveSessionInfos(List<SeckillSessionWithSkus> sessions) {
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
List<String> collect = session.getRelationSkus().stream().map(item -> session.getId().toString()).collect(Collectors.toList());
//缓存活动信息
redisTemplate.opsForList().leftPushAll(key, collect);
});
}
b.缓存秒杀活动的关联商品信息
分sku基本信息、sku秒杀信息、设置当前商品的秒杀时间信息、设置随机码、使用库存作为分布式的信号量。限流:
private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions) {
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
//准备hash操作
sessions.stream().forEach(session -> {
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//缓存商品信息。缓存为Hash结构
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
//1.sku基本信息。
// 远程product服务的SkuInfo
R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (r.getCode() == 0) {
SkuInfoVo skuInfo = r.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.设置随机码。seckill?skuId=1&key=1234r5498r5
String token = UUID.randomUUID().toString().replace("_", "");
redisTo.setRandomCode(token);
//5.使用库存作为分布式的信号量。限流:
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
//商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
//为了好保存,把数据变成JSON数据
String jsonString = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getSkuId().toString(), jsonString);
});
});
}
导入Redisson依赖、Redisson配置
<!--使用redisson作为所有分布式锁,分布式对象等功能性框架-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* @Description:
**/
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
//Redis url 应该以 redis:// 或 redis:// 开头
//2、根据Config创建出RedissonClient实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
远程商品服务,获取sku信息。feign接口
//信息
@RequestMapping("/info/{skuId}")
public R info(@PathVariable("skuId") Long skuId) {
SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
return R.ok().put("skuInfo", skuInfo);
}
@FeignClient("gulimall-product")
public interface ProductFeignService {
//获取sku商品信息
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);
}
3.2.3. 秒杀上架幂等性问题
上架过的商品重复上架,咋办??。保证幂等性问题
不管多少服务,商品只能上架一次。使用分布式锁
1).修改定时上架SeckillSkuScheduled-分布式锁
使用分布式锁,实现一次商品上架
锁的业务执行完成,状态已经更新完成。其他人会获取到最新状态
@Slf4j
@Service
public class SeckillSkuScheduled {
@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 * * ?")
@Scheduled(cron = "0 * * * * ?")
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();
}
}
}
2).修改SeckillServiceImpl实现类
判断redis是否有数据key,没有就缓存数据到redis
3.2.4. 同一商品多次被秒杀,怎办?
.同一商品,在不同场次秒杀,怎么处理?修改保存的redis的key
redis秒杀信息为sessionId_skuId,商品信息为sessionId_skuIds
4.秒杀商品页面渲染
4.1 查询当前时秒杀的商品
1).控制器controller
@RestController
public class SeckillController {
@Autowired
SeckillService seckillService;
//返回当前时间可以参与的秒杀商品信息
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus() {
List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
System.out.println("当前时间秒杀场次的商品数据vos: "+ vos);
return R.ok().setData(vos);
}
}
2).实现类SeckillServiceImpl
//返回当前时间可以参与的秒杀商品信息
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
//1.确定当前时间属于哪个秒杀场次
long time = new Date().getTime();
//从redis获取缓存数据。获取开始、结束时间
Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
for (String key : keys) {
//seckill:sessions:1624068000000_1624075200000
String replace = key.replace(SESSION_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 = redisTemplate.opsForList().range(key, -100, 100);
//获得操作redis的ops
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<String> list = ops.multiGet(range);
if (list != null) {
List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
SeckillSkuRedisTo redis = JSON.parseObject((String) item, SeckillSkuRedisTo.class);
//redis.setRandomCode(null);//当前秒杀开始需要随机码
return redis;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
return null;
}
4.2 页面渲染-product
1).修改网关
- id: gulimall_seckill_route
uri: lb://gulimall-seckill
predicates:
- Host=seckill.gulimall.com
2).配置域名
使用SwitchHosts.exe
测试http://seckill.gulimall.com/currentSeckillSkus
3).页面渲染
商品项目的index.html
<div class="swiper-slide">
<ul id="seckillSkuContent">
</ul>
</div>
$.get("http://seckill.gulimall.com/currentSeckillSkus", function (res) {
if (res.data.length > 0) {
res.data.forEach(function (item) {
$("<li οnclick='toDetail(" + item.skuId + ")'></li>")
.append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />"))
.append($("<p>"+item.skuInfo.skuTitle+"</p>"))
.append($("<span>" + item.seckillPrice + "</span>"))
.append($("<s>" + item.skuInfo.price + "</s>"))
.appendTo("#seckillSkuContent");
})
}
})
function toDetail(skuId) {
location.href = "http://item.gulimall.com/" + skuId + ".html";
}
4).获得秒杀的商品
4.1).秒杀的控制器
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
System.out.println("当前时间秒杀的商品to: " + to);
return R.ok().setData(to);
}
4.2).秒杀的实现类
//获得秒杀的商品
@Override
public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
//1.找到所有需要参与秒杀的商品key---
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
Set<String> keys = ops.keys();
if (keys != null && keys.size() > 0) {
String regx = "\\d_" + skuId;//正则表达式
for (String key : keys) {
//7_9,8_1。匹配成功,说明有此商品参与秒杀
if (Pattern.matches(regx, key)) {
String s = ops.get(key);
SeckillSkuRedisTo redisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);
//随机码
long current = new Date().getTime();
if (current >= redisTo.getStartTime() && current <= redisTo.getEndTime()) {
} else redisTo.setRandomCode(null);
System.out.println("redisTo: "+redisTo);
return redisTo;
}
}
}
return null;
}
4.3).商品product项目秒杀的feign接口
@FeignClient("gulimall-seckill")
public interface SecKillFeignService {
//查询秒杀的商品sku信息
@GetMapping("/sku/seckill/{skuId}")
R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
4.4).修改展示当前sku商品详情–SkuInfoServiceImpl
@Autowired
SecKillFeignService secKillFeignService;
public SkuItemVo item(Long skuId){
//3.查询当前sku商品是否参与秒杀优惠
CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
R r = secKillFeignService.getSkuSeckillInfo(skuId);
if (r.getCode() == 0) {
SeckillInfoVo data = r.getData(new TypeReference<SeckillInfoVo>() {
});
skuItemVo.setSeckillInfo(data);
}
}, executor);
CompletableFuture<Void> allOf = CompletableFuture.allOf( saleAttrFuture, descFuture, baseAttrFuture, imageFuture,secKillFuture);
}
4.5).渲染商品详情页面-item.html
<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>