当前位置: 首页 > 后端技术 > Java

SpringBoot+Redis:抗十万人,秒杀抢单!

时间:2023-04-02 01:52:05 Java

本文内容主要讲解redis分布式锁,几乎是各大厂面试必备的。下面结合模拟抢单场景使用;本文未涉及的redis环境搭建,快速搭建个人测试环境,这里推荐使用docker;本文内容节点如下:如何删除Jedis的NX生成锁模拟抢单动作(10w个人开抢)Jedis的NX生成锁是Java操作redis的好方法。使用jedis首先要在pom中引入依赖:redis.clientsjedis对于分布式锁的生成,通常需要注意以下几个方面:SpringBoot基础的就不介绍了。推荐这个实用教程:https://github.com/javastacks...创建锁的策略:普通的rediskey一般允许覆盖。用户A设置了一把钥匙后,B在设置的同一个场景如果是锁场景,无法知道是哪个用户成功设置了钥匙;这里jedis的setnx方法为我们解决了这个问题。简单的原理就是:当用户A先设置成功,然后B用户设置时,返回失败,并且在某个时间点只允许一个用户获得锁。锁过期时间:在抢购场景下,如果没有过期的概念,当用户A产生了锁,但是后续进程阻塞,无法释放锁,那么其他用户总是会在获取锁失败本次且无法完成抢购活动;当然一般情况下是不会阻塞的,A用户进程会正常释放锁;过期时间只是为了更安全。下面来上段setnx操作的代码:publicbooleansetnx(Stringkey,Stringval){Jedisjedis=null;尝试{jedis=jedisPool.getResource();如果(jedis==null){返回假;}返回jedis.set(键、值、“nx”、“px”、1000*60)。EqualSignorecase("OK");}Catch(ExceptionEx){}最后{if(jedis!=NULL){jedis.close();}}returnfalse;}这里需要注意的是jedis的set方法,其参数解释如下:NX:如果有key,则不会设置成功PX:key过期时间的单位设置为毫秒(EX:inseconds)如果setnx失败,直接封装返回false。接下来,我们通过getAPI调用setnx方法:@GetMapping("/setnx/{key}/{val}")publicbooleansetnx(@PathVariableStringkey,@PathVariableStringval){returnjedisCom.setnx(key,val);}访问以下测试url。通常,它第一次返回true,第二次返回false。由于第二次请求中已经存在rediskey,所以无法设置成功。从上图我们可以看出只有一个set成功了,而且key有有效时间,此时已经达到了分布式锁的条件。如何删除锁上面是创建锁,锁也是有有效时间的,但是我们不能完全依赖这个有效时间。例如设置有效时间为1分钟。用户A获得锁后,没有遇到特殊情况,正常生成抢购订单。之后,此时其他用户应该可以正常下单,但是由于锁只能在1分钟后自动释放,所以在这1分钟内其他用户无法正常下单(因为锁还是用户A的),所以我们需要在A用户操作后,主动解锁:PublicIntDelnx(StringKey,StringVal){jedisjedis=null;试试{jedis=jedispool.getResource();if(jedis==null){return0;}///ifredis.call('get','orderkey')=='1111'thenreturnredis.call('del','orderkey')elsereturn0结束StringBuildersbScript=newStringBuilder();sbScript.append("ifredis.call('get','").append(key).append("')").append("=='").append(val).append("'").append("then")..end");returnInteger.valueOf(jedis.eval(sbScript.toString()).toString();}catch(ExceptionEX){}最后{if(jedis!=Null){jedis.close();}return0;}这里也是用JEDIS方法直接执行LUA脚本:判断是否判断是否存在,如果存在,del;其实我个人认为,通过jedis的get方法获取到val之后,再比较这个值是否是当前持有锁的用户,如果是,那么在最后删除,效果其实是一样的;直接通过eval执行脚本即可,避免再次操作redis,缩短原子操作的间隔(如有不同意见欢迎留言讨论);还创建一个获取API来测试:@GetMapping("/delnx/{key}/{val}")publicintdelnx(@PathVariableStringkey,@PathVariableStringval){returnjedisCom.delnx(key,val);}注意,使用delnx时,需要传递创建锁时的值,因为et的值和delnx的值是用来判断是否是操作请求持有锁的,只是值相同只允许删除;模拟抢单动作(10万人开始抢单)有了上面分布式锁的粗略基础,我们模拟10万人抢单的场景,其实只是一个并发的操作请求。由于环境有限,我们只能这样测试;如下初始化100000个用户,并初始化库存、商品等信息,如下代码://totalinventoryprivatelongnKuCuen=0;//商品键名privateStringshangpingKey="computer_key";//获取锁超时秒数privateinttimeout=30*1000;@GetMapping("/qiangdan")publicListqiangdan(){//抢到商品的用户ListshopUsers=newArrayList<>();//多次构造UsersListusers=newArrayList<>();IntStream.range(0,100000).parallel().forEach(b->{users.add("神牛-"+b);});//初始化库存nKuCuen=10;//模拟开抢users.parallelStream().forEach(b->{StringshopUser=qiang(b);如果(!StringUtils.isEmpty(shopUser)){shopUsers.add(shopUser);商品}});以上10w的设置,只有10个用户不同然后通过并行流的方式模拟抢购,抢购的实现如下:/****模拟抢单动作*@paramb*@return*/privateStringqiang(Stringb){//用户开始抢时间longstartTime=System.currentTimeMillis();//30秒内未抢到继续获取锁while((startTime+timeout)>=System.currentTimeMillis()){//产品是否剩余if(nKuCuen<=0){Break;}if(jediscom.setnx(statepingkey,b)){//用户B获取logger.info("用户{}获取锁...",b);nkucuen<=0){break;}//模拟生成订单的耗时操作,方便查看:神牛-50获取锁记录more{timeunit.seconds.sleep(1);}Catch(InterruptedExceptionE){e.printstacktrace();}//抢购成功,商品减少,记录用户nkucuen-=1;//冲单成功跳出Logger.info("用户{}订单成功跳出订单...剩余库存:{}",b,nKuCuen);returnb+"抢单成功,剩余库存:"+nKuCuen;}finally{logger.info("用户{}释放锁...",b...//释放锁jedisCom.delnx(shangpingKey,b);}}}else{//用户b没有获取到锁,在超时范围内继续申请锁,不需要处理//"b-quals("b-quals("")||b.equals("Godox-69")){//logger.info(“user{}正在等待获取锁...”,b);//年期逻辑为:1。ParallerStream():平行流模拟多用户恐慌购买2.+timeout)>=System.currentTimeMillis():判断用户不成功,在timeout秒内继续获取锁3.获取锁前和4.jedisCom.delnx(shangpingKey,b):用户获取抢购锁5.获取锁下单成功后,最后释放锁:jedisCom.delnx(shangpingKey,b)我们再看一下记录的日志结果:最后返回的是抢购成功的用户:—结束—