谷粒商城:秒杀商品定时上架

本文介绍了Spring中定时任务的配置与使用,包括非阻塞执行、线程池配置以及@Scheduled注解的详细说明。同时,阐述了秒杀商品上架的流程,涉及到分布式秒杀系统的实现,包括从远程服务获取秒杀活动、缓存策略以及使用Redis进行分布式信号量控制以确保秒杀的公平性。最后,提出了幂等性的解决方案,通过分布式锁防止重复上架。
摘要由CSDN通过智能技术生成

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();
    }
}

文章完!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值