一、背景
在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以微服务的形式做分布式部署,对库存的操作也单独封装为一个微服务,这样在高并发情况下,加减库存时,就会出现超卖等问题,这时候就需要对库存操作做分布式锁处理。最近对分布式锁的实现以及性能做了对比分析,今天记录下来,与君共勉。
二、分布式锁介绍
分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性。
分布式锁要具有以下几个特性:
1、可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
2、这把锁要是一把可重入锁(避免死锁)
3、这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
4、有高可用的获取锁和释放锁功能
5、获取锁和释放锁的性能要好
三、分布式锁的几种实现方式
1.基于zookeeper实现分布式锁
2.采用中间件redisson提供分布式锁
3.采用redis的watch做分布式锁
4.采用redis的lua脚本编程方式实现分布式锁
四、基于zookeeper实现分布式锁的原理
1、zk的底层数据结构是树形结构,由一个一个的数据节点组成;
2、zk的节点分为永久节点和临时节点,客户端可以创建临时节点,当客户端会话终止或超时后,zk会自动删除临时节点,该特性可以避免死锁;
3、当节点的状态发生变化时,zk的watch机制会通知监听相应事件的客户端,该特性可以可以用来实现阻塞等待加锁;
4、客户端可以在某个节点下创建子节点,Zookeeper会根据子节点数量自动生成整数序号,类似于数据库的自增主键;
基于zk以上特性,可以实现分布式锁,思路为:
创建一个永久节点作为锁节点,试图加锁的客户端在锁节点下创建临时顺序节点。Zookeeper会保证子节点的有序性。若锁节点下id最小的节点是为当前客户端创建的节点,说明当前客户端成功加锁。否则加锁失败,订阅上一个顺序节点。当上一个节点被删除时,当前节点为最小,说明加锁成功。操作完成后,删除锁节点释放锁。
该方案的特征是优先排队等待的客户端会先获得锁,这种锁称为公平锁。而锁释放后,所有客户端重新竞争锁的方案称为非公平锁。
五、代码实现
1、引入相关zookeeper和curator相关jar
1 <dependency> 2 <groupId>org.apache.zookeeper</groupId> 3 <artifactId>zookeeper</artifactId> 4 <version>3.4.13</version> 5 <scope>compile</scope> 6 <exclusions> 7 <exclusion> 8 <groupId>org.slf4j</groupId> 9 <artifactId>slf4j-log4j12</artifactId> 10 </exclusion> 11 </exclusions> 12 </dependency> 13 <dependency> 14 <groupId>org.apache.curator</groupId> 15 <artifactId>curator-recipes</artifactId> 16 <version>4.0.1</version> 17 </dependency>
2、curatorFramework初始化,放spring容器中
1 /** 2 * curatorFramework初始化 3 * 4 * @author LiJunJun 5 * @date 2018/12/7 6 */ 7 @Configuration 8 public class CuratorBean { 9 10 @Bean 11 public CuratorFramework curatorFramework() { 12 13 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); 14 CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.10.110:2381", retryPolicy); 15 return client; 16 } 17 18 @Bean 19 public InterProcessMutex interProcessMutex() { 20 21 curatorFramework().start(); 22 23 return new InterProcessMutex(curatorFramework(), "/curator/lock"); 24 } 25 }
3、业务代码
1 /** 2 * interProcessMutex 3 */ 4 @Resource 5 private InterProcessMutex interProcessMutex; 6 7 /** 8 * 减库存(基于zookeeper分布式锁实现) 9 * 10 * @param trace 请求流水 11 * @param stockManageReq(stockId、decrNum) 12 * @return -1为失败,大于-1的正整数为减后的库存量,-2为库存不足无法减库存 13 */ 14 @Override 15 @ApiOperation(value = "减库存", notes = "减库存") 16 @RequestMapping(value = "/decrByStock", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) 17 public int decrByStock(@RequestHeader(name = "Trace") String trace, @RequestBody StockManageReq stockManageReq) { 18 19 long startTime = System.nanoTime(); 20 21 LOGGER.reqPrint(Log.CACHE_SIGN, Log.CACHE_REQUEST, trace, "decrByStock", JSON.toJSONString(stockManageReq)); 22 23 int res = 0; 24 String stockId = stockManageReq.getStockId(); 25 Integer decrNum = stockManageReq.getDecrNum(); 26 27 // 添加分布式锁 28 boolean lockResult = false; 29 30 try { 31 if (null != stockId && null != decrNum) { 32 33 stockId = PREFIX + stockId; 34 35 // 获取锁最多等待5s 36 lockResult = interProcessMutex.acquire(5, TimeUnit.SECONDS); 37 38 if (!lockResult) { 39 LOGGER.info("本次请求获取锁失败,lockResult=1"); 40 return -1; 41 } 42 43 // redis 减库存逻辑 44 String vStock = redisStockPool.get(stockId); 45 46 long realV = 0L; 47 if (StringUtils.isNotEmpty(vStock)) { 48 realV = Long.parseLong(vStock); 49 } 50 //库存数 大于等于 要减的数目,则执行减库存 51 if (realV >= decrNum) { 52 Long v = redisStockPool.decrBy(stockId, decrNum); 53 res = v.intValue(); 54 } else { 55 res = -2; 56 } 57 } 58 } catch (Exception e) { 59 LOGGER.error(trace, "decr sku stock failure.", e); 60 res = -1; 61 } finally { 62 if (lockResult) { 63 try { 64 // 释放锁 65 interProcessMutex.release(); 66 } catch (Exception e) { 67 e.printStackTrace(); 68 } 69 } 70 LOGGER.respPrint(Log.CACHE_SIGN, Log.CACHE_RESPONSE, trace, "decrByStock", System.nanoTime() - startTime, String.valueOf(res)); 71 } 72 return res; 73 }
六、ab压测结果分析
发现性能低的简直无法忍受,5000个请求,100并发量,tps仅有19.76,还有922个请求失败
统计日志中打印的获取锁失败的请求个数,发现等待5s后仍未获取到锁数目就是就是ab压测中失败的922个
压测过程中,我们可以看下zk的/curator/lock节点下的临时节点变化情况,我们连接zk客户端
./zkCli.sh -server 192.168.10.110:2381
查看目录节点
ls /curator/lock,发现/curator/lock创建了很多临时节点,并且随着请求的执行,临时节点也在不停的变化
七、总结
zookeeper确实可以实现分布式锁,但由于需要频繁的新增和删除节点,性能比较差,不推荐使用。
下一篇我们分享基于redisson中间件实现的分布式锁。