• 分布式锁的三种实现方式


    1、基于数据库实现分布式锁

    1.2、基于数据库表

      要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。创建一张像这样的表

    CREATE TABLE `methodLock` (
        `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
        `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
        `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
        `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON 
        UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

      当我们想要锁住某个方法时,执行以下SQL:

    insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

      因为对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

      当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:

    delete from methodLock where method_name ='method_name'

       上面这种简单的实现有以下几个问题:

    1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

    2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

    3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

    4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

    当然,我们也可以有其他方式解决上面的问题。

    针对 数据库是单点问题搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。

    针对 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

    针对 非阻塞的?搞一个while循环,直到insert成功再返回成功。

    针对 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

    1.2 基于数据库排他锁

      除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:

    public boolean lock(){
        connection.setAutoCommit(false)
        while(true){
            try{
                result = select * from methodLock where method_name=xxx 
                for update;
                if(result==null){
                    return true;
                }
            }catch(Exception e){
    
            }
            sleep(1000);
        }
        return false;
    }

      在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:

    public void unlock(){
        connection.commit();
    }

      通过connection.commit()操作来释放锁。

      这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。

      阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。

      锁定之后 服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。

      但是还是无法直接解决数据库单点和可重入问题。

    1.3 总结

      总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。

      数据库实现分布式锁的 优点: 直接借助数据库,容易理解。

      数据库实现分布式锁的 缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。

      操作数据库需要一定的开销,性能问题需要考虑。

    2、基于Redis实现分布式锁
     
      通过setnx设置lock(设置一个适当的超时时间),设置成功说明获取锁,然后执行业务逻辑,执行完del lock。

    3、基于zookeeper实现分布式锁
      
      基于zookeeper临时有序节点可以实现的分布式锁。大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。
     
    当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

      来看下Zookeeper能不能解决前面提到的问题。

    锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

    非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可

    以执行业务逻辑了。

    不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到

    锁,如果不一样就再创建一个临时的顺序节点,参与排队。

    单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

    public boolean tryLock(long timeout, TimeUnit unit) throws 
    InterruptedException {
        try {
            return interProcessMutex.acquire(timeout, unit);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
    public boolean unlock() {
        try {
            interProcessMutex.release();
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
        } finally {
            executorService.schedule(new Cleaner(client, path), 
            delayTimeForClean, TimeUnit.MILLISECONDS);
        }
        return true;
    }

      Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

    使用ZK实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务

    那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

    总结

      使用Zookeeper实现分布式锁的优点: 有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

      使用Zookeeper实现分布式锁的缺点 : 性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。

    4、三种方案的比较
      从理解的难易程度角度(从低到高): 数据库 > 缓存 > Zookeeper

      从实现的复杂性角度(从低到高): Zookeeper >= 缓存 > 数据库

      从性能角度(从高到低): 缓存 > Zookeeper >= 数据库

      从可靠性角度(从高到低): Zookeeper > 缓存 > 数据库

  • 相关阅读:
    Linux文件系统的层级结构
    3.求m+mm+mmm+…+m…m(n个)的和,其中m为1~9之间的整数。 例如,当m=3、n=4时,求3+33+333+3333的和。
    编写一个Java程序,计算一下1,2,…,9这9个数字可以组成多少个互不相同的、无重复数字的三位偶数。
    抽象类和抽象方法
    定义一个“点”(Point)类用来表示三维空间中的点(有三个坐标)。要求如下: (1)可以生成具有特定坐标的点对象。 (2)提供可以设置三个坐标的方法。 (3)提供可以计算该“点”距原点距离平方的方法。 (4)编写主类程序验证。
    (1)定义一个接口CanFly,描述会飞的方法public void fly(); (2)分别定义类飞机和鸟,实现CanFly接口。 (3)定义一个测试类,测试飞机和鸟,在main方法中创建飞机对象和鸟对象, 再定义一个makeFly()方法,其中让会飞的事物飞。并在main方法中调用该方法, 让飞机和鸟起飞。
    (1)定义一个接口Compute含有一个方法int computer(int n,int m); (2)设计四个类分别实现此接口,完成+-*/运算 (3)设计一个类UseCompute,含有方法: public void useCom(Compute com, int one, int two) (4)设计一个测试类
    完成Adventure中的主方法
    中国特色社会主义的体制中有这样的现象:地方省政府要坚持党的领导和按 照国务院的指示进行安全生产。请编写一个java应用程序描述上述的体制现象。
    (1)编写一个接口:InterfaceA,只含有一个方法int method(int n); (2)编写一个类:ClassA来实现接口InterfaceA,实现int method(int n)接口方 法时,要求计算1到n的和; (3)编写另一个类:ClassB来实现接口InterfaceA,实现int method(int n)接口 方法时,要求计算n的阶乘(n!); (4)编写测试类E测试
  • 原文地址:https://www.cnblogs.com/jing-yi/p/12986122.html
Copyright © 2020-2023  润新知