前言
在分布式锁的实现中,zookeeper也是一种不错的选择,了解zookeeper的同学应该知道,zookeeper不仅可以作为集群的部署的中间件的服务协调器,器本身也是具有一定的数据存储结构的,有点类似于文件的分层结构,但是它们本身是有序的
基于它的存储数据的文件结构特性,可以利用zookeeper实现分布式锁,具体来说,其实现原理如下:
- 利用zookeeper的临时有序节点的特性【zookeeper的节点类型】
- 多线程并发创建临时节点时,得到的节点序列是有序的
- 序号最小的那个线程获得锁
- 其他线程则监听自己序号的前一个序号【即比它小的那个序号】
- 当前一个序号对应的线程执行完毕,删除自己序号的节点
- 以此类推…
总结来说,就是在创建节点的时候,就已经确定了节点的执行顺序,用一张简单的图展示如下:
结合上述的zookeeper的节点特性描述和上述图示的展示,我们可以做如下设想,当前存在一个业务根节点,假如其路径名称为 : /business,这个路径可以是持久节点,因为不同的业务模块可以设置不同的根节点前缀
那么后续的请求过来,假如第一个线程过来了,zookeeper可能创建这么一个名称的路基为: /business/node_1,再来一个请求,/business/node_2,依次类推,在前面一个节点处理耗时业务期间,后面的请求对应的节点依次排列,每一个节点监听排在它前面的那个节点,一旦那个节点的删除事件发生了,则这个节点就成为新的最小顺序的那个节点,即获取到了锁
按照这个思路,我们使用代码来实现下
1、引入pom依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.6</version>
</dependency>
zookeeper的依赖版本最好和本地启动的那个zookeeper服务的版本保持一致
2、zookeeper锁的核心代码
这里,为了简化业务,我们提供2个方法,获取锁的方法和释放锁的方法
@Slf4j
public class ZkLock implements Watcher,Closeable {
private ZooKeeper zooKeeper;
private String znode;
public ZkLock() {
String connectionUrl = "localhost:2181";
int sessionTimeout = 10000;
try {
this.zooKeeper = new ZooKeeper(connectionUrl, sessionTimeout, this);
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean getLock(String businessCode) {
try {
//创建业务根节点
Stat stat = zooKeeper.exists("/" + businessCode, false);
if (stat == null) {
zooKeeper.create("/" + businessCode, businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//创建临时节点,这里的命名可以根据业务规则定制,只要满足有序即可
znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//判断当前这个节点是否是所有节点中序号最小的那一个
List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
Collections.sort(childrenNodes);
String firstNode = childrenNodes.get(0);
//如果说所有的子节点中排在第一位的那个节点正好就是,说明获取到了锁
if (znode.endsWith(firstNode)) {
return true;
}
//如果不是第一个子节点,则监听前一个节点
String lastNode = firstNode;
for (String node : childrenNodes) {
if (znode.endsWith(node)) {
zooKeeper.exists("/" + businessCode + "/" + lastNode, true);
break;
} else {
lastNode = node;
}
}
//等待前一个节点唤醒
synchronized (this) {
wait();
}
return true;
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public void close() {
try {
zooKeeper.delete(znode,-1);
zooKeeper.close();
log.info("我已经释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
/**
* 监听删除事件
* @param event
*/
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
synchronized (this) {
notify();
}
}
}
}
3、测试
@Test
public void testZkLock(){
ZkLock lock = new ZkLock();
boolean info = lock.getLock("order");
System.out.println("获取锁的结果是:" + info);
lock.close();
}
运行一下这个单元测试代码,可以看到关于锁的实现是没问题的
下面我们直接通过接口进行测试,编写一个简单的web接口
@GetMapping("/testZkLock")
public String testZkLock() {
ZkLock zkLock = new ZkLock();
boolean info = zkLock.getLock("order");
try {
if (info) {
logger.info("获取到了锁,开始执行耗时业务");
Thread.sleep(10000);
logger.info("方法执行完成,释放锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
zkLock.close();
return "success";
}
按照之前的方式,将当前的服务启动2份,以不同的端口号区分,浏览器输入:
http://localhost:7748/testZkLock
http://localhost:7749/testZkLock
观察后台的打印日志:
通过对比分析执行时间间隔,可以发现,不同的线程进来的请求,按照获取锁的顺序有序执行,从而可以保证了数据的安全性
下面我们使用上面的锁尝试一下对之前的下单扣库存的逻辑进行一下改造
@Override
@Transactional(rollbackFor = Exception.class)
public ResponseResult createOrder() throws InterruptedException {
log.info("进入方法");
ZkLock zkLock = new ZkLock();
boolean info = zkLock.getLock("order");
String name = Thread.currentThread().getName();
try {
if (info) {
Thread.sleep(15000);
log.info("线程:{} ->获取到了锁,准备创建订单...", name);
Product product = productMapper.selectProductById(productId);
if (product == null) {
throw new BusinessException("购买的商品不存在");
}
Integer currCount = product.getCount();
if (purchaseProductNum > currCount) {
log.info("购买的商品库存数量不够了,购买的数量是:{},实际库存数是:{}", purchaseProductNum, product.getCount());
throw new BusinessException("购买的商品库存数量不够了");
}
Integer leftCount = currCount - purchaseProductNum;
product.setCount(leftCount);
//更新商品的库存
productMapper.updateById(product);
//订单表和订单详情表各自插入一条数据
String orderId = insertOrder(product);
insertOrderItem(product, orderId);
}
} finally {
zkLock.close();
log.info("线程:{} ->释放锁", name);
}
return ResponseResult.success(200, "订单创建成功");
}
然后启动服务,浏览器分别输入:
http://localhost:7748/order/create
http://localhost:7749/order/create
观察后台日志:
分析日志可以知,只有一个线程可以最终成功创建一笔订单,说明利用zookeeper的分布式锁也达到了我们预期的效果
基于zookeeper实现分布式锁的另一个组件,curator
如果使用zookeeper实现分布式锁,还有另一个基于此的更简单的组件,即curator,有点类似与redisson和redis的关系,下面我们来看看使用curator如何实现分布式锁
1、导入依赖包
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.3.0</version>
</dependency>
2、首先让我们来看一段简单的测试代码
@Test
public void testCuratorLock() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/order");
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
try {
System.out.println("获取到了锁");
} finally {
System.out.println("释放了了锁");
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
client.close();
}
}
可以直接运行进行测试,代码的前2行属于固定写法,参考curator官网提供的demo
下面我们使用一个web接口进行模拟一下,
将CuratorFramework封装成一个bean注入到spring容器中
@Configuration
public class CuratorLock {
@Bean(initMethod = "start",destroyMethod = "close")
public CuratorFramework getCuratorFramework(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
return client;
}
}
提供一个web接口
@Autowired
private CuratorFramework client;
@GetMapping("/testCuratorLock")
public String testCuratorLock() {
logger.info("进入方法");
InterProcessMutex lock = new InterProcessMutex(client, "/order");
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
logger.info("获取到了锁,开始执行耗时业务");
Thread.sleep(10000);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
logger.info("方法执行完成,释放锁");
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
return "success";
}
同样为了并发情况下分布式锁的效果,我们将服务启动类复制一份,以启动的端口号做区分,浏览器输入:
http://localhost:7748/testCuratorLock
http://localhost:7749/testCuratorLock
通过观察后台的打印日志,可以明显发现先进入的线程持有了锁先执行业务,第二个请求要等到前面的执行完毕才能获取到锁,要注意的是下面这里,在实际应用中要结合自己的业务属性,进行合理的设置,比如订单模块我们统一以order命名,库存模块以storage命名等
有了上面的基础,想必再对之前的下单扣库存业务逻辑进行改造就很简单了吧,这里篇幅原因就不再做演示了
本篇介绍了基于zookeeper的两种分布式锁的实现,从开发的角度上说,使用curator可能要更简单了,毕竟底层帮我们做了封装,使用更方便了,本篇到此结束,最后感谢观看!