1.定时任务
1、spring中6位组成,不允许第七位的年,即秒、分、时、日、月、周
2、在周几的位置,1-7代表周一到周日,MON-SUN
3、定时任务不应该是阻塞的,默认是阻塞的。
(1)可以让业务以异步的方式运行,自己提交给线程池
(2)支持定时任务线程池,设置TaskShedulingProperties
spring.task.scheduling.pool.size=5 #默认size是1,也就是阻塞
(3)让定时任务异步执行,自动配置类 TaskExecutionAutoConfiguration
spring:
task:
execution:
pool:
core-size: 5 #默认是8
max-size: 50 #默认最大是Integer.MAX_VALUE
定时任务不阻塞最终解决方案:异步+定时任务
@Async
@Scheduled(cron = "*/5 * * ? * 1")
public void hello(){
log.info("定时任务...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) { }
}
基本注解:
1、定时任务
(1)@EnableScheduling:开启定时任务
(2)@Scheduled:开启一个定时任务
2、异步任务
(1)@EnableAsync:开启异步任务功能
(2)@Async:给希望异步执行的方法上标注
在配置类里面标注注解,其他地方就不需要标了
@EnableAsync
@Configuration
@EnableScheduling
public class ScheduledConfig {
}
2. 秒杀商品上架
秒杀商品上架的流程图
gulimall-coupn服务:定时扫描数据,获取最近3天的秒杀活动
@Override // gulimall-coupon SeckillSessionServiceImpl
public List<SeckillSessionEntity> getLate3DaySession() {
// 获取最近3天的秒杀活动
List<SeckillSessionEntity> list = this.list(
new QueryWrapper<SeckillSessionEntity>()
.between("start_time", startTime(), endTime()));
// 设置秒杀活动里的秒杀商品
if(list != null && list.size() > 0){
return list.stream().map(session -> {
// 给每一个活动写入他们的秒杀项
Long id = session.getId();
// 根据活动id获取每个sku项
List<SeckillSkuRelationEntity> entities =
skuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
session.setRelationSkus(entities);
return session;
}).collect(Collectors.toList());
}
return null;
}
// LocalDateTime.of把时间合在一起组合:startTime=now+LocalTime.MIN:00:00
// 最小时间LocalTime.MIN:00:00
// 最大时间LocalTime.MAX:23:59:59:99999999
private String startTime(){
LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
return start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
private String endTime(){
LocalDate acquired = LocalDate.now().plusDays(2);//加两天
return LocalDateTime.of(acquired, LocalTime.MAX).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
gulimall-seckill服务:
远程从gulimall-coupon中获取最近三天要参加秒杀的商品,写入到redis缓存中(活动信息+活动的关联的商品信息)
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
private final String SKUSTOCK_SEMAPHONE = "seckill:stock:"; // +商品随机码
@Override // SeckillServiceImpl
public void uploadSeckillSkuLatest3Day() {
// 1.扫描最近三天要参加秒杀的商品
R r = couponFeignService.getLate3DaySession();
if(r.getCode() == 0){
List<SeckillSessionsWithSkus> sessions = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {});
// 2.缓存活动信息
saveSessionInfo(sessions);
// 3.缓存活动的关联的商品信息
saveSessionSkuInfo(sessions);
}
}
缓存活动信息
key=seckill:sessions:开始时间_结束时间
value=List 格式是sessionid-skuId
private void saveSessionInfo(List<SeckillSessionsWithSkus> sessions){
if(sessions != null){
// 遍历session
sessions.stream().forEach(session -> {
//时间比较的话Long值比较方便
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime; // "seckill:sessions:";
Boolean hasKey = stringRedisTemplate.hasKey(key);
// 防止重复添加活动到redis中
if(!hasKey){
// 获取所有商品id // 格式:活动id-skuId
// 遍历sku
List<String> skus = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId()).collect(Collectors.toList());
// 缓存活动信息,leftPushAll从左边往里面放一整个集合
stringRedisTemplate.opsForList().leftPushAll(key, skus);
}
});
}
}
缓存活动的关联的商品信息
创建SeckillSkuRedisTo来表示给缓存中写的数据传输内容
内容包括:活动场次id+sku详细信息(SkuInfoVo)+sku秒杀信息(秒杀价格、总量、开始结束时间)+秒杀随机码
此处添加秒杀随机码的目的:
- 避免恶意争抢,确保是公平的秒杀,只有在秒杀开始时才可以秒杀,如果不加随机码,直接带skuId的话就会有被脚本恶意攻击的可能
- 分布式信号量:seckill:stock:#(商品随机码),在使用库存作为分布式信号量时也是使用的这个随机码,避免被伪造的恶意请求把信号量给减去了
比如:seckill?skuId=1&key=skjdncsk
@Data
public class SeckillSkuRedisTo {
private Long promotionId;
/**
* 活动场次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 商品的秒杀随机码
*/
private String randomCode;
/**
* 秒杀价格
*/
private BigDecimal seckillPrice;
/**
* 秒杀总量
*/
private BigDecimal seckillCount;
/**
* 每人限购数量
*/
private BigDecimal seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
/**
* sku的详细信息
*/
private SkuInfoVo skuInfoVo;
/**
* 商品秒杀的开始时间
* Long类型进行比较时很快,所以使用Long类型
*/
private Long startTime;
/**
* 商品秒杀的结束时间
*/
private Long endTime;
}
缓存活动的关联商品信息
(1)seckill:skus:Hash结构,设置SeckillSkuRedisTo的内容,绑定hash操作,key:sessionid-skuId
(2)设置库存为分布式信号量semaphore,
/**
* 活动redis
* seckill:sessions:开始时间_结束时间
* skuIds[]
* ====================
* 商品redis
* Map【seckill:skus:】
* <session-skuId,skuId的图片、随机码等详细信息>
* ========================
* 信号量
* <seckill:stock:随机码,
* 信号量>
* */
private void saveSessionSkuInfo(List<SeckillSessionsWithSkus> sessions){
if(sessions != null){
// 遍历session
sessions.stream().forEach(session -> {
// sku信息redis,准备往里放内容
BoundHashOperations<String, Object, Object> ops =
stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); // "seckill:skus:";
// 遍历sku往上面redis里放内容
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
// 1.商品的随机码
String randomCode = UUID.randomUUID().toString().replace("-", "");
// 缓存中没有再添加 // key:sessionid-skuId
if(!ops.hasKey(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId())){
// 2.缓存商品
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
BeanUtils.copyProperties(seckillSkuVo, redisTo);
// 3.sku的基本数据 sku的秒杀信息
R info = productFeignService.skuInfo(seckillSkuVo.getSkuId());
if(info.getCode() == 0){
SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
redisTo.setSkuInfoVo(skuInfo);
}
// 4.设置当前商品的秒杀信息
// 设置时间
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
// 设置随机码
redisTo.setRandomCode(randomCode);
// sku信息放到第二个redis里 // key:活动id-skuID
ops.put(seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId(),
JSON.toJSONString(redisTo));
// 5.使用库存作为分布式信号量 限流,得到信号量才去改db
RSemaphore semaphore =
redissonClient.getSemaphore(SKUSTOCK_SEMAPHONE + randomCode);//"seckill:stock:";
// 在信号量中设置秒杀数量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
}
});
});
}
}
秒杀上架的幂等性问题
(1)使用Redis的分布式锁,锁的名称为seckill:upload:lock
(2)在代码内部再判断一次,如果key不存在才会进行上架,缓存中没有才添加
private final String upload_lock = "seckill:upload:lock";
/**
* 这里应该是幂等的
* 三秒执行一次:* /3 * * * * ?
* 8小时执行一次:0 0 0-8 * * ?
*/
@Scheduled(cron = "0 0 0-8 * * ?")
public void uploadSeckillSkuLatest3Day(){
log.info("\n上架秒杀商品的信息");
// 1.重复上架无需处理 加上分布式锁 状态已经更新 释放锁以后其他人才获取到最新状态
RLock lock = redissonClient.getLock(upload_lock);// "seckill:upload:lock";
lock.lock(10, TimeUnit.SECONDS);//锁的释放时间
try {
seckillService.uploadSeckillSkuLatest3Day();
} finally {
lock.unlock();
}
}
文章完!!!