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 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(); } } }
运行结果: