Day437&438.秒杀服务 -谷粒商城
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day437&438.秒杀服务 -谷粒商城相关的知识,希望对你有一定的参考价值。
秒杀服务
一、定时任务-Quartz
Cron表达式
执行定时任务需要给一个时间计划,这个时间计划可以用 Cron 表达式来编写
官方文档 Cron 表达式是一个字符串,是用空格分割的六到七个属性。
语法:秒 分 时 日 月 周 年(可忽略年,Spring 不支持年)
定时任务只能精确到秒
Seconds
:0-59,举例:0 就是整秒执行,1 就是在第1秒的时候执行
Day of week
:值可以写 1-7,也可以写 SUN-SAT,1 就是周日,7 就是周六
①特殊字符
,
:枚举
(cron="7,9,23 * * * * ?")
:代表任意时刻的7,9,23秒启动这个任务;
-
:范围
(cron="7-20 * * * * ?")
:任意时刻的 7-20 秒之间,每秒启动一次
*
:任意
- 指定位置的任意时刻都可以
/
:步长
(cron="7/5 * * * * ?")
:第 7 秒启动,每 5 秒一次;(cron="*/5 * * * * ?")
:任意时间启动之后,每 5 秒一次;
?`:(出现在日和周几的位置)为了防止日和周冲突,如果1个精确了,另一个就得写`?
(cron="* * * 1 * ?")
:每月的 1 号,启动这个任务,如果两个都写精确值的话,可能会导致冲突,所以其中一个要使用?
L
:(出现在日和周的位置)”,last:最后一个
(cron="* * * ? * 3L")
:每月的最后一个周二
W
: Work Day:工作日
(cron="* * * W * ?")
:每个月的工作日触发(cron="* * * LW * ?")
:每个月的最后一个工作日触发
#
: 第几个
(cron="* * * ? * 5#2")
:5 代表周 4,#2 代表第 2 个,合起来就是每个月的第 2 个周 4
②示例
表达式 | 意义 |
---|---|
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点启动,每隔1分钟执行一次 |
0 0/5 14 ? | 每天14点启动,每隔5分钟执行一次 |
0 0/5 14,18 ? | 每天的14点、18点启动,每隔5分钟执行一次 |
0 0-5 14 ? | 14:00-14:05执行,每分钟执行一次 |
0 10,44 14 ? 3 WED | 3月的每个星期三的14:10 、14:44启动这个任务,每分钟执行一次 |
0 15 10 ? * MON-FRI | 每月的周一到周五,10:15执行 |
0 15 10 ? * 6L | 每月的最后一个周五,10:15执行 |
0 15 10 ? * 6#3 | 每月的第3个周5,10:15执行 |
二、Spring Boot整合定时任务
1、与Quarts的区别
自动配置类参考 TaskSchedulingAutoConfiguration
@Slf4j
@Component
@EnableScheduling // 开启定时功能
public class HelloSchedule {
/**
* 一、与Quarts的区别
* 1、Spring中的定时任务由6位组成,不支持第7位的年
* 2、第6位的数字格式,1-7,代表周一到周日,当然也可以写成MON-SUN
* 3、其它普遍与Quarts一致
*/
@Scheduled(cron = "*/5 * * ? * 1") // 开启定时任务
public void hello(){
log.info("hello");
}
}
2、定时任务默认是阻塞的
/**
* 二、定时任务默认是阻塞的
* 只要当前任务没执行完,下一个任务就执行不了
* @throws InterruptedException
*/
@Scheduled(cron = "* * * ? * 1")
public void block() throws InterruptedException {
log.info("hello......");
Thread.sleep(3000);
}
3、解决定时任务阻塞的方法
-
可以使用异步任务的方式,
CompletableFuture.runAsync()
,自己提交到线程池 -
修改配置文件,
spring.task.scheduling.pool.size=5
-
让定时任务异步执行
①异步任务
- 首先在类上面标注
@EnableAsync
,开启异步任务功能 - 然后在方法上标注
@Async
,执行异步任务 - 这个异步任务不是只能搭配定时任务,它可以替代
CompletableFuture
- 自动配置类参考
TaskExecutionAutoConfiguration
- 它在配置文件中的线程池属性是:
spring.task.execution.pool.xxx
②最终
使用异步+定时任务来实现定时任务不阻塞
三、定时上架秒杀商品
1、简介
每天凌晨3点,上架最近3天所需要秒杀的商品
因为这个时间段服务器压力较小,并且比较空闲,
上架最近3天的商品,可以给用户一个预告的功能,让用户提前知道哪个商品什么时间将要开启秒杀
2、随机码
为了防止有用户在得知秒杀请求时,发送大量请求对商品进行秒杀,我们采取了随机码的方式,即每个要参加秒杀的商品,都有一个随机码,只有通过正常提交请求的流程才可以获取,否则谁都无法得知随机码是多少,避免了恶意秒杀
3、商品的分布式信号量
信号量保存了当前秒杀商品的库存信息
我们的库存秒杀不应该是实时去数据库扣库存,因为几百万请求进来的时候,如果都去扣,那会直接把数据库压垮。
所以现在秒杀最大的问题就是,如何应对这些高并发的流量
首先,这么大的流量进到服务器的话,肯定有一些流量是无效的,比如秒杀不成功,假设我们现在就一百个商品要被秒杀,哪怕放进来一百万请求,最终也只有一百个请求,能成功的去数据库扣掉库存。
所以我们可以提前在 redis 里边设置一个信号量,这个信号量可以认为是一个自增量,假设这个信号量叫 count
,它专门用来计数,它的初始值是 100,每进来一个请求,我们就让这个值减一,如果有用户想要秒杀这个商品,我们先去 redis
里边获取一个信号量,也就是给这一百的库存减一,然后这个值就变成九十九,如果能减成功了,那就把这个请求放行,然后再做后边的处理数据库。如果不能减,那就不用进行后续的操作了,我们只会阻塞很短的时间,就会释放这个请求,我们只有每一个请求都能很快的释放,能很快的做完,我们才能拥有处理大并发的能力。
这块有一个注意点,由于每一个请求进来减这个信号量的值,就是当前商品的库存信息,只有请求里携带了我们给秒杀商品设计的随机码,才可以来减信号量,如果不带随机码的话,直接减信号量的话,就会出现问题,可能秒杀还没开始,有一些恶意请求,就把信号量就减了了。
所以上面说的随机码是一种保护机制。
4、代码
- 创建秒杀项目模块
-
引入依赖
-
com.achang.achangmall.coupon.controller.SeckillSessionController
/**
* 查询最近三天需要参加秒杀商品的信息
* @return
*/
@GetMapping(value = "/Lates3DaySession")
public R getLates3DaySession() {
List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLates3DaySession();
return R.ok().setData(seckillSessionEntities);
}
- com.achang.achangmall.coupon.service.impl.SeckillSessionServiceImpl
@Service("seckillSessionService")
public class SeckillSessionServiceImpl extends ServiceImpl<SeckillSessionDao, SeckillSessionEntity> implements SeckillSessionService {
@Autowired
private SeckillSkuRelationService seckillSkuRelationService;
@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;
}
/**
* 当前时间
*/
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;
}
}
- com.achang.achangmall.coupon.entity.SeckillSessionEntity
@TableField(exist = false)
private List<SeckillSkuRelationEntity> relationSkus;
- com.achang.achangmall.seckill.vo.SeckillSkuVo
@Data
public class SeckillSkuVo {
private Long id;
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private Integer seckillCount;
/**
* 每人限购数量
*/
private Integer seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
}
- com.achang.achangmall.seckill.config.ScheduledConfig
@Configuration
@EnableAsync
@EnableScheduling
public class ScheduledConfig {}
- com.achang.achangmall.seckill.feign.CouponFeignService
@FeignClient("achangmall-coupon")
public interface CouponFeignService {
/**
* 查询最近三天需要参加秒杀商品的信息
*/
@GetMapping(value = "/coupon/seckillsession/Lates3DaySession")
R getLates3DaySession();
}
- com.achang.achangmall.seckill.to.SeckillSkuRedisTo
@Data
public class SeckillSkuRedisTo {
/**
* 活动id
*/
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private Integer seckillCount;
/**
* 每人限购数量
*/
private Integer seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
//sku的详细信息
private SkuInfoVo skuInfo;
//当前商品秒杀的开始时间
private Long startTime;
//当前商品秒杀的结束时间
private Long endTime;
//当前商品秒杀的随机码
private String randomCode;
}
- com.achang.achangmall.seckill.vo.SeckillSessionWithSkusVo
@Data
public class SeckillSessionWithSkusVo {
private Long id;
/**
* 场次名称
*/
private String name;
/**
* 每日开始时间
*/
private Date startTime;
/**
* 每日结束时间
*/
private Date endTime;
/**
* 启用状态
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
private List<SeckillSkuVo> relationSkus;
}
- com.achang.achangmall.seckill.feign.ProductFeignService
@FeignClient("achangmall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getSkuInfo(@PathVariable("skuId") Long skuId);
}
- 引入分布式锁redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
- com.achang.achangmall.seckill.config.MyRedissonConfig
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.109.101:6379");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
- com.achang.achangmall.seckill.service.impl.SeckillServiceImpl
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
private ProductFeignService productFeignService;
@Autowired
private CouponFeignService couponFeignService;
private final String SESSION__CACHE_PREFIX = "seckill:sessions:";
private final String SECKILL_CHARE_PREFIX = "seckill:skus";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void uploadSeckillSkuLatest3Days() {
R lates3DaySession = couponFeignService.getLates3DaySession();
if (lates3DaySession.getCode()==0){
List<SeckillSessionWithSkusVo> vo = lates3DaySession.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {
});
//缓存到Redis
//1、缓存活动信息
saveSessionInfos(vo);
//2、缓存活动的关联商品信息
saveSessionSkuInfo(vo);
}
}
/**
* 缓存秒杀活动所关联的商品信息
*/
private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> 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());
}
});
});
}
/**
* 缓存秒杀活动信息
*/
private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
sessions.stream().forEach(session -> {
//获取当前活动的开始和结束时间的时间戳
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
//存入到Redis中的key
String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;
//获取到活动中所有商品的skuId
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,skuIds);
});
}
}
四、上架秒杀商品的幂等性保证
1、定时任务在分布式下的问题
Day762.Redis秒杀场景 -Redis 核心技术与实战