本篇内容次要解说的是redis分布式锁,这个在各大厂面试简直都是必备的,上面联合模仿抢单的场景来应用她;本篇不波及到的redis环境搭建,疾速搭建集体测试环境,这里倡议应用docker;本篇内容节点如下:
Jedis的nx生成锁
- 如何删除锁
- 模仿抢单动作(10w集体开抢)
- jedis的nx生成锁
对于java中想操作redis,好的形式是应用jedis,首先pom中引入依赖:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId></dependency>
对于分布式锁的生成通常须要留神如下几个方面:
Spring Boot 根底就不介绍了,举荐下这个实战教程:https://github.com/javastacks...
创立锁的策略: redis的一般key个别都容许笼罩,A用户set某个key后,B在set雷同的key时同样能胜利,如果是锁场景,那就无奈晓得到底是哪个用户set胜利的;这里jedis的setnx形式为咱们解决了这个问题,简略原理是:当A用户先set胜利了,那B用户set的时候就返回失败,满足了某个工夫点只容许一个用户拿到锁。
锁过期工夫: 某个抢购场景时候,如果没有过期的概念,当A用户生成了锁,然而前面的流程被阻塞了始终无奈开释锁,那其余用户此时获取锁就会始终失败,无奈实现抢购的流动;当然失常状况个别都不会阻塞,A用户流程会失常开释锁;过期工夫只是为了更有保障。
上面来上段setnx操作的代码:
public boolean setnx(String key, String val) { Jedis jedis = null; try { jedis = jedisPool.getResource(); if (jedis == null) { return false; } return jedis.set(key, val, "NX", "PX", 1000 * 60). equalsIgnoreCase("ok"); } catch (Exception ex) { } finally { if (jedis != null) { jedis.close(); } } return false; }
这里留神点在于jedis的set办法,其参数的阐明如:
- NX:是否存在key,存在就不set胜利
- PX:key过期工夫单位设置为毫秒(EX:单位秒)
setnx如果失败间接封装返回false即可,上面咱们通过一个get形式的api来调用下这个setnx办法:
@GetMapping("/setnx/{key}/{val}")public boolean setnx(@PathVariable String key, @PathVariable String val) { return jedisCom.setnx(key, val);}
拜访如下测试url,失常来说第一次返回了true,第二次返回了false,因为第二次申请的时候redis的key已存在,所以无奈set胜利。
由上图可能看到只有一次set胜利,并key具备一个无效工夫,此时已达到了分布式锁的条件。
如何删除锁
下面是创立锁,同样的具备无效工夫,然而咱们不能齐全依赖这个无效工夫,场景如:无效工夫设置1分钟,自身用户A获取锁后,没遇到什么非凡状况失常生成了抢购订单后,此时其余用户应该能失常下单了才对,然而因为有个1分钟后锁能力主动开释,那其余用户在这1分钟无奈失常下单(因为锁还是A用户的),因而咱们须要A用户操作完后,被动去解锁:
public int delnx(String key, String val) { Jedis jedis = null; try { jedis = jedisPool.getResource(); if (jedis == null) { return 0; } //if redis.call('get','orderkey')=='1111' then return redis.call('del','orderkey') else return 0 end StringBuilder sbScript = new StringBuilder(); sbScript.append("if redis.call('get','").append(key).append("')").append("=='").append(val).append("'"). append(" then "). append(" return redis.call('del','").append(key).append("')"). append(" else "). append(" return 0"). append(" end"); return Integer.valueOf(jedis.eval(sbScript.toString()).toString()); } catch (Exception ex) { } finally { if (jedis != null) { jedis.close(); } } return 0; }
这里也应用了jedis形式,间接执行lua脚本:依据val判断其是否存在,如果存在就del;
其实集体认为通过jedis的get形式获取val后,而后再比拟value是否是以后持有锁的用户,如果是那最初再删除,成果其实相当;只不过间接通过eval执行脚本,这样防止多一次操作了redis而已,缩短了原子操作的距离。(如有不同见解请留言探讨);同样这里创立个get形式的api来测试:
@GetMapping("/delnx/{key}/{val}")public int delnx(@PathVariable String key, @PathVariable String val) { return jedisCom.delnx(key, val);}
留神的是delnx时,须要传递创立锁时的value,因为通过et的value与delnx的value来判断是否是持有锁的操作申请,只有value一样才容许del;
模仿抢单动作(10w集体开抢)
有了上面对分布式锁的粗略根底,咱们模仿下10w人抢单的场景,其实就是一个并发操作申请而已,因为环境无限,只能如此测试;如下初始化10w个用户,并初始化库存,商品等信息,如下代码:
//总库存 private long nKuCuen = 0; //商品key名字 private String shangpingKey = "computer_key"; //获取锁的超时工夫 秒 private int timeout = 30 * 1000; @GetMapping("/qiangdan") public List<String> qiangdan() { //抢到商品的用户 List<String> shopUsers = new ArrayList<>(); //结构很多用户 List<String> users = new ArrayList<>(); IntStream.range(0, 100000).parallel().forEach(b -> { users.add("神牛-" + b); }); //初始化库存 nKuCuen = 10; //模仿开抢 users.parallelStream().forEach(b -> { String shopUser = qiang(b); if (!StringUtils.isEmpty(shopUser)) { shopUsers.add(shopUser); } }); return shopUsers; }
有了下面10w个不同用户,咱们设定商品只有10个库存,而后通过并行流的形式来模仿抢购,如下抢购的实现:
/** * 模仿抢单动作 * * @param b * @return */ private String qiang(String b) { //用户开抢工夫 long startTime = System.currentTimeMillis(); //未抢到的状况下,30秒内持续获取锁 while ((startTime + timeout) >= System.currentTimeMillis()) { //商品是否残余 if (nKuCuen <= 0) { break; } if (jedisCom.setnx(shangpingKey, b)) { //用户b拿到锁 logger.info("用户{}拿到锁...", b); try { //商品是否残余 if (nKuCuen <= 0) { break; } //模仿生成订单耗时操作,不便查看:神牛-50 屡次获取锁记录 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //抢购胜利,商品递加,记录用户 nKuCuen -= 1; //抢单胜利跳出 logger.info("用户{}抢单胜利跳出...所剩库存:{}", b, nKuCuen); return b + "抢单胜利,所剩库存:" + nKuCuen; } finally { logger.info("用户{}开释锁...", b); //开释锁 jedisCom.delnx(shangpingKey, b); } } else { //用户b没拿到锁,在超时范畴内持续申请锁,不须要解决// if (b.equals("神牛-50") || b.equals("神牛-69")) {// logger.info("用户{}期待获取锁...", b);// } } } return ""; }
这里实现的逻辑是:
1、parallelStream():并行流模仿多用户抢购
2、(startTime + timeout) >= System.currentTimeMillis():判断未抢胜利的用户,timeout秒内持续获取锁
3、获取锁前和后都判断库存是否还足够
4、jedisCom.setnx(shangpingKey, b):用户获取抢购锁
5、获取锁后并下单胜利,最初开释锁:jedisCom.delnx(shangpingKey, b)
再来看下记录的日志后果:
最终返回抢购胜利的用户:
— End —