Zk实现分布式锁
Posted ericz2j
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zk实现分布式锁相关的知识,希望对你有一定的参考价值。
Zookeeper实现分布式锁
zookeeper实现分布式锁,主要得益于ZooKeeper保证了数据的强一致性这一特性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
1. 保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过 create znode 的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
2. 控制时序,就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock预先已经存在,客户端在它下面创建临时有序节点。Zk 的父节点(/distribute_lock)维持一份 sequence, 保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
分布式锁的产生的原因:
1.单纯的Lock锁或者synchronize只能解决单个jvm线程安全问题
2.分布式 Session 一致性问题
3.分布式全局id(也可以使用分布式锁)
换个角度来说,分布式锁产生的原因就是集群。
在单台服务器上,如何生唯一的订单号,方案有UUid+时间戳方式,redis方式。
生成订单号, 秒杀抢购时候,首先如果预测是100w订单号,生成放在redis。客户端下单,直接redis去获取即可。因为redis是单线程的,如果实际是150w用户,当redis剩下50w订单号时候,继续生成补充。
但是在集群环境下,这种方式其实并不能保证其唯一性。
import java.text.SimpleDateFormat; import java.util.Date; //生成订单号 时间戳 public class OrderNumGenerator //区分不同的订单号 private static int count = 0; //单台服务器,多个线程同时生成订单号 public String getNumber() try Thread.sleep(300); catch (Exception e) SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); return simpt.format(new Date()) + "-" + ++count; //时间戳后面加了 count
开启100个线程调用:
public class OrderService implements Runnable private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); public void run() getNumber(); public void getNumber() String number = orderNumGenerator.getNumber(); System.out.println(Thread.currentThread().getName()+"num"+number); public static void main(String[] args) OrderService orderService = new OrderService(); //开启100个线程 for (int i = 0; i <100; i++) new Thread(orderService).start();
结果:
因为多个线程共享同一个全局变量,会产生线程安全问题!
解决方案当然就是可以加锁:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class OrderService implements Runnable private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); private Lock lock = new ReentrantLock(); public void run() getNumber(); public void getNumber() //加锁 lock.lock(); String number = orderNumGenerator.getNumber(); System.out.println(Thread.currentThread().getName() + "生成订单:" + number); //释放锁 lock.unlock(); public static void main(String[] args) OrderService orderService = new OrderService(); // 开启100个线程 for (int i = 0; i < 100; i++) new Thread(orderService).start();
但是这种方式效率很低!
如果是集群环境下:
每台jvm都有一个count,都有自增的代码操作这个count, 三个不同的jvm独立的用户请 过来 映射到哪个就操作哪个,这时候就产生分布式锁的问题。
这时候需要分布式锁,共享一个count
jvm1 操作时候 其他的jvm2 和 jvm3 不可以操作他。
分布式锁:保证分布式领域中共享数据安全问题,它的实现方式可以有这些:
1、数据库实现(效率很低)
2、redis实现(使用redission实现,但是需要考虑释放问题。也比较麻烦)
3、Zookeeper实现(使用临时节点,效率高,失效时间可以控制)
4、Spring Cloud实现全局锁(内置的)
下面用一个业务场景加上代码来说明:
业务场景
在分布式情况,生成全局订单号ID
产生问题
在分布式集群环境下,每台机器不能实现同步,在分布式场景下使用时间戳生成订单号可能会重复
Zookeeper实现分布式锁原理
使用zookeeper创建临时序列节点来实现分布式锁,大体思路就是创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理,依次类推……
因为zk节点唯一的,不能重复,节点类型为临时节点, 一台zk服务器创建成功时候,另外的zk服务器创建节点时候就会报错,该节点已经存在。这时候其他的zk服务器就会开始监听并等待。让这台zk服务器的程序现在执行完毕,释放锁。关闭当前会话。临时节点就会消失,并且事件通知Watcher,其他的就会来创建。
代码实现
创建锁的接口
public interface ExtLock //ExtLock基于zk实现分布式锁 public void getLock(); //释放锁 public void unLock();
实现zk分布式锁:
import java.util.concurrent.CountDownLatch; import org.I0Itec.zkclient.IZkDataListener; public class ZookeeperDistrbuteLock implements ExtLock private static final String CONNECTION="192.168.2.222:2181"; private ZkClient zkClient = new ZkClient(CONNECTION); private String lockPath="/distribute_lock"; private CountDownLatch countDownLatch; //获取锁 public void getLock() // 如果节点创建成果,直接执行业务逻辑,如果节点创建失败,进行等待 if (tryLock()) System.out.println("#####成功获取锁######"); else //进行等待 waitLock(); //释放锁 public void unLock() //执行完毕 直接连接 if (zkClient != null) zkClient.close(); System.out.println("######释放锁完毕######"); public boolean tryLock() try zkClient.createEphemeral(lockPath); return true; catch (Exception e) // 如果失败 直接catch return false; public void waitLock() IZkDataListener iZkDataListener = new IZkDataListener() // 节点被删除 public void handleDataDeleted(String arg0) throws Exception if (countDownLatch != null) countDownLatch.countDown(); // 计数器为0的情况,await 后面的继续执行 // 节点被修改 public void handleDataChange(String arg0, Object arg1) throws Exception System.out.println("########节点被修改#######"); ; // 监听事件通知 zkClient.subscribeDataChanges(lockPath, iZkDataListener); // 控制程序的等待 if (zkClient.exists(lockPath)) //如果检查出已经被创建了就等待 countDownLatch = new CountDownLatch(1); try countDownLatch.wait(); //当为0时候,后面的继续执行 catch (Exception e) //后面代码继续执行 //删除该事件监听 zkClient.unsubscribeDataChanges(lockPath, iZkDataListener);
生产订单号:
import java.text.SimpleDateFormat; import java.util.Date; //生成订单号 时间戳 public class OrderNumGenerator //区分不同的订单号 private static int count = 0; //单台服务器,多个线程 同事生成订单号 public String getNumber() SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); return simpt.format(new Date()) + "-" + ++count; //时间戳后面加了 count
运行方法:
public class OrderService implements Runnable private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); private ExtLock lock = new ZookeeperDistrbuteLock(); public void run() getNumber(); public synchronized void getNumber() // 加锁 保证线程安全问题 让一个线程操作 lock.getLock(); String number = orderNumGenerator.getNumber(); System.out.println(Thread.currentThread().getName() + ",number" + number); try Thread.sleep(10000);//为了看效果 catch (InterruptedException e) e.printStackTrace(); lock.unLock(); public static void main(String[] args) for (int i = 0; i < 100; i++) // 开启100个线程 //模拟分布式锁的场景 new Thread(new OrderService()).start();
运行结果:
以上是关于Zk实现分布式锁的主要内容,如果未能解决你的问题,请参考以下文章