准备工作概述 商城秒杀业务的性质是高并发 我们的基本数据 并发12万 同时在线45万用户 日活跃用户10万 学习完秒杀业务,我们能具备处理一般高并发业务的基本逻辑 下面我们要做的是准备工作 除了基本的CRUD之外 我们还要做一些缓存预热工作 秒杀模块是mallseckill,这个模块操作的数据库是mallseckill 数据库中包含秒杀spu信息和秒杀sku信息以及秒杀成功记录 我们要利用Quartz周期性的将每个批次的秒杀商品,预热到Redis 所谓预热就是将即将出现高并发查询的数据提前保存在Redis中 我们的业务只是将每个商品的库存数保存在Redis即可查询秒杀商品列表 mallseckillwebapi项目开发持久层 创建mapper包,创建SeckillSpuMapper 代码如下RepositorypublicinterfaceSeckillSpuMapper{查询秒杀商品列表ListSeckillSpufindSeckillSpus();} SeckillSpuMapper。xml文件添加内容!查询秒杀spu的sql片段sqlidSimpleFieldiftesttrueid,spuid,listprice,starttime,endtime,gmtcreate,gmtmodifiedifsql!查询秒杀商品列表spu的方法selectidfindSeckillSpusresultTypemall。pojo。seckill。model。SeckillSpuselectincluderefidSimpleFieldfromseckillspuselect开发业务逻辑层 创建包service。impl 包中创建SeckillSpuServiceImpl实现ISeckillSpuServiceServiceSlf4jpublicclassSeckillSpuServiceImplimplementsISeckillSpuService{查询秒杀spu表的数据AutowiredprivateSeckillSpuMapperseckillSpuM秒杀spu表中没有商品详细介绍,需要根据spuid借助Dubbo查询product模块pms数据库中的spu表,获得商品信息DubboReferenceprivateIForSeckillSpuServicedubboSeckillSpuSOverridepublicJsonPageSeckillSpuVOlistSeckillSpus(Integerpage,IntegerpageSize){分页查询秒杀表中spu信息PageHelper。startPage(page,pageSize);ListSeckillSpuseckillSpusseckillSpuMapper。findSeckillSpus();我们返回给页面的,应该是包含商品详细信息的对象,不能只是SeckillSpu中的信息业务逻辑层返回值泛型类型SeckillSpuVO中包含秒杀信息和商品详细信息,返回它的集合可以满足查询需要ListSeckillSpuVOseckillSpuVOsnewArrayList();循环遍历秒杀列表,根据秒杀商品列表中元素的spuId查询spu详情for(SeckillSpuseckillSpu:seckillSpus){获得SpuIdLongspuIdseckillSpu。getSpuId();dubbo调用查询商品详情SpuStandardVOspuStandardVOdubboSeckillSpuService。getSpuById(spuId);实例化包含秒杀信息和商品信息的对象SeckillSpuVOseckillSpuVOnewSeckillSpuVO();将商品详情信息赋值给同名属性BeanUtils。copyProperties(spuStandardVO,seckillSpuVO);为了防止已有的值被意外覆盖,我们剩下的属性单独赋值赋值秒杀价seckillSpuVO。setSeckillListPrice(seckillSpu。getListPrice());赋值秒杀的开始时间和结束时间seckillSpuVO。setStartTime(seckillSpu。getStartTime());seckillSpuVO。setEndTime(seckillSpu。getEndTime());将包含商品详情和秒杀信息的seckillSpuVO对象保存在循环前定义的集合中seckillSpuVOs。add(seckillSpuVO);}翻页返回查询结果returnJsonPage。restPage(newPageInfo(seckillSpuVOs));}OverridepublicSeckillSpuVOgetSeckillSpu(LongspuId){}OverridepublicSeckillSpuDetailSimpleVOgetSeckillSpuDetail(LongspuId){}}开发控制层 创建controller包 创建SeckillSpuController类RestControllerRequestMapping(seckillspu)Api(tags秒杀Spu模块)publicclassSeckillSpuController{AutowiredprivateISeckillSpuServiceseckillSpuSGetMapping(list)ApiOperation(分页查询秒杀列表Spu信息)ApiImplicitParams({ApiImplicitParam(value页码,namepage,requiredtrue,dataTypeint),ApiImplicitParam(value每页条数,namepageSize,requiredtrue,dataTypeint)})查询秒杀列表不需要登录publicJsonResultJsonPageSeckillSpuVOlistSeckillSpus(Integerpage,IntegerpageSize){JsonPageSeckillSpuVOlistseckillSpuService。listSeckillSpus(page,pageSize);returnJsonResult。ok(list);}} 下面可以测试 NacosRedisSeata 服务需要依次启动 LeafProductpassportseckill 测试10007秒杀端口号 正常配置登录JWT查询秒杀商品的Sku列表(开发持久层) 我们将秒杀的商品Spu列表查询出来 当用户选择一个商品时 我们要将这个商品的sku也查询出来 也就是根据SpuId查询Sku的列表 创建SeckillSkuMapperRepositorypublicinterfaceSeckillSkuMapper{根据SpuId查询Sku列表ListSeckillSkufindSeckillSkusBySpuId(LongspuId);} SeckillSkuMapper。xml文件添加内容sqlidSimpleFieldsiftesttrueid,skuid,spuid,seckillstock,seckillprice,gmtcreate,gmtmodified,seckilllimitifsqlselectidfindSeckillSkusBySpuIdresultTypemall。pojo。seckill。model。SeckillSkuselectincluderefidSimpleFieldsfromseckillskuwherespuid{spuId}select根据时间查询正在进行秒杀的商品(开发持久层) 根据给定时间查询出正在进行秒杀的商品列表 首先保证数据库中的seckillspu表的数据正在秒杀时间段(检查数据,如果不在秒杀时间段,将结束时间后移如2024年) SeckillSpuMapper添加方法根据给定的时间查询正在秒杀的商品ListSeckillSpufindSeckillSpusByTime(LocalDateTimetime); SeckillSpuMapper。xml!根据给定时间查询秒杀商品列表selectidfindSeckillSpusByTimeresultTypemall。pojo。seckill。model。SeckillSpuselectincluderefidSimpleFieldfromseckillspuwherestarttime{time}andendtime{time}select查询所有秒杀商品的SpuId(开发持久层) 这次查询主要是因为后面我们要学习的布隆过滤器,方式缓存穿透使用的 SeckillSpuMapper,添加一个方法查询所有秒杀商品的SpuId数据Long〔〕findAllSeckillSpuIds(); SeckillSpuMapper。xml!查询所有SpuIdselectidfindAllSeckillSpuIdsresultTypelongselectspuidfromseckillspuselect缓存预热思路 我们要使用Quartz调度工具完成任务调度 按照秒杀的批次在秒杀开始前很短的时间内进行进行缓存预热工作 例如每天的12:0014:0016:0018:00进行秒杀 那么就在11:5513:5515:5517:55进行预热 1。我们预热的内容是将参与秒杀商品的sku查询出来,根据skuid将该商品的库存保存在Redis中 2。在秒杀开始后,当有用户查询秒杀商品后,将该商品保存在Redis中,还要注意防止雪崩(有效时间添加随机数) 3(待完善)。在秒杀开始前,生成布隆过滤器,访问时先判断布隆过滤器,如果判断商品存在,再继续访问设置定时任务将库存和随机码保存到Redis 利用Quartz将库存和随机码保存到Redis 1。创建Job接口实现类 2。创建配置类,配置JobDetail和Trigger 在seckill包下创建timer。job包 包中创建SeckillInitialJob类 代码如下Slf4jpublicclassSeckillInitialJobimplementsJob{AutowiredprivateSeckillSpuMapperspuMAutowiredprivateSeckillSkuMapperskuM查询出来的数据要保存到Redis中AutowiredprivateRedisTemplateredisT上面的RedisTemplate对象是向Redis中保存对象用的,内部会将数据序列化后,以二进制的格式保存在Redis中,读写速度确实快,但是数据无法修改这种设计无法在Redis内部完成对象属性或值的修改我们的库存是一个数字,Redis支持直接在Redis内部对数字进行增减,减少java操作而前提是必须保存字符串格式数据,而不是二进制格式我们需要声明一个用字符串操作Redis的对象AutowiredprivateStringRedisTemplatestringRedisTOverridepublicvoidexecute(JobExecutionContextjobExecutionContext)throwsJobExecutionException{我们做的是预热,在秒杀还没有开始的时候,将要开始参与秒杀的商品库存保存到redis这个方法运行时,距离下次秒杀开始还有5分钟所以我们创建一个5分钟之后的时间,查询5分钟后要参与秒杀的商品LocalDateTimetimeLocalDateTime。now()。plusMinutes(5);查询这个时间的所有秒杀商品ListSeckillSpuseckillSpusspuMapper。findSeckillSpusByTime(time);遍历seckillSpus(查询到的所有秒杀商品)目标是将这些商品对应的sku库存保存到Redis为了方便随机数生成定一个对象RandomrannewRandom();for(SeckillSpuspu:seckillSpus){spu是一个商品品类,库存不在spu中,而是sku保存库存所以要根据spuId查询Sku集合ListSeckillSkuseckillSkusskuMapper。findSeckillSkusBySpuId(spu。getSpuId());遍历当前spu的所有sku列表for(SeckillSkusku:seckillSkus){log。info(开始将{}号商品的库存保存到Redis,sku。getSkuId());在编程过程中,涉及RedisKey值时,最好声明常量,如果再业务中使用大量Key值建议创建一个保存RedisKey值常量的类,SeckillCacheUtils类就是mall:seckill:sku:stock:1StringskuStockKeySeckillCacheUtils。getStockKey(sku。getSkuId());检查当前Redis中是否已经包含这个Keyif(redisTemplate。hasKey(skuStockKey)){如果Redis已经包含了这个key(可能是前一场秒杀就包含的商品)log。info({}号商品已经在缓存中,sku。getSkuId());}else{不在缓存中的,就要将库存数据保存到Redis中利用stringRedisTemplate保存,方便减少库存数stringRedisTemplate。boundValueOps(skuStockKey)。set(sku。getSeckillStock(),60604ran。nextInt(6030),TimeUnit。SECONDS);log。info(成功为{}号商品添加库存,sku。getSkuId());}}仍然在遍历所有Spu对象的集合中将当前Spu包含所有sku保存到Redis之后我们要为Spu生成随机码无论任何请求都是访问控制器的路径,秒杀购买商品也是正常情况下我们输入的路径中,包含要购买商品的id即可例如seckillspu,如果这个商品的id已经暴露,那么就可能有人在秒杀未开始前就访问这个路径如果不断访问,数据库就需要反复查询这个商品是否在秒杀时间段内,反复查询数据库影响性能我们要防止这个事情,就要做到秒杀购买商品的路径,平时就不存在而在秒杀开始5分钟前,生成随机码,有了随机码,购买秒杀商品的路径才出现我们的随机码生成后也是保存在Redis中获得随机码keyStringrandomCodeKeySeckillCacheUtils。getRandCodeKey(spu。getSpuId());判断随机码是否已经生成过如果没有生成过再生成if(!redisTemplate。hasKey(randomCodeKey)){生成随机数,随机数越大越不好猜intrandCoderan。nextInt(900000)100000;redisTemplate。boundValueOps(randomCodeKey)。set(randCode,1,TimeUnit。DAYS);log。info(spuId为{}的商品随机码为{},spu。getSpuId(),randCode);}}}}配置Quartz触发 上面的类中的代码只是编写了 我们需要在Quartz中触发才能生效 我们创建time。config包 包中创建QuartzConfig类编写Job的触发ConfigurationSlf4jpublicclassQuartzConfig{声明JobDetail保存到Spring容器BeanpublicJobDetailinitJobDetail(){log。info(预热任务绑定!);returnJobBuilder。newJob(SeckillInitialJob。class)。withIdentity(initSeckill)。storeDurably()。build();}定义Quartz的触发,保存到Spring容器BeanpublicTriggerinitSeckillTrigger(){log。info(预热触发器运行);学习过程中,每分钟加载一次方便测试,实际开发时,根据业务编写正确Cron表达式即可Cron表达式CronScheduleBuildercronScheduleBuilderCronScheduleBuilder。cronSchedule(001?);returnTriggerBuilder。newTrigger()。forJob(initJobDetail())。withIdentity(initSeckillTrigger)。withSchedule(cronScheduleBuilder)。build();}} 启动NacosRedisSeata 项目启动Leafproductseckill显示秒杀商品详情 上面章节我们完成了缓存预热 下面要根据SpuId查询正在秒杀的商品 和普通的SpuId查询商品详情相比 它的业务判断更复杂 1。布隆过滤器判断(后期完成) 2。判断商品是否在Redis中 3。判断要购买的商品是否在秒杀时间段内 4。如果一切正常在返回详情信息前,要为url属性赋值,其实就是固定路径随机码完成根据SpuId查询商品detail详情 在SeckillSpuServiceImpl类中编写新的方法OverridepublicSeckillSpuVOgetSeckillSpu(LongspuId){}常量类中每没有定义Detail用的常量Key,我们声明一个publicstaticfinalStringSECKILLSPUDETAILVOPREFIXseckill:spu:detail:vo:;AutowiredprivateRedisTemplateredisT根据SpuId查询detail详细信息OverridepublicSeckillSpuDetailSimpleVOgetSeckillSpuDetail(LongspuId){将spuId拼接在常量后返回StringseckillDetailKeySECKILLSPUDETAILVOPREFIXspuId;声明返回值类型对象,初值为null即可SeckillSpuDetailSimpleVOseckillSpuDetailSimpleVO判断Redis中是否已经有这个Keyif(redisTemplate。hasKey(seckillDetailKey)){如果Redis中已经有这个key,就获得这个key的值赋值给上面声明的实体类seckillSpuDetailSimpleVO(SeckillSpuDetailSimpleVO)redisTemplate。boundValueOps(seckillDetailKey)。get();}else{如果Redis中没有这个key,我们就需要从数据库中查询后新增到Redis中dubbo调用根据spuId查询detail的方法SpuDetailStandardVOspuDetailStandardVOdubboSeckillSpuService。getSpuDetailById(spuId);判断当前SpuId查询的对象不能为空if(spuDetailStandardVOnull){可以将null保存到Redis中抛出异常thrownewCoolSharkServiceException(ResponseCode。NOTFOUND,您查找的商品不存在);}确认商品存在实例化返回值类型的对象seckillSpuDetailSimpleVOnewSeckillSpuDetailSimpleVO();同名属性赋值BeanUtils。copyProperties(spuDetailStandardVO,seckillSpuDetailSimpleVO);保存到Redis中redisTemplate。boundValueOps(seckillDetailKey)。set(seckillSpuDetailSimpleVO,606024RandomUtils。nextInt(60602),TimeUnit。SECONDS);}返回查询出的对象returnseckillSpuDetailSimpleVO;}根据SpuId查询Detail详情 上次课完成了查询根据SpuId查询Detail详情的业务逻辑层 下面开发控制层开发控制层代码 SeckillSpuController添加方法GetMapping({spuId}detail)ApiOperation(根据SpuId查询Detail详情)ApiImplicitParam(valuespuId,namespuId,requiredtrue,dataTypelong,example1)publicJsonResultSeckillSpuDetailSimpleVOgetSeckillDetail(PathVariableLongspuId){SeckillSpuDetailSimpleVOdetailSimpleVOseckillSpuService。getSeckillSpuDetail(spuId);returnJsonResult。ok(detailSimpleVO);} 启动测试 NacosSeataRedis LeafProductSeckillpassport http:localhost:10007doc。html测试根据spuId查询Detail的功能根据SpuId查询秒杀商品详情 1。布隆过滤器判断(后期完成) 2。判断商品是否在Redis中 3。判断要购买的商品是否在秒杀时间段内 4。如果一切正常在返回详情信息前,要为url属性赋值,其实就是固定路径随机码开发持久层 SeckillSpuMapper添加方法根据SpuId查询秒杀Spu实体SeckillSpufindSeckillSpuById(LongspuId); SeckillSpuMapper。xml添加内容!根据SpuId查询秒杀Spu实体selectidfindSeckillSpuByIdresultMapBaseResultMapselectincluderefidSimpleFieldfromseckillspuwherespuid{spuId}select开发业务逻辑层 SeckillSpuServiceImpl业务逻辑层实现类根据SpuId查询Spu详情OverridepublicSeckillSpuVOgetSeckillSpu(LongspuId){先判断参数spuId是否在布隆过滤器中如果不在直接返回抛出异常(后期会实现)声明一个返回值用于返回SeckillSpuVOseckillSpuVO获取当前SpuId对应的Redis的Keymall:seckill:spu:vo:1StringseckillSpuKeySeckillCacheUtils。getSeckillSpuVOKey(spuId);执行判断这个Key是否已经保存在Redis中if(redisTemplate。hasKey(seckillSpuKey)){如果在redis中,直接获取seckillSpuVO(SeckillSpuVO)redisTemplate。boundValueOps(seckillSpuKey)。get();}else{如果Redis中没有个这个Key先按spuId查询秒杀spu信息SeckillSpuseckillSpuseckillSpuMapper。findSeckillSpuById(spuId);当前商品是否存在if(seckillSpunull){thrownewCoolSharkServiceException(ResponseCode。NOTFOUND,您访问的商品不存在);}上面SeckillSpu对象只有秒杀信息,没有商品信息商品信息需要根据Dubbo来查询pms数据库SpuStandardVOspuStandardVOdubboSeckillSpuService。getSpuById(spuId);将商品详情赋值给seckillSpuVO,先实例化seckillSpuVOnewSeckillSpuVO();BeanUtils。copyProperties(spuStandardVO,seckillSpuVO);秒杀信息手动赋值seckillSpuVO。setSeckillListPrice(seckillSpu。getListPrice());seckillSpuVO。setStartTime(seckillSpu。getStartTime());seckillSpuVO。setEndTime(seckillSpu。getEndTime());查询出的对象保存在Redis中redisTemplate。boundValueOps(seckillSpuKey)。set(seckillSpuVO,6060241000RandomUtils。nextInt(100060602),TimeUnit。MILLISECONDS);}判断当前Spu是否在秒杀时间段内获得当前时间LocalDateTimenowTimeLocalDateTime。now();判断当前时间是否在秒杀开始之后between比较两个时间参数前大后小返回negativeDurationafterTimeDuration。between(nowTime,seckillSpuVO。getStartTime());判断当前时间是否在秒杀结束之前DurationbeforeTimeDuration。between(seckillSpuVO。getEndTime(),nowTime);判断两个Duration是否同时为negative,如果是,表示当前时间确实再当前spu商品的秒杀时间段内if(afterTime。isNegative()beforeTime。isNegative()){从Redis中获得随机码赋值给seckillSpuVO的url属性StringrandCodeKeySeckillCacheUtils。getRandCodeKey(spuId);seckillSpuVO。setUrl(seckillredisTemplate。boundValueOps(randCodeKey)。get());}别忘了返回前端根据seckillSpuVO对象的url属性是否为null判断是否可以进行秒杀购买操作如果为空,提示无法购买,如果有值并赋值了随机码,就可以进行下一步提交操作returnseckillSpuVO;}开发控制层 SeckillSpuController添加方法GetMapping({spuId})ApiOperation(根据SpuId查询秒杀Spu详情)ApiImplicitParam(valuespuId,namespuId,requiredtrue,dataTypelong,example2)publicJsonResultSeckillSpuVOgetSeckillSpuVO(PathVariableLongspuId){SeckillSpuVOseckillSpuVOseckillSpuService。getSeckillSpu(spuId);returnJsonResult。ok(seckillSpuVO);} 重启Seckill模块 测试10007端口功能根据SpuId查询秒杀Sku列表 之前编写加载数据的Mapper时,完成了根据SpuId查Sku列表的功能 下面我们从业务逻辑层开始编写开发业务逻辑层 我们也需要讲SpuId对应的Sku信息保存到Redis 在service。impl包中创建SeckillSkuServiceImpl类中编写代码如下ServiceSlf4jpublicclassSeckillSkuServiceImplimplementsISeckillSkuService{AutowiredprivateSeckillSkuMapperskuMDubbo查询sku详细信息的生产者DubboReferenceprivateIForSeckillSkuServicedubboSkuS保存到Redis的支持AutowiredprivateRedisTemplateredisT根据SpuId查询秒杀Sku列表OverridepublicListSeckillSkuVOlistSeckillSkus(LongspuId){调用根据spuId查询所有Sku列表的方法ListSeckillSkuseckillSkusskuMapper。findSeckillSkusBySpuId(spuId);声明包含sku详情和秒杀信息类型泛型的ListListSeckillSkuVOseckillSkuVOsnewArrayList();遍历数据库查询出来的所有sku列表for(SeckillSkusku:seckillSkus){声明既包含秒杀信息,又包含详情的sku对象SeckillSkuVOseckillSkuVO获取skuId保存在变量中LongskuIdsku。getSkuId();在检查Redis是否包含对象前,先准备KeyStringseckillSkuVOKeySeckillCacheUtils。getSeckillSkuVOKey(skuId);判断Redis是否包含Keyif(redisTemplate。hasKey(seckillSkuVOKey)){seckillSkuVO(SeckillSkuVO)redisTemplate。boundValueOps(seckillSkuVOKey)。get();}else{Redis中没有这个key,需要我们从数据库查询后,保存在Redis中Dubbo根据skuId查询sku详情信息SkuStandardVOskuStandardVOdubboSkuService。getById(skuId);将seckillSkuVO实例化后,将详情赋值给它seckillSkuVOnewSeckillSkuVO();BeanUtils。copyProperties(skuStandardVO,seckillSkuVO);将秒杀信息也赋值给seckillSkuVO,这样它就包含详情信息和秒杀信息了seckillSkuVO。setStock(sku。getSeckillStock());seckillSkuVO。setSeckillPrice(sku。getSeckillPrice());seckillSkuVO。setSeckillLimit(sku。getSeckillLimit());将对象保存到Redis中redisTemplate。boundValueOps(seckillSkuVOKey)。set(seckillSkuVO,6060241000RandomUtils。nextInt(260601000),TimeUnit。MILLISECONDS);}seckillSkuVOs。add(seckillSkuVO);}别忘了返回!!returnseckillSkuVOs;}}开发控制层 新建SeckillSkuController添加方法RestControllerRequestMapping(seckillsku)Api(tags秒杀sku模块)publicclassSeckillSkuController{AutowiredprivateISeckillSkuServiceseckillSkuSGetMapping(list{spuId})ApiOperation(根据SpuId查询秒杀Sku列表)ApiImplicitParam(valuespuId,namespuId,requiredtrue,dataTypelong,example2)publicJsonResultListSeckillSkuVOlistSeckillSkus(PathVariableLongspuId){ListSeckillSkuVOlistseckillSkuService。listSeckillSkus(spuId);returnJsonResult。ok(list);}}准备流控和降级的处理类 Sentinel是阿里提供的SpringCloud组件,主要用于限制外界访问当前服务器的控制器方法 之前的课程中,我们已经比较详细的学习的Sentinel使用的方式 下面我们要先编写Sentinel限流和服务降级时,运行的自定义异常处理类 我们酷鲨前台项目seckillwebapi模块 创建一个exception包,包中新建SeckillBlockHandler代码如下秒杀执行业务限流异常处理类Slf4jpublicclassSeckillBlockHandler{声明的限流方法,返回值必须和控制器方法一致参数是包含控制器方法参数前提下,额外添加BlockException异常参数这个方法我们定义为静态方法,方便调用者不实例化对象,直接用类名调用publicstaticJsonResultseckillBlock(StringrandCode,SeckillOrderAddDTOseckillOrderAddDTO,BlockExceptione){log。error(请求被限流);returnJsonResult。failed(ResponseCode。INTERNALSERVERERROR,服务器忙);}} 再创建降级类SeckillFallBackSentinel秒杀降级方法Slf4jpublicclassSeckillFallBack{参数和返回值的要求基本和秒杀限流方法一致只是降级方法是因为控制器发生了异常才会运行的,我们使用Throwable类型类接收publicstaticJsonResultseckillFall(StringrandCode,SeckillOrderAddDTOseckillOrderAddDTO,Throwablethrowable){log。error(秒杀业务降级);returnJsonResult。failed(ResponseCode。INTERNALSERVERERROR,throwable。getMessage());}}提交秒杀订单开发业务逻辑层 我们之前完成了缓存预热的准备工作 用户也可以在秒杀开始后访问当前的商品信息了 如果用户选择商品规格(sku)提交订单,那么就要按照提交秒杀订单的业务流程处理 秒杀提交订单和普通订单的区别 1。从Redis中判断是否有库存 2。要判断当前用户是否为重复购买 3。秒杀订单转换成普通订单,需要使用dubbo在order模块完成 4。用消息队列(RabbitMQ)的方式将秒杀成功信息保存在success表中 创建一个SeckillServiceImpl业务逻辑层实现类,完成上面的业务ServiceSlf4jpublicclassSeckillServiceImplimplementsISeckillService{需要普通订单生成的方法,dubbo调用DubboReferenceprivateIOmsOrderServicedubboOrderS减少Redis库存,是操作Redis字符串类型的数据AutowiredprivateStringRedisTemplatestringRedisT秒杀订单提交成功,需要发送到消息队列后续处理AutowiredprivateRabbitTemplaterabbitT1。从Redis中判断是否有库存2。要判断当前用户是否为重复购买3。秒杀订单转换成普通订单,需要使用dubbo在order模块完成4。用消息队列(RabbitMQ)的方式将秒杀成功信息保存在success表中OverridepublicSeckillCommitVOcommitSeckill(SeckillOrderAddDTOseckillOrderAddDTO){第一阶段从Redis中判断是否有库存和检查重复购买防止超卖:Redis中预热了sku的库存,判断用户请求的skuid是否有库存如果有可以购买,如果没有阻止购买LongskuIdseckillOrderAddDTO。getSeckillOrderItemAddDTO()。getSkuId();获得秒杀用户的IdLonguserIdgetUserId();获得skuId和userId就能明确知道谁买了什么商品秒杀业务规定,一个用户只能购买一个sku一次我们将该用户和sku的购买关系保存到Redis中,以防止重复购买生成用户和此sku关联的keyStringreSeckillKeySeckillCacheUtils。getReseckillCheckKey(skuId,userId);mall:seckill:reseckill:2:1向这个key中保存一个数据(一般保存一个1表示购买了一次),可以使用自增的方式increment()我们调用这个方法实现两个功能1。如果key为空,会自动将这个key保存值为12。如果key不为空,会自增当前的值122334最后将这个数值返回给seckillTimesLongseckillTimesstringRedisTemplate。boundValueOps(reSeckillKey)。increment();if(seckillTimes1){购买次数超过1,证明不是第一次购买,抛出异常,终止业务thrownewCoolSharkServiceException(ResponseCode。FORBIDDEN,您已经购买过该商品);}运行到此处证明用户符合购买资格(之前没买过)下面检查是否有库存,先确定库存的KeyStringseckillSkuCountKeySeckillCacheUtils。getStockKey(skuId);从Redis中获得库存调用decrement()方法,将库存数减1,返回的数值,就是减1后的库存数LongleftStockstringRedisTemplate。boundValueOps(seckillSkuCountKey)。decrement(1);leftStock表示当前用户购买完之后剩余的库存数如果是0表示次用户购买完库存为0,所以只有返回值小于0时才是没有库存了if(leftStock0){thrownewCoolSharkServiceException(ResponseCode。NOTACCEPTABLE,对不起您购买的商品已经无货);}运行到此处,表示用户可以生成订单,进入第二阶段第二阶段开始生成订单秒杀订单转换成普通订单我们现获得的秒杀订单参数seckillOrderAddDTO这个参数信息也是前端收集并发送到后端的,它的信息量和普通订单发送的内容基本相同我们可以直接dubbo调用order模块新订单的业务来完成通过调用转换方法将seckillOrderAddDTO转换为OrderAddDTO类型对象OrderAddDTOorderAddDTOconvertSeckillOrderToOrder(seckillOrderAddDTO);转换过程中没有UserId需要手动赋值orderAddDTO。setUserId(userId);信息全了,发送Dubbo请求调用Order模块新增订单OrderAddVOorderAddVOdubboOrderService。addOrder(orderAddDTO);订单生成完毕下面进入第三阶段第三阶段消息队列(RabbitMQ)发送消息我们的目的是将购买成功的信息保存到success表中但是这个业务并不需要立即完成,可以将它发送给消息队列,异步完成,减轻当前业务的运行压力SuccesssuccessnewSuccess();Success主要保存秒杀成功的信息,或者描述为秒杀卖出的商品具体到商品是Sku信息,所以它的属性和SeckillOrderItemAddDTO更相似所以用SeckillOrderItemAddDTO给Success同名属性赋值BeanUtils。copyProperties(seckillOrderAddDTO。getSeckillOrderItemAddDTO(),success);缺少的其他属性后面赋值success。setUserId(userId);success。setOrderSn(orderAddVO。getSn());Success信息收集完成,将消息发送给RabbitMQrabbitTemplate。convertAndSend(RabbitMqComponentConfiguration。SECKILLEX,RabbitMqComponentConfiguration。SECKILLRK,success);消息队列会处理后续的操作最终返回值SeckillCommitVO和OrderAddVO属性一致,实例化后赋值返回即可SeckillCommitVOseckillCommitVOnewSeckillCommitVO();BeanUtils。copyProperties(orderAddVO,seckillCommitVO);returnseckillCommitVO;}将秒杀订单转换为普通订单的方法privateOrderAddDTOconvertSeckillOrderToOrder(SeckillOrderAddDTOseckillOrderAddDTO){实例化OrderAddDTOOrderAddDTOorderAddDTOnewOrderAddDTO();先将属性一致的值赋值到orderAddDTOBeanUtils。copyProperties(seckillOrderAddDTO,orderAddDTO);SeckillOrderAddDTO秒杀订单中只可能对应一个OrderItem对象但是普通订单OrderAddDTO对象中可能包含多个OrderItem对象所以OrderAddDTO对象中的OrderItem是一个List,而秒杀订单是单个对象我们需要讲秒杀订单的单个对象添加到OrderAddDTO对象中OrderItem的集合里实例化OrderItemAddDTOOrderItemAddDTOorderItemAddDTOnewOrderItemAddDTO();将SeckillOrderItemAddDTO同名属性赋值给orderItemAddDTOBeanUtils。copyProperties(seckillOrderAddDTO。getSeckillOrderItemAddDTO(),orderItemAddDTO);要想将赋好值的orderItemAddDTO对象添加到orderAddDTO的集合中需要先实例化这个类型的集合ListOrderItemAddDTOorderItemAddDTOsnewArrayList();将orderItemAddDTO添加到集合中orderItemAddDTOs。add(orderItemAddDTO);再将集合赋值到orderAddDTO对象中orderAddDTO。setOrderItems(orderItemAddDTOs);最终返回转换完成的orderAddDTO对象returnorderAddDTO;}publicCsmallAuthenticationInfogetUserInfo(){获得SpringSecurity容器对象UsernamePasswordAuthenticationTokenauthenticationToken(UsernamePasswordAuthenticationToken)SecurityContextHolder。getContext()。getAuthentication();判断获取的容器信息是否为空if(authenticationToken!null){如果容器中有内容,证明当前容器中有登录用户信息我们获取这个用户信息并返回CsmallAuthenticationInfocsmallAuthenticationInfo(CsmallAuthenticationInfo)authenticationToken。getCredentials();returncsmallAuthenticationI}thrownewCoolSharkServiceException(ResponseCode。UNAUTHORIZED,没有登录信息);}业务逻辑层中大多数方法都是获得用户id,所以编写一个返回用户id的方法publicLonggetUserId(){returngetUserInfo()。getId();}} redis储存开发控制层 随机码判断逻辑 随机码 controller包下创建SeckillController 代码如下RestControllerRequestMapping(seckill)Api(tags提交秒杀模块)publicclassSeckillController{AutowiredprivateISeckillServiceseckillSAutowiredprivateRedisTemplateredisTPostMapping({randCode})ApiOperation(验证随机码并提交秒杀订单)ApiImplicitParam(value随机码,namerandCode,requiredtrue,dataTypestring)PreAuthorize(hasRole(ROLEuser))Sentinel限流和降级的配置SentinelResource(valueseckill,blockHandlerClassSeckillBlockHandler。class,blockHandlerseckillBlock,fallbackClassSeckillFallBack。class,fallbackseckillFall)publicJsonResultSeckillCommitVOcommitSeckill(PathVariableStringrandCode,SeckillOrderAddDTOseckillOrderAddDTO){先获取spuIdLongspuIdseckillOrderAddDTO。getSpuId();确定spuId对应随机码的keyStringrandCodeKeySeckillCacheUtils。getRandCodeKey(spuId);判断Redis中是否有randCodeKeyif(redisTemplate。hasKey(randCodeKey)){如果有这个Key判断随机码是否正确获取随机码StringredisRandCoderedisTemplate。boundValueOps(randCodeKey)。get();判断redisRandCode是否为nullif(redisRandCodenull){Redis中随机码丢失,服务器内部错误thrownewCoolSharkServiceException(ResponseCode。INTERNALSERVERERROR,服务器内部异常,请联系管理员);}判断Redis的随机码和参数随机码是否一致,防止投机购买if(!redisRandCode。equals(randCode)){如果不一致,认为是投机购买,抛出异常thrownewCoolSharkServiceException(ResponseCode。NOTFOUND,没有这个商品);}调用业务逻辑层提交订单SeckillCommitVOseckillCommitVOseckillService。commitSeckill(seckillOrderAddDTO);returnJsonResult。ok(seckillCommitVO);}else{没有Key对应随机码,秒杀列表中没有这个商品thrownewCoolSharkServiceException(ResponseCode。NOTFOUND,没有指定商品);}}} 为了knife4j测试顺利,我们在SeckillInitialJob类的最后位置输出一下spuId为2的随机码用于测试 修改后代码如下if(!redisTemplate。hasKey(randomCodeKey)){生成随机数,随机数越大越不好猜intrandCoderan。nextInt(900000)100000;redisTemplate。boundValueOps(randomCodeKey)。set(randCode,1,TimeUnit。DAYS);log。info(spuId为{}的商品随机码为{},spu。getSpuId(),randCode);}else{输出spuId对应的随机码用于测试StringcoderedisTemplate。boundValueOps(randomCodeKey)。get();log。info(spuId为{}的商品随机码为{},spu。getSpuId(),code);} 启动NacosSeataRedisSentinelRabbitMQ 项目Leafproductpassportorderseckill 注意yml配置文件中的RabbitMQ的用户名和密码 如果说已经购买过,就修改允许购买的数量1100 如果说没有库存,可以吧判断库存的if注释掉 测试成功即可 还可以测试sentinel的限流success成功信息的处理 我们再上次课提交秒杀信息业务最后 向RabbitMQ队列中,输出了添加秒杀成功信息的消息 但是我们没有任何处理 将秒杀成功信息发送到消息队列的原因: 秒杀成功信息用于统计秒杀数据,是秒杀结束后才需要统计的 所以在秒杀并发高时,消息队列的发送可以延缓,在服务器不忙时,再运行(削峰填谷)开发持久层 秒杀数据库中有success表 其中的信息就是保存秒杀成功的数据(userId,skuId等) 我们要连接数据库,对这个表进行新增 还有对秒杀数据库sku库存的修改 SeckillSkuMapper接口中添加方法来修改指定skuId的库存数根据skuId修改库存数voidupdateReduceStockBySkuId(Param(skuId)LongskuId,Param(quantity)Integerquantity); SeckillSkuMapper。xml!根据SkuId修改数量updateidupdateReduceStockBySkuIdupdateseckillskusetseckillstockseckillstock{quantity}whereskuid{skuId}update 下面编写新增Success的方法RepositorypublicinterfaceSuccessMapper{新增Success对象到数据库的方法voidsaveSuccess(Successsuccess);} SuccessMapper。xmlinsertidsaveSuccessinsertintosuccess(userid,userphone,skuid,title,mainpicture,seckillprice,quantity,barcode,data,ordersn)values({userId},{userPhone},{skuId},{title},{mainPicture},{seckillPrice},{quantity},{barCode},{data},{orderSn})insert开发消息的接收 我们当前触发新增Success的方法并不是常规的业务逻辑层 而是由RabbitMQ消息收发机制中接收消息的对象来调用 所有我们编写一个接收消息的监听器类来完成这个操作 创建consumer包,包中创建类SekillQueueConsumer代码如下必须交由Spring容器管理ComponentRabbitMQ监听器声明RabbitListener(queues{RabbitMqComponentConfiguration。SECKILLQUEUE})publicclassSeckillQueueConsumer{将业务需要的对象都装配过来AutowiredprivateSuccessMappersuccessMAutowiredprivateSeckillSkuMapperskuM编写监听队列调用的方法保证方法的参数和发送时参数类型一致RabbitHandlerpublicvoidprocess(Successsuccess){扣库存扣库存操作不会在并发高时和数据库同步,只会在服务器较闲时,影响数据库真正的实时库存保存在Redis中skuMapper。updateReduceStockBySkuId(success。getSkuId(),success。getQuantity());新增successsuccessMapper。saveSuccess(success);如果上面方法有失败情况需要在下面发送异常消息,可能发送给秒杀模块处理也可以直接发送给死信队列,让人工处理}} 环境方面 NacosSentinelSeataredisRabbitMQ 服务方面 Leafproductorderpassportseckill 学习记录,如有侵权请联系删除