分布式锁原理与实现
场景描述:
小型电商网站,下单,生产有一定业务含义的唯一订单编号。
思路分析:
如果单台服务器已无法撑起并发量,怎么办?集群?
分布式锁的用途:
在分布式环境下协同共享资源的使用。
分布式锁的特点:
1.排他性: 只有一个线程能获取到。
2.阻塞性: 其他未抢到的线程阻塞,直到锁释放出来,在抢。
3.可重入性:线程获得锁后,后续是否可重复获取该锁。
我们掌握的计算机技术中,有哪些能提供排他性?
1. 文件系统
2. 数据库: 主键 唯一约束 for update
3. 缓存redis: setnx
4. zookeeper: 类似文件系统
常用的分布式锁实现技术
1. 基于数据库实现分布式锁
性能较差。容易出现单点故障。
锁没有失效时间,容易死锁。
2. 基于缓存实现分布式锁
实现复杂
存在死锁
3. 基于zookeeper实现分布式锁
实现相对简单
可靠性高
性能较好
基于zookeeper实现分布式锁
方式一:
1.去获取锁创建节点
2.获取锁成功,执行业务并且释放锁,等待唤醒。
3. 获取锁失败,注册节点的watcher,阻塞等待,直到上一个成功获取锁释放到锁,才会取消watcher,尝试抢锁。
特性:
同父的子节点不可重名
假如部署在规模较大集群会发生'惊群效应'
1.巨大的服务器性能损耗
2.网络冲击
3.可能造成宕机
方式二: (解决方案)
1.创建一个锁目录lock
2.希望获得锁的线程A就在lock目录下,创建临时顺序节点
3.获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁
4.线程B获取所有节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)
5.线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁。
特性:
临时顺序节点
代码实现
/** * zookeeper锁实现(临时顺序结点) * @author skymr * */ public class ZookeeperLock1 implements Lock, Watcher{ public ZookeeperLock1(String url, int sessionTimeOut, String path){ this.parrentPath = path; try { //url是zookepper服务器的地址 zk = new ZooKeeper(url, sessionTimeOut, this); latch.await(); } catch (Exception e) { e.printStackTrace(); } } //zk客户端 private ZooKeeper zk; //结点路径 private String parrentPath; //用于初始化zk的,zk连接是异步的,但连接成功后才能进行调用 private CountDownLatch latch = new CountDownLatch(1); private static ThreadLocal<String> currentNodePath = new ThreadLocal<String>(); public void lock() { if(!tryLock()){ String mypath = currentNodePath.get(); //如果尝试加锁失败,则进入等待 synchronized(mypath){ System.out.println(Thread.currentThread().getName() +" lock失败,进入等待"); try { mypath.wait(); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() +" lock等待完成"); } //等待别人释放锁后,自己再去加锁 lock(); } else{ System.out.println(Thread.currentThread().getName() +" lock成功"); } } public void lockInterruptibly() throws InterruptedException { } public boolean tryLock() { try { //加锁代码是创建一个节点 String mypath = currentNodePath.get(); if(mypath == null){ mypath = zk.create(parrentPath + "/", "111".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); currentNodePath.set(mypath); } final String currentPath = mypath; List<String> allNodes = zk.getChildren(parrentPath, false); Collections.sort(allNodes); //不抛异常就表示创建成功啦 String nodeName = mypath.substring((parrentPath + "/").length()); if(allNodes.get(0).equals(nodeName)){ //当前结点是最小的节点,获取锁成功 return true; } else{ //监听最小的结点 String targetNodeName = parrentPath + "/" + allNodes.get(0); System.out.println(Thread.currentThread().getName() +" 需要等待节点删除" + targetNodeName); zk.exists(targetNodeName, new Watcher() { public void process(WatchedEvent event) { if(event.getType() == EventType.NodeDeleted){ synchronized(currentPath){ currentPath.notify(); } System.out.println(Thread.currentThread().getName() +" 通知Lock等待中的线程重试加锁"); } } }); } return false; } catch (Exception e) { return false; } } public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } public void unlock() { try { //释放锁,删除节点 String mypath = currentNodePath.get(); if(mypath != null){ System.out.println(Thread.currentThread().getName() +" 释放锁"); zk.delete(mypath, -1); currentNodePath.remove(); } } catch (Exception e) { } } public Condition newCondition() { return null; } public void process(WatchedEvent event) { System.out.println(event); if(event.getType() == EventType.None){ //当连接上了服务器后,初始化完成 latch.countDown(); } } }