赞
踩
/** * 秒杀活动商品关联 * * @author lianxiaoyu * @email 376536577@qq.com * @date 2021-06-04 16:44:55 */ @RestController @RequestMapping("coupon/seckillskurelation") public class SeckillSkuRelationController { @Autowired private SeckillSkuRelationService seckillSkuRelationService; /** * 列表 * params 代表请求参数 */ @RequestMapping("/list") public R list(@RequestParam Map<String, Object> params){ PageUtils page = seckillSkuRelationService.queryPage(params); return R.ok().put("page", page); } }
根据活动场次id获取SeckillSkuRelationEntity信息
@Service("seckillSkuRelationService") public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService { @Override public PageUtils queryPage(Map<String, Object> params) { //封装条件构造器 QueryWrapper<SeckillSkuRelationEntity> wrapper = new QueryWrapper<>(); //请求参数获取场次id 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); } }
<dependencies> <dependency> <groupId>com.lian.gulimall</groupId> <artifactId>gulimall-common</artifactId> <version>0.0.1-SNAPSHOT</version> <exclusions> <exclusion> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </exclusion> </exclusions> </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.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--redis的 redisson 实现分布式锁--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.12.0</version> </dependency> <!--引入分布式session--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!--引入rabbitmq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.10:3306/gulimall_seckill username: root password: root application: name: gulimall-seckill cloud: nacos: discovery: server-addr: 127.0.0.1:8848 #配置oss对象存储 #配置日期格式 jackson: date-format: yyyy-MM-dd HH:mm:ss #关闭thymeleaf缓存 thymeleaf: cache: false #配置redis redis: host: 192.168.56.10 port: 6379 #配置redis缓存类型 cache: type: redis redis: time-to-live: 3600000 # 指定redis中的过期时间为1h use-key-prefix: true #使用key前缀 cache-null-values: true #缓存空值,解决缓存穿透问题,缓存穿透是 数据库和缓存中不存在的数据 # alicloud: # access-key: LTAI5t6PMA6dybm1iuVL6RXQ # secret-key: EkkMyqaTwC3DgYWnSTuJOCPoR1kzJr # oss: # endpoint: oss-cn-beijing.aliyuncs.com mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml global-config: db-config: id-type: auto #配置逻辑删除,mybatisplus官网 logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) server: port: 25000 #配置日志打印 logging: level: com.lian.gulimall: debug
#spring.task.scheduling.pool.size=10 #session中存储的类型 spring.session.store-type=redis #redis主机 spring.redis.host=192.168.56.10 #配置rabbitmq的虚拟主机 spring.rabbitmq.virtual-host=/ spring.rabbitmq.host=192.168.56.10 spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.listener.simple.acknowledge-mode=manual #关闭thymeleaf的缓存 spring.thymeleaf.cache=false
@EnableRedisHttpSession //开启分布式事务
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GulimallSeckillApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSeckillApplication.class, args);
}
}
corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
cron表达式生成器
https://cron.qqe2.com/
定时任务启动,就会查到最近3天的秒杀商品
package com.lian.gulimall.seckill.schedule; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** * 定时任务 * 1、Scheduling 开启定时任务 * 2、Scheduled 开启一个定时任务 * 3、自动配置类 TaskSchedulingAutoConfiguration * * 异步任务 * 1、@EnableAsync 开启异步任务 * 2、@Async 给执行异步的方法加上 * 3、自动配置类 TaskExecutionAutoConfiguration * * 使用异步+定时任务 完成定时任务不阻塞的功能 */ @Slf4j @Component @EnableAsync @EnableScheduling public class HelloSchedule { /** * cron = "* * * * * ?" * 秒 分 时 日 月 周 * 日和周的位置,随便出现一个问号 ? */ @Async @Scheduled(cron = "* * * * * *") public void hello() throws Exception { log.info("hello..."); Thread.sleep(1000); } }
/**
* 定时任务和异步任务的配置类
* @EnableScheduling 定时任务
* @EnableAsync 异步任务
*/
@EnableScheduling
@EnableAsync
@Configuration
public class ScheduledConfig {
}
/** * 秒杀商品的定时上架 * 每天晚上3点,上架最近3天需要秒杀的商品 */ @Slf4j @Service public class SeckillSkuScheduled { @Autowired SeckillService seckillService; @Autowired RedissonClient redissonClient; //秒杀商品上架的锁 private final String upload_lock = "seckill:upload:lock"; @Async @Scheduled(cron = "0 * 3 * * ?") public void uploadSeckillLatest3Days(){ //上架参与秒杀的商品 log.info("上架秒杀的商品信息"); //上锁,锁10秒钟 RLock lock = redissonClient.getLock(upload_lock); lock.lock(10, TimeUnit.SECONDS); //无论成功与失败,最后都要解锁 try { //执行业务 seckillService.uploadSeckillLatest3Days(); } finally { //解锁 lock.unlock(); } } }
@RestController
@RequestMapping("coupon/seckillsession")
public class SeckillSessionController {
@Autowired
private SeckillSessionService seckillSessionService;
@GetMapping("/latest3DaySession")
public R getLatest3DaySession(){
//获取最近3天参与秒杀的活动及每个活动场次的秒杀商品列表
List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
return R.ok().setData(sessions);
}
}
获取最近3天参与秒杀的活动及每个活动场次的秒杀商品列表
两个表:sms_seckill_session(活动表)、sms_seckill_sku_relation(活动场次对应秒杀商品表)
@Override public List<SeckillSessionEntity> getLatest3DaySession() { //查询出最近3天参与的秒杀活动 List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime())); if (list != null && list.size()>0){ List<SeckillSessionEntity> collect = list.stream().map((session) -> { //获取活动场次id Long id = session.getId(); //根据活动场次id获取到对应的 秒杀商品列表 List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id)); //关联活动场次和秒杀商品列表 session.setRelationSkus(relationEntities); return session; }).collect(Collectors.toList()); //返回活动场次类列表,及每场活动id对应的秒杀商品列表 return collect; } return null; } //开始时间 private String startTime(){ //获取此时日期 年月日 LocalDate now = LocalDate.now(); //获取此时最小时间 时分秒 LocalTime localTime = LocalTime.MIN; //获取此时 年月日 时分秒 LocalDateTime startTime = LocalDateTime.of(now, localTime); //格式化日期模式 String formatStartTime = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return formatStartTime; } //结束时间 private String endTime(){ LocalDate now = LocalDate.now(); LocalDate localDate = now.plusDays(2); LocalTime localTime = LocalTime.MAX; LocalDateTime endTime = LocalDateTime.of(localDate, localTime); String formatEndTime = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return formatEndTime; } }
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
/**
* 获取最近3天参与的秒杀活动及每场活动场次id对应的所有商品
* sms_seckill_session 活动场次表
* sms_seckill_sku_relation 活动场次对应秒杀商品列表
*/
@GetMapping("/coupon/seckillsession/latest3DaySession")
R getLatest3DaySession();
}
@Service public class SeckillServiceImpl implements SeckillService { @Autowired CouponFeignService couponFeignService; //优惠服务远程调用 @Autowired StringRedisTemplate stringRedisTemplate; @Autowired ProductFeignService productFeignService; //商品服务远程调用 @Autowired RedissonClient redissonClient; @Autowired RabbitTemplate rabbitTemplate; //秒杀场次 private final String SESSIONS_CACHE_PREFIX ="seckill:sessions:"; //秒杀商品 private final String SKUKILL_CACHE_PREDIX ="seckill:skus"; //信号量 private final String SKU_STOCK_SEMPHORE ="seckill:stock:"; //后缀加商品随机码 @Override public void uploadSeckillLatest3Days() { //扫描最近3天需要参与秒杀的活动 //远程调用优惠服务,获取最近3天的所有活动场次及所有秒杀商品 R session = couponFeignService.getLatest3DaySession(); if (session.getCode() == 0){ //得到最近3天所有活动场次及对应的所有秒杀商品 SeckillSessionsWithSkus 等同于 SeckillSessionEntity(里面包含了sms_seckill_sku_relation表) List<SeckillSessionsWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {}); //上架秒杀商品,将秒杀商品缓存到redis里 //1、缓存活动信息 map(key=seckill:sessions:startTime_endTime,value=List<活动场次id_商品id>) saveSessionInfos(sessionData); //2、缓存活动的关联商品信息 saveSessionSkuInfos(sessionData); } } //sessions:代表最近3天的所有活动场次 及 每场次对应的所有秒杀商品 private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions) { //stream流处理每一个活动场次 sms_seckill_session sessions.stream().forEach((session)->{ //获取每个活动场次的开始和结束时间 Long startTime = session.getStartTime().getTime(); Long endTime = session.getEndTime().getTime(); //redis中保存的key=seckill:sessions:startTime_endTime String key = SESSIONS_CACHE_PREFIX+startTime+"_"+endTime; //判断redis中是否保存了此 key Boolean hasKey = stringRedisTemplate.hasKey(key); if (!hasKey){ //获取所有参与秒杀商品的 活动场次id_商品id //stream流处理 sms_seckill_sku_relation 秒杀商品表 List<String> collect = session.getRelationSkus().stream().map((item) -> { // 每个秒杀商品的 活动场次id_商品id 例如:2_3 return item.getPromotionSessionId()+"_"+item.getSkuId().toString(); }).collect(Collectors.toList()); //缓存活动信息 map(key,list<活动场次id_商品id>),将商品id的集合保存到redis中 stringRedisTemplate.opsForList().leftPushAll(key, collect); } }); } private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions) { //遍历活动场次类(包含了sms_seckill_sku_relation) sessions.stream().forEach((session)->{ //准备hash操作 BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX); //遍历秒杀商品类 sms_seckill_sku_relation = seckillSkuVo session.getRelationSkus().stream().forEach((seckillSkuVo -> { //4、生成商品的秒杀随机码 String token = UUID.randomUUID().toString().replace("-", ""); //redis保存的key: 活动场次id_商品id Boolean hasKey = ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString()); if (!hasKey) { //缓存商品 //此to 封装了 秒杀商品表和商品属性信息表,SeckillSkuVo SkuInfoEntity SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo(); //1、sku的基本数据 pms_sku_info R r = productFeignService.info(seckillSkuVo.getSkuId()); if (r.getCode() == 0) { SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {}); redisTo.setSkuInfoVo(skuInfo); } //2、sku的秒杀信息 sms_seckill_sku_relation BeanUtils.copyProperties(seckillSkuVo, redisTo); //3、设置当前商品的秒杀开始和结束时间 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); //秒杀商品设置随机码 redisTo.setRandomCode(token); //转为json字符串保存到redis中 String jsonString = JSON.toJSONString(redisTo); //redis保存商品 map(场次id_商品id,redisTo) ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(), jsonString); //5、引入分布式的信号量,使用库存作为分布式的信号量 seckill:stock:token RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMPHORE + token); //商品可以秒杀的数量作为信号量 semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } })); }); }
<!--redis的 redisson 实现分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
redisson是专门负责做分布式锁的
@Configuration public class MyRedissonConfig { /** * 所有对redisson的使用都是通过 redissonClient对象 */ @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient(){ //1、创建配置 Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.56.10:6379"); //2、根据config创建出redissonClient示例 RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
<!--redis的 redisson 实现分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
配置类
package com.lian.gulimall.seckill.config; 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; @Configuration public class MyRedissonConfig { /** * 所有对redisson的使用都是通过 redissonClient对象 */ @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient(){ //1、创建配置 Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.56.10:6379"); //2、根据config创建出redissonClient示例 RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
只要开启定时任务,redis就会查询出最近3天的活动场次及所有参与秒杀的商品
@Controller public class SeckillController { @Autowired SeckillService seckillService; /** * 返回当前时间可以参与的秒杀商品信息 */ @ResponseBody @GetMapping("/currentSeckillSkus") public R getCurrentSeckillSkus(){ List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus(); return R.ok().setData(vos); } }
//返回当前时间可以参与的秒杀商品信息 @Override public List<SeckillSkuRedisTo> getCurrentSeckillSkus() { //1、确定当前时间属于哪个秒杀场次 //获取当前时间 long time = new Date().getTime(); //获取redis中保存的所有秒杀商品场次 key:seckill:sessions:开始时间_结束时间 Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*"); for (String key : keys) { //用空串替代前缀,留下 开始时间_结束时间 String replace = key.replace(SESSIONS_CACHE_PREFIX, ""); String[] s = replace.split("_"); long startTime = Long.parseLong(s[0]); long endTime = Long.parseLong(s[1]); if (time >= startTime && time <= endTime){ //2、获取这个秒杀场次需要的所有商品信息 //获取在活动期间的所有key的值 1_1 、 2_1 key:seckill:sessions:1629245700000_1629302399000 //range:取出此key中的所有值 List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100); //绑定值操作 BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX); //批量获取 key 1_1 、 2_1 的值 List<String> list = hashOps.multiGet(range); if (list != null){ List<SeckillSkuRedisTo> collect = list.stream().map((item) -> { SeckillSkuRedisTo redisTo = JSON.parseObject((String) item, SeckillSkuRedisTo.class); //redisTo.setRandomCode(null); //当前秒杀开始才需要随机码 return redisTo; }).collect(Collectors.toList()); return collect; } break; } } return null; }
/**
* 获取当前sku的秒杀信息
* 判读当前sku的商品是否参与秒杀活动
*/
@ResponseBody
@GetMapping("/sku/seckill/{skuId}")
public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId){
SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
return R.ok().setData(to);
}
@Override public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) { //找到所有需要参与秒杀的商品的key BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX); Set<String> keys = hashOps.keys(); if (keys.size()>0 && keys!=null){ //场次id + 商品skuId 6_4 String regex = "\\d_"+skuId; for (String key : keys) { //只要活动场次中有此skuId的商品,就都匹配 if (Pattern.matches(regex, key)){ String json = hashOps.get(key); SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class); //随机码 //如果在秒杀期间,就有随机码,否则随机码就为空 long current = new Date().getTime(); Long startTime = skuRedisTo.getStartTime(); Long endTime = skuRedisTo.getEndTime(); if (current >= startTime && current <= endTime){ }else { //否则,随机码为空 skuRedisTo.setRandomCode(null); } return skuRedisTo; } } } return null; }
@FeignClient("gulimall-seckill")
public interface SeckillFeignService {
/**
* 获取当前sku的秒杀信息
*/
@GetMapping("/sku/seckill/{skuId}")
R getSkuSeckillInfo(@PathVariable("skuId") Long skuId);
}
商品服务的 itemController层查询skuId商品是否参与秒杀,并将其封装起来,供前台详情页调用
@Override public SkuItemVo item(Long skuId) throws Exception{ //商品详情页返回数据都封装到 SkuItemVo SkuItemVo skuItemVo = new SkuItemVo(); /** * 使用异步编排,节省时间提升效率,一起执行不阻塞等待 * supplyAsync 有返回值,其他任务可以用 * 开启一个异步任务,创建异步对象 * infoFuture 任务完成后,saleAttrFuture、descFuture、baseAttrFuture 才开始执行,因为他们都需要依赖任务1的数据结果 */ CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> { //1、sku基本信息获取,标题、副标题、价格等 pms_sku_info SkuInfoEntity info = baseMapper.selectById(skuId); skuItemVo.setInfo(info); //因为其他任务要用基本信息,所以我们返回基本信息 return info; //executor代表要放到自己的线程池里面 }, executor); //接下来接收任务的返回结果,accept只是接收上一个任务的结果,自己不返回结果 CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> { //执行第二个任务 //3、获取spu的销售属性组合 List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId()); skuItemVo.setSaleAttr(saleAttrVos); }); //继续执行任务 CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> { //4、获取spu的介绍 SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId()); skuItemVo.setDesc(spuInfoDescEntity); }, executor); //继续执行任务,任务3、4、5都依赖任务1的结果 获取spuId CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> { //5、获取spu的规格参数信息 List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId()); skuItemVo.setGroupAttrs(attrGroupVos); }, executor); /** * 任务2 不需要依赖任务1提供的结果数据,所以不需要等待任务1完成,直接和任务1同步执行,所以自己也开启一个异步任务 * runAsync 代表不需要返回结果,因为也没有其他任务需要依赖任务2的数据 */ CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> { //2、sku图片信息 pms_sku_images List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId); skuItemVo.setImages(images); }, executor); /** * 查询当前sku是否参加秒杀优惠 */ CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> { R r = seckillFeignService.getSkuSeckillInfo(skuId); if (r.getCode() == 0) { SeckillInfoVo data = r.getData(new TypeReference<SeckillInfoVo>() { }); skuItemVo.setSeckillInfoVo(data); } }, executor); /** * 等待所有任务都完成,因为每一个任务都是在给 vo 中封装数据 * get()方法就是阻塞等待所有任务都执行完 * infoFuture 也可以不写,因为别人是依赖她的,如果别人都执行完了,那么她肯定也执行完了 */ CompletableFuture.allOf(infoFuture, saleAttrFuture, descFuture, baseAttrFuture, imageFuture,secKillFuture).get(); return skuItemVo; }
pom依赖
<!--引入分布式session--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
application.properties
#session中存储的类型
spring.session.store-type=redis
#redis主机
spring.redis.host=192.168.56.10
主启动类配置注解
/** * 定时任务和异步任务的配置类 * @EnableScheduling 定时任务 * @EnableAsync 异步任务 */ @EnableScheduling @EnableAsync @EnableRedisHttpSession //开启分布式事务 @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class GulimallSeckillApplication { public static void main(String[] args) { SpringApplication.run(GulimallSeckillApplication.class, args); } }
springSession配置类
/** * 自定义cookie的序列化 * 自定义redis的序列化 */ @Configuration public class GulimallSessionConfig { @Bean public CookieSerializer cookieSerializer(){ DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer(); defaultCookieSerializer.setDomainName("gulimall.com"); defaultCookieSerializer.setCookieName("GULISESSION"); defaultCookieSerializer.setCookiePath("/"); return defaultCookieSerializer; } @Bean public RedisSerializer<Object> redisSerializer(){ return new GenericJackson2JsonRedisSerializer(); } }
@Component public class LoginUserIntereptor implements HandlerInterceptor { public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //对指定路径放行。秒杀请求: localhost:8080/kill //获取请求路径 uri:代表localhost:8080/ 斜杠后面的请求 url:代表是localhost:8080/ 全部的请求路径 String uri = request.getRequestURI(); AntPathMatcher matcher = new AntPathMatcher(); boolean match = matcher.match("/kill", uri); //如果是秒杀请求,才需要判断是否登录,只有登录才可以进行秒杀活动 if (match){ MemberRespVo respVo = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER); if (respVo != null){ loginUser.set(respVo); return true; }else { request.getSession().setAttribute("msg","请先进行登录"); response.sendRedirect("http://auth.gulimall.com/login.html"); return false; } }else { //如果不是秒杀请求,其他的请求都放行 return true; } } }
拦截器配置类
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserIntereptor loginUserIntereptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserIntereptor).addPathPatterns("/**");
}
}
/** * 秒杀方法 * @param killId 场次id_商品id * @param key 随机码 * @param num 秒杀数量 * @return */ @GetMapping("/kill") public String secKill(@RequestParam("killId") String killId, @RequestParam("key") String key, @RequestParam("num") Integer num, Model model){ //秒杀成功就返回一个订单号 String orderSn = seckillService.kill(killId,key,num); System.out.println("orderSn是:"+orderSn); model.addAttribute("orderSn", orderSn); return "success"; }
@Override public String kill(String killId, String key, Integer num) { //获取拦截器中登录的用户 MemberRespVo memberRespVo = LoginUserIntereptor.loginUser.get(); //1、获取当前秒杀商品的详细信息 BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREDIX); //根据秒杀id获取商品信息 2_1 String json = hashOps.get(killId); if (StringUtils.isEmpty(json)){ return null; }else { SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class); //todo 校验合法性 //1、校验时间的合法性 //获取开始和结束时间 Long startTime = redisTo.getStartTime(); Long endTime = redisTo.getEndTime(); //获取活动持续时间 Long ttl = endTime - startTime; //获取当前时间 long time = new Date().getTime(); //如果秒杀时间正确,就进行秒杀,否则返回为空 if (time >= startTime || time <= endTime){ //2、校验随机码和商品id //获取随机码 String randomCode = redisTo.getRandomCode(); //获取秒杀id = 场次id+商品skuid String skuId = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId(); //匹配传入的随机码和秒杀id是否正确 if (randomCode.equals(key) && skuId.equals(killId)){ //3、验证购物数量是否合理 Integer seckillLimit = redisTo.getSeckillLimit(); //如果购买数量小于每人限购数量,才是合法的 if (num <= seckillLimit){ //4、验证这个人是否已经购买过此秒杀商品(防止一人多次重复秒杀商品) String redisKey = memberRespVo.getId()+"_"+skuId; //自动过期 等同于 setnx 设置如果不存在 Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS); if (aBoolean){ //如果此 redisKey 设置key-v成功,代表从来没有买过秒杀商品 //获取信号量 + 后缀随机码 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMPHORE+randomCode); //从信号量中取出一个 //请求一个信号量,减少num个 boolean b = semaphore.tryAcquire(num); if (b) { //秒杀成功 //快速下单,发送mq消息,10ms //获取商品订单 ID String timeId = IdWorker.getTimeId(); //封装 SeckillOrderTo orderTo = new SeckillOrderTo(); orderTo.setOrderSn(timeId); orderTo.setPromotionSessionId(redisTo.getPromotionSessionId()); orderTo.setSkuId(redisTo.getSkuId()); orderTo.setSeckillPrice(redisTo.getSeckillPrice()); orderTo.setNum(num); //用户id,说明是哪个用户秒杀的商品 orderTo.setMemberId(memberRespVo.getId()); //利用rabbitmq发送消息到mq(订单服务接收此消息) rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo); return timeId; } return null; } }else { //说明此人已经买过秒杀商品,就不能买了 return null; } } }else { return null; } } return null; }
秒杀服务秒杀成功后,发送消息到mq中,订单服务监听消息,负责下单处理
<!--引入rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
#配置rabbitmq的虚拟主机
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@Configuration
public class MyRabbitmqConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
//声明秒杀队列 @Bean public Queue orderSeckillOrderQueue(){ Queue queue = new Queue("order.seckill.order.queue", true, false, false, null); return queue; } //声明秒杀队列与交换机绑定 @Bean public Binding orderSeckillOrderQueueBinding(){ return new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.seckill.order", null ); }
@Slf4j @RabbitListener(queues = "order.seckill.order.queue") @Service public class OrderSeckillListener { @Autowired OrderService orderService; @RabbitHandler public void listener(SeckillOrderTo seckillOrderTo, Channel channel, Message message){ try { log.info("准备创建秒杀单的详细信息"); orderService.createSeckillOrder(seckillOrderTo); } catch (Exception e) { e.printStackTrace(); } } }
/** * 创建秒杀单的详细信息,就是保存订单 orderEntity 和 orderItemEntity * @param seckillOrderTo */ @Override public void createSeckillOrder(SeckillOrderTo seckillOrderTo) { //1、保存订单信息 OrderEntity orderEntity = new OrderEntity(); orderEntity.setOrderSn(seckillOrderTo.getOrderSn()); orderEntity.setMemberId(seckillOrderTo.getMemberId()); orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode()); BigDecimal price = seckillOrderTo.getSeckillPrice().multiply(new BigDecimal("" + seckillOrderTo.getNum())); orderEntity.setPayAmount(price); this.save(orderEntity); //2、保存订单项信息 OrderItemEntity itemEntity = new OrderItemEntity(); //设置sku的详细设置 R r = productFeignService.getSpuInfoBySkuId(seckillOrderTo.getSkuId()); itemEntity.setSkuQuantity(seckillOrderTo.getNum()); SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {}); itemEntity.setSpuBrand(spuInfo.getBrandId().toString()); itemEntity.setSpuName(spuInfo.getSpuName()); itemEntity.setCategoryId(spuInfo.getCatalogId()); orderItemService.save(itemEntity); }
http://localhost:25000/kill?killId=1_1&key=a7397e32a3794ac3acaa7639a2e51917&num=1
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。