当前位置: 首页 > 科技观察

基于代码实践SpringBoot、Redis、LUA秒杀系统_0

时间:2023-03-20 22:53:19 科技观察

前言学过redis基础的都学的差不多了,但是没有做过具体的项目实践。可以看这篇文章做一个项目来巩固知识。相关要求及说明总体来说,秒杀系统的功能并不多,主要包括:制定秒杀计划。某天几点开始,卖什么产品,卖多少,持续多久。显示闪购列表。一般是当天陈列,有的8点卖,有的10点卖。商品详情页。下单购买。等等等等。本文主要目的是用代码实现防止商品超卖的功能,所以制定秒杀计划、展示商品等功能不会重写。也有电商产品主要加工SPU(比如iPhone12、iPhone11是两个SPU)和SKU(比如iPhone1264G白色、iPhone12128G黑色是两个SKU)。显示SPU,采购扣除库存后使用的SKU。为了方便,本文直接将其替换为product。下单也有一些前置条件,比如通过风控系统确认你是否是黄牛;营销系统,是否有相关的优惠券、虚拟货币等。下单后还有仓管、物流、积分,本文不做赘述。这篇文章不涉及数据库,一切都是在Redis上操作的,但是我还是想说一下数据库和缓存数据的一致性。如果我们系统的并发不高,数据库可以支持的话,我们可以直接操作数据库。为了防止超卖,我们可以使用:悲观锁select*fromSKUtablewheresku_id=1forupdate;乐观锁updateSKU表setstock=stock-1wheresku_id=1andupdate_version=旧版本号;如果并发度较高,比如商品详情页一般并发度最高。为了减轻数据库的压力,使用了Redis等缓存。为了保证数据库与Redis的一致性,大多采用“修改后删除”的方案。但是在这种方案并发较高的情况下,比如C10K,C10M等,在修改数据库,删除Redis内容的瞬间,大量的并发查询会传到数据库,导致在一个例外。在这种情况下,SPUdetails的接口一定不能连接到数据库。步骤应该是:B端管理系统操作数据库(这个并发不会高)。存储数据后,向MQ发送消息。相关处理程序收到订阅的MQTopic后,从数据库中取出信息放入Redis。相关服务接口只从Redis取数据。代码实现在实际项目中,建议将ToC端的秒杀产品相关接口组合成一个微服务,product-server。销售接口被组合成一个微服务,order-server。编码可以参考之前的SpringCloud系列文章。本文简单使用了一个SpringBoot项目。秒杀计划实体类省略*/privateLongprice;/***Dashlinepriceunit:cents*/privateLonglinePrice;/***stocknumber*/privateLongstock;/***用户只购买了一件商品,标识符为0No1Yes*/privateintbuyOneFlag;/***计划状态0未提交,1已提交*/privateintplanStatus;/***开始时间*/privateDatestartTime;/***结束时间*/privateDateendTime;/***创建时间*/privateDatecreateTime;}说明:如上所述,限时抢购商品应显示SPU,销售扣减库存应为SKU。为方便起见,本文仅使用product代替。用户购买闪购产品有两种方式:一个用户只能购买一件商品。用户可以多次购买多个项目。所以这个类使用buyOneFlag作为标识。planStatus表示秒杀是否真正执行。0不会显示给C端,也不会出售;1会显示到C端并出售。添加秒杀计划&查询秒杀计划@RestControllerpublicclassProductController{@ResourceprivateRedisTemplateredisTemplate;//随机生成秒杀计划并设置到Redis@GetMapping("/addSecKillPlan")@ResponseBodypublicDefaultResult>addSecKillPlan(@RequestParam("saledate")StringsaleDate){DateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ss");Randomrand=newRandom();Gsongson=newGson();Listlist=Lists.newArrayList();for(inti=0;i<10;i++){longproductId=rand.nextInt(100)+1;longprice=rand.nextInt(100)+1;longstock=rand.nextInt(100)+1;StringsaleStartTime="10:00:00";StringsaleEndTime="12:00:00";intbuyOneFlag=0;if(i>4){saleStartTime="14:00:00";saleEndTime="16:00:00";buyOneFlag=1;}SecKillPlanEntityentity=newSecKillPlanEntity();entity.setId(i+1L);entity.setProductId(productId);entity.setProductName("商品"+productId);entity.setBuyOneFlag(buyOneFlag);entity.setLinePrice(999999L);entity.setPlanStatus(1);entity.setPrice(价格*100);entity.setStock(股票);entity.setEndTime(Date.from(LocalDateTime.parse(saleDate+saleEndTime),dtf).atZone(ZoneId.systemDefault()).toInstant()));entity.setStartTime(Date.from(LocalDateTime.parse(saleDate+saleStartTime,dtf).atZone(ZoneId.systemDefault()).toInstant()));entity.setCreateTime(newDate());//将商品详情写入RedisValueOperationssetProduct=redisTemplate.opsForValue();setProduct.set("product_"+productId,gson.toJson(entity));//写入库存if(buyOneFlag==1){//一个用户只购买了一个产品//产品购买用户SetredisTemplate.opsForSet().add("product_buyers_"+productId,"");//产品库存为(intj=0;j>findSecKillPlanByDate(@RequestParam("saledate")StringsaleDate){Gsongson=newGson();StringplanJson=redisTemplate.opsForValue().get("seckill_plan_"+saleDate);列表list=gson.fromJson(planJson,newTypeToken>(){}.getType());//设置新的库for(SecKillPlanEntity:list){if(entity.getBuyOneFlag()==1){longnewStock=redisTemplate.opsForList().size("product_one_stock_"+entity.getProductId());entity.setStock(newStock);}else{longnewStock=Long.parseLong(redisTemplate.opsForValue().get("product_stock_"+entity.getProductId()));实体。setStock(newStock);}}returnDefaultResult.success(list);}}说明:addSecKillPlan就是随机生成10个销售计划,只有卖一件或卖多件并将相关数据推送到Redis。seckill_plan_date,代表某一天的所有秒杀计划,用于列表显示。product_productID,代表一个商品信息,用于详情页。product_one_stock_ProductID,表示只卖一种商品的存货数量,取值为List,有存货多少就往里面塞多少个“1”。product_buyers_产品ID,代表只销售一种产品的买家,已经购买过的用户不允许再次购买。product_stock_productID,表示可销售的多个商品的库存数量,值为库存数量。findSecKillPlanByDate,显示某天的秒杀销售计划。从与库存相关的两个KEY中获取库存编号。LUA脚本只卖一个buyone.lua:--商品库存Keyproduct_one_stock_XXXlocalstockKey=KEYS[1]--商品购买用户记录Keyproduct_buyers_XXXlocalbuyersKey=KEYS[2]--用户IDlocaluid=KEYS[3]--查询用户是否购买localresult=redis.call("sadd",buyersKey,uid)if(tonumber(result)==1)then--没有购买,可以购买localstock=redis.call("lpop",stockKey)--除了nil和false,其他值为真(包括0)if(stock)then--有货退货1else--缺货退货-1endelse-已购买退货-3end可卖多件buymore.lua:--productKeylocalkey=KEYS[1]--购买数量localval=ARGV[1]--现有总库存localstock=redis.call("GET",key)if(tonumber(stock)<=0)then--无库存返回-1else--得到扣除后的总库存=总库存-购买数量localdecrstock=redis.call("DECRBY",key,val)if(tonumber(decrstock)>=0)then--扣除购买数量后没有超卖,返回当前库存returndecrstockelse--超卖,把扣掉的加回来redis.call("INCRBY",key,val)return-2endend注:1.只卖一件。首先使用命令“sadd”将买家ID输入product_buyers_productID。如果返回1,表示用户之前没有购买过,否则返回-3,表示已经购买过。lpopoutproduct_one_stock_productID中的值,如果还有存货,则返回1,有存货,否则为nil,无存货。2.可以卖多件。前面已经说过了,不再赘述。将这两个lua文件放在SpringBoot项目的resources目录下。销售接口@RestControllerpublicclassOrderController{@ResourceprivateRedisTemplateredisTemplate;@GetMapping("/addOrder")@ResponseBodypublicDefaultResultaddOrder(@RequestParam("uid")longuserId,@RequestParam("pid")longproductId,@RequestParam("quantity")intquantity){Gsongson=newGson();StringproductJson=redisTemplate.opsForValue().get("product_"+productId);SecKillPlanEntityentity=gson.fromJson(productJson,SecKillPlanEntity.class);//TODO验证销售是否plan已提交,是否到销售时间longcode=0;if(entity.getBuyOneFlag()==1){//用户只购买一件商品code=this.buyOne("product_one_stock_"+productId,"product_buyers_"+productId,userId);}else{//用户购买多件商品code=this.buyMore("product_stock_"+productId,quantity);}DefaultResultresult=DefaultResult.success(null);//错误码处理应该使用ENUM,本文省去if(code<0){result.setCode(code);if(code==-1){result.setMsg("Nostock");}elseif(code==-2){结果。设置消息("库存不足");}elseif(code==-3){result.setMsg("已购买过");}}returnresult;}privateLongbuyOne(StringstockKey,StringbuysKey,longuserId){DefaultRedisScriptdefaultRedisScript=newDefaultRedisScript();defaultRedisScript.setResultType(Long.class);defaultRedisScript.setScriptSource(newResourceScriptSource(newClassPathResource("buyone.lua")));//"{pre}:"Listkeys=Lists.newArrayList(stockKey,buysKey,userId+"");Longresult=redisTemplate.execute(defaultRedisScript,keys,"");returnresult;}privateLongbuyMore(StringstockKey,intquantity){DefaultRedisScriptdefaultRedisScript=newDefaultRedisScript();defaultRedisScript.setResultType(Long.class);defaultRedisScript.setScriptSource(newResourceScriptSource(newClassPathResource("buymore.lua")));Listkeys=Lists.newArrayList(stockKey);Longresult=redisTemplate.execute(defaultRedisScript,keys,quantity+"");returnresult;}}说明:1.主要看buyOne和buyMore这两个私有方法,描述了如何使用RedisTemplate执行lua脚本。另外,我看到一些资料,如果使用Redis集群,会报错。因为我没有Redis集群环境,所以无法测试。如果你有环境,你可以试试。2.为了节省时间,addOrder中有些代码写的很low。比如有些checksums没有加,错误码应该用ENUM。测试用例:用户只购买一件商品1,成功。用户再次只购买一件商品1并失败。N个用户只购买了一件商品1,库存不足。某用户购买多件可售出的商品2,成功。某用户购买了多件可售商品2,库存不足。