本文主要讲解redis分布式锁,几乎是各大厂面试必备的。下面结合模拟抢单场景使用;本文本文不涉及redis环境搭建,快速搭建个人测试环境。这里推荐使用docker;本文内容节点如下:如何删除jedis的NX生成锁,模拟抢单动作(10w个人开抢)最好的方法是使用绝地武士。首先在pom中引入依赖:redis.clientsjedis对于分布式锁的生成一般需要注意以下几个方面:锁创建策略:普通的rediskey一般允许被覆盖。用户A设置某个key后,B也可以成功设置相同的key。如果是锁场景,无法知道是哪个用户设置成功的?这里jedis的setnx方法为我们解决了这个问题。简单的原理就是:当用户A先设置成功,然后用户B设置时返回失败,满足某个时间点。用户获取锁。锁过期时间:在抢购场景下,如果没有过期的概念,当用户A产生了锁,但是后续进程阻塞,无法释放锁,此时其他用户总是获取不到锁且无法完成抢购活动;当然一般情况下是不会阻塞的,A用户进程会正常释放锁;过期时间只是为了更安全。下面是上述setnx操作的代码:,"NX","PX",1000*60).equalsIgnoreCase("ok");}catch(Exceptionex){}最后{if(jedis!=null){jedis.close();}}returnfalse;}这里重点是jedis的set方法,其参数说明如下:NX:是否存在key,存在则不设置成功PX:key过期时间单位设置为毫秒(EX:很快)。如果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;try{jedis=jedisPool.getResource();if(jedis==null){return0;}//ifredis.call('get','orderkey')=='1111'thenreturnredis.call('del','orderkey')elsereturn0endStringBuildersbScript=newStringBuilder();sbScript.append("ifredis.call('get','").append(key).append("')").append("=='").append(val).append("'").append("then").append("returnredis.call('del','").append(key).append("')").append("else").append("return0").append("end");returnInteger.valueOf(jedis.eval(sbScript.toString()).toString());}catch(Exceptionex){}finally{if(jedis!=null){jedis.close();}}return0;}这里也使用jedis方法直接执行lua脚本:根据val判断其值是否存在,存在则del;其实我个人认为通过jedis的get方法获取val后,再比较当前是否持有该值如果最后删除锁的用户,效果其实是一样的;但是脚本直接通过eval执行,避免了再次操作redis,缩短了原子操作的间隔(如有不同意见欢迎留言讨论);同时创建一个getAPI来测试:@GetMapping("/delnx/{key}/{val}")publicintdelnx(@PathVariableStringkey,@PathVariableStringval){returnjedisCom.delnx(key,val);}注意,使用delnx时,需要传递创建锁时的值,因为et的值和delnx的值是用来判断是否是持有锁的操作请求,只有值相同才允许del;模拟抢单Action(10万人开始抢单)有了上面分布式锁的粗略基础,我们模拟10万人抢单的场景,其实只是一个并发的操作请求。由于环境有限,只能通过这种方式进行测试;下面初始化100000个User,并初始化库存、商品等信息,如下代码://总库存privatelongnKuCuen=0;//商品键名privateStringshangpingKey="computer_key";//获取锁的超时时间,单位秒privateinttimeout=30*1000;@GetMapping("/qiangdan")publicListqiangdan(){//抢到商品的用户列表ListshopUsers=newArrayList<>();//构造多个用户Listusers=newArrayList<>();IntStream。range(0,100000).parallel().forEach(b->{users.add("神牛-"+b);});//初始化库存nKuCuen=10;//模拟开抢users.parallelStream().forEach(b->{StringshopUser=qiang(b);if(!StringUtils.isEmpty(shopUser)){shopUsers.add(shopUser);}});returnshopUsers;}有了上面10w个不同的用户,我们设置那里某商品只有10个存货,然后通过并行流模拟抢购,如下实现:/***模拟抢单动作**@paramb*@return*/privateStringqiang(Stringb){//用户开始抢单时间longstartTime=System.currentTimeMillis();//如果没有抢单,30秒内继续获取lockswhile((startTime+timeout)>=System.currentTimeMillis()){//产品是否保留if(nKuCuen<=0){break;}if(jedisCom.setnx(shangpingKey,b)){//用户b获取锁logger.info("User{}获取了锁...",b);try{//产品是否剩余if(nKuCuen<=0){break;}//模拟生成订单的耗时操作,方便查看:神牛-50次获取锁记录try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}//抢购成功,items递减,记录usernKuCuen-=1;//抢单成功跳出logger.info("User{}成功抢单跳出...剩余库存:{}",b,nKuCuen);returnb+"成功抢单,剩余库存:"+nKuCuen;}finally{logger.info("User{}释放锁...",b);//释放锁jedisCom.delnx(shangpingKey,b);}}else{//用户b没有拿到锁,在超时范围内继续申请锁,不需要处理//if(b.equals("Godox-50")||b.equals("Godox-69")){//logger.info("用户{}正在等待获取锁...",b);//}}}return"";}这里实现的逻辑是:parallelStream():Parallelstream模拟多用户购买(startTime+timeout)>=System.currentTimeMillis():判断不购买成功的用户会在获取锁前后的timeout秒内继续获取锁。减掉库存就够了吗?用户: