单机应用架构中,秒杀案例使用ReentrantLcok或者synchronized来达到秒杀商品互斥的目的。然而在分布式系统中,会存在多台机器并行去实现同一个功能。也就是说,在多进程中,如果还使用以上JDK提供的进程锁,来并发访问数据库资源就可能会出现商品超卖的情况。因此,需要我们来实现自己的分布式锁。
实现一个分布式锁应该具备的特性:
A、高可用、高性能的获取锁与释放锁 B、在分布式系统环境下,一个方法或者变量同一时间只能被一个线程操作 C、具备锁失效机制,网络中断或宕机无法释放锁时,锁必须被删除,防止死锁 D、具备阻塞锁特性,即没有获取到锁,则继续等待获取锁 E、具备非阻塞锁特性,即没有获取到锁,则直接返回获取锁失败 F、具备可重入特性,一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁 分布式锁几种实现方式:
1)、基于数据库实现分布式锁 2)、基于 Redis 实现分布式锁 3)、基于 Zookeeper 实现分布式锁 前两种对于分布式生产环境来说并不是特别推荐,高并发下数据库锁性能太差,Redis在锁时间限制和缓存一致性存在一定问题。
Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。如下图:
1).持久节点 (PERSISTENT):默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
2).持久节点顺序节点(PERSISTENT_SEQUENTIAL):所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
3).临时节点(EPHEMERAL):和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
4).临时顺序节点(EPHEMERAL_SEQUENTIAL)顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:
1)、获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的
2)释放锁
释放锁分为两种情况:A
A、任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。
B、任务执行过程中,客户端崩溃
获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。
最终,Client3成功得到了锁。
Springboot实现zookeeper分布式锁,这里使用第三方库Curator来实现对zookeeper的操作 Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。
1、maven引入
<!-- ZooKeeper 之 Curator--> <!-- ZooKeeper版本号为4的话,机器安装zookeeper的版本要求是3.5及其以上的版本--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.0.1</version> </dependency>2、配置文件 application.properties
server.port= 10181 ##ZooKeeper 集成 Curator zk.url = 127.0.0.1:21873、代码类
配置类:
package com.allen.lock.config; import lombok.extern.slf4j.Slf4j; import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author xuguocai 2020/9/3 11:00 */ @Configuration @Slf4j public class ZooKeeperConf { @Value("${zk.url}") private String zkUrl; @Bean public CuratorFramework getCuratorFramework() { // 用于重连策略,1000毫秒是初始化的间隔时间,3代表尝试重连次数 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl, retryPolicy); //必须调用start开始连接ZooKeeper client.start(); return client; } }controller类:
package com.allen.lock.controller; import com.allen.lock.service.ZooKeeperService; import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.data.Stat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @author xuguocai 2020/9/3 11:12 */ @RestController @RequestMapping("/zookeeper") public class ZooKeeperController { @Autowired private ZooKeeperService zooKeeperService; @Autowired private CuratorFramework zkClient; /** * zookeeper 获取节点下的数据 * * post请求: http://localhost:10181/zookeeper/getData?path=/task * * @param path * @return */ @PostMapping("/getData") public String getData(@RequestParam String path) { byte[] bytes = null; try { bytes = zkClient.getData().forPath(path); } catch (Exception e) { e.printStackTrace(); } String str = new String(bytes); return str; } /** * 创建节点路径 * @param path * @return */ @PostMapping("/create") public String create(@RequestParam String path) { try { zkClient.create().forPath(path); } catch (Exception e) { e.printStackTrace(); } return "success"; } @PostMapping("/delete") public String delete(@RequestParam String path) { try { zkClient.delete().forPath(path); } catch (Exception e) { e.printStackTrace(); } return "success"; } @PostMapping("/setData") public String setData(@RequestParam(value = "path") String path, @RequestParam(value = "data") String data) { try { zkClient.setData().forPath(path, data.getBytes()); } catch (Exception e) { e.printStackTrace(); } return "success"; } @PostMapping("/check") public String check(@RequestParam(value = "path") String path) { Stat stat = null; try { stat = zkClient.checkExists().forPath(path); } catch (Exception e) { e.printStackTrace(); } return "stat" + stat; } @PostMapping("/children") public String children(@RequestParam(value = "path") String path) { List<String> children = null; try { children = zkClient.getChildren().forPath(path); } catch (Exception e) { e.printStackTrace(); } return "children" + children; } @PostMapping("/watch") public String watch(@RequestParam(value = "path") String path) { Stat stat = null; try { stat = zkClient.checkExists().watched().forPath(path); } catch (Exception e) { e.printStackTrace(); } return "watch " + stat; } /** * zookeeper分布式锁 * * @param product * @return */ @PostMapping("/makeOrder") public String makeOrder(@RequestParam(value = "product") String product) { zooKeeperService.makeOrder(product); return "success"; } }实现类:
package com.allen.lock.service; import lombok.extern.slf4j.Slf4j; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.locks.InterProcessMutex; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /** * @author xuguocai 2020/9/3 11:12 */ @Service @Slf4j public class ZooKeeperService { private static final String lockPath = "/lock/order"; @Autowired private CuratorFramework zkClient; public void makeOrder(String product) { log.info("try do job for " + product); String path = lockPath + "/" + product; try { // InterProcessMutex 构建一个分布式锁 InterProcessMutex lock = new InterProcessMutex(zkClient, path); try { if (lock.acquire(5, TimeUnit.HOURS)) { // 获得锁 // 模拟业务处理耗时5秒 Thread.sleep(5*1000); log.info("do job " + product + " done"); } } finally { // 释放该锁 lock.release(); } } catch (Exception e) { log.error("失败:{}",e.getMessage()); // zk异常 e.printStackTrace(); } } }
学习地址:
https://blog.csdn.net/holmes369/article/details/104947495
https://blog.csdn.net/qq_15199097/article/details/89920570
https://blog.csdn.net/wuzhiwei549/article/details/80692278
项目地址:
https://github.com/krycai/gc-framework/tree/master/gc-zookeeperlock