一、什么是分布式锁,它为什么会产生
现在我们的项目大多都是分布式部署的,分布式部署一个显而易见的好处就是可以减轻服务器的压力,但也带来一系列的问题,比如:分布式session一致性问题、分布式配置中心、分布式任务调度平台、分布式日志平台、分布式全局ID的生成、分布式事务、分布式限流、分布式锁。
由于项目是分布式部署的,这样部署的每一份都会有自己的jvm运行项目,但是它们需要共享一些实例变量,但是在高并发的情况下,可能有多个jvm同时操作实例变量,这样就会造成数据不一致、值不同步的问题,这个时候我们需要保证只有一个jvm在一个时间操作实例变量,这样我们就引用了分布式锁来限制只有一个jvm能执行任务,其他的先排队。
synchronized和lock只能解决单个jvm中多个线程的线程安全问题
二、分布式锁的常见解决方案,它们的优缺点是
(1) 使用数据库,我们知道数据库本身有表锁、行锁,但是使用数据库作分布式锁的效率非常低下
(2)使用redis,使用redis本身自带的setNx命令,这是一个互斥的命令,如果setNx需要创建的key在redis中已经有了,那么其他请求不能成功返回异常。 好处:我们知道redis的写的效率是81000,读的效率是110000,因此它的效率还是可以的。 缺点:创建分布式锁容易造成死锁,不好控制过期时间,而且有一个bug就是执行任务删除锁,需要先判断一下这个锁是否存在,返回正确的情况下,这个时候恰好过期了,而新锁又由其他线程创建,这个时候去删除就会删除掉别的线程的锁,需要使用redis+lua脚本解决这个问题,一旦发现自己的锁,立即删除。
(3)RedisSon 是专门用来做分布式锁的
(4)zookeeper做分布式锁,比较简单,不同意造成死锁
三、zookeeper实现分布式锁的原理
使用zookeeper的创建的是临时节点的原理,为什么要创建临时节点是因为zookeeper的连接session close以后。临时节点自动消失。
(1)多个jvm中的项目中的线程去zookeeper中创建同一个节点,这个节点是临时节点
(2)创建临时节点的结果可能是一个成功,其他失败,这样成功创建节点的相当于拿到了锁,可以执行任务,没有创建成功相当于没有拿到锁,那么进行等待
(3)拿到锁的执行完任务之后,那么就可以释放锁(close掉zookeeper的session,强制关闭会造成短暂死锁),通过watcher通知给其他的jvm,重新去创建节点,竞争锁。
四、代码演示
zk实现分布式锁
/** * Created by 辉 on 2020/6/30. */ public class ZookeeperLock { // zk连接地址 private static final String CONNECTSTRING = "192.168.196.175:2181"; // 创建zk连接 protected ZkClient zkClient = new ZkClient(CONNECTSTRING); //防止死锁的发生,在创建zk连接的时候可以传入session的失效 //时间,这样就能在zk断开的时候自动去除这个临时节点 protected static final String PATH = "/lock"; public void getLock() { if (tryLock()) { System.out.println("##获取lock锁的资源####"); } else { // 等待 waitLock(); // 重新获取锁资源 getLock(); } } public void unLock() { if (zkClient != null) { zkClient.close(); System.out.println("释放锁资源..."); } } private CountDownLatch countDownLatch = null; boolean tryLock() { try { zkClient.createEphemeral(PATH); return true; //创建临时节点成功,返回true } catch (Exception e) { // e.printStackTrace(); return false; //创建节点失败,那么就会出异常这里就会捕获,这样就会到这里,返回false } } void waitLock() { IZkDataListener izkDataListener = new IZkDataListener() { public void handleDataDeleted(String path) throws Exception { // 唤醒被等待的线程 if (countDownLatch != null) { countDownLatch.countDown(); } } public void handleDataChange(String path, Object data) throws Exception { } }; // 注册事件 zkClient.subscribeDataChanges(PATH, izkDataListener); if (zkClient.exists(PATH)) { countDownLatch = new CountDownLatch(1); try { countDownLatch.await(); } catch (Exception e) { e.printStackTrace(); } } // 删除监听 zkClient.unsubscribeDataChanges(PATH, izkDataListener); } }订单类
//生成订单类 public class OrderNumGenerator { //全局订单id public static int count = 0; public String getNumber() { try { Thread.sleep(200); } catch (Exception e) { } SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); return simpt.format(new Date()) + "-" + ++count; } }生成全局订单ID
//使用多线程模拟生成订单号 public class OrderService implements Runnable { private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); ZookeeperLock zkLock=new ZookeeperLock(); public void run() { getNumber(); } public void getNumber() { try { zkLock.getLock(); String number = orderNumGenerator.getNumber(); System.out.println(Thread.currentThread().getName() + ",生成订单ID:" + number); } catch (Exception e) { e.printStackTrace(); } finally { zkLock.unLock(); } } public static void main(String[] args) { System.out.println("####生成唯一订单号###"); for (int i = 0; i < 100; i++) { new Thread(new OrderService()).start(); } }效果:
代码:https://github.com/xuexionghui/zookeepertest.git