基于数据库的分布式锁实现

    科技2022-07-10  106

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

    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'

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

    这把锁依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用这把锁没有失效时间,一旦解决操作失败,就会导致记录一直在数据库中,其他线程无法在获得锁这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁的操作这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据库表中数据已经存在了

    针对以上几点,因此有其他方式解决上面的问题:

    数据库是单点?那就搞两个数据库,数据库之前双向同步,一旦挂掉快速切换到备库上没有失效时间?可以做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍非阻塞?可以写一个while循环,直到insert成功再返回成功非重入?可以在数据库表中加一个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库中可以查到的话,就直接把锁分配给它即可

    二、基于数据库表做乐观锁   系统认为数据的更新在大多数情况下是不会产生冲突的,只在数据库更新操作提交的时候才对数据作冲突检测。如果检测的结果出现了与预期数据不一致的情况,则返回失败信息。   乐观锁大多数是基于数据版本(version)的记录机制实现的。何谓数据版本号?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个 “version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。在更新过程中,会对版本号进行比较,如果是一致的,没有发生改变,则会成功执行本次操作;如果版本号不一致,则会更新失败。

    使用版本号实现乐观锁   使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号。查询出商品信息:

    CREATE TABLE `optimistic_lock` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `resource` int NOT NULL COMMENT '锁定的资源', `version` int NOT NULL COMMENT '版本信息', `created_at` datetime COMMENT '创建时间', `updated_at` datetime COMMENT '更新时间', `deleted_at` datetime COMMENT '删除时间', PRIMARY KEY (`id`), UNIQUE KEY `uiq_idx_resource` (`resource`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';

      除此之外借助更新时间戳(updated_at)也可以实现乐观锁,和采用version字段的方式相似:更新操作执行前线获取记录当前的更新时间,在提交更新时,检测当前更新时间是否与更新开始时获取的更新时间戳相等。

    优点与不足   乐观锁的优点比较明显,由于在检测数据冲突时并不依赖数据库本身的锁机制,不会影响请求的性能,当产生并发且并发量较小的时候只有少部分请求会失败。缺点是需要对表的设计增加额外的字段,增加了数据库的冗余,另外,当应用并发量高的时候,version值在频繁变化,则会导致大量请求失败,影响系统的可用性。所以综合数据库乐观锁的优缺点,乐观锁比较适合并发量不高,并且写操作不频繁的场景。

    三、基于数据库表做悲观锁(排他锁)   除了可以通过增删操作数据库表中的记录以外,还可以借助数据库中自带的锁来实现分布式锁。基于MySQL的InnoDB引擎,可以使用以下方法来实现加锁操作:

    public boolean lock(){ Connection.setAutoCommit(false); while (true) { try { result = select * from MethodLock where methodName = 'xxxx' for update; if (result == null) { return false; } } catch (Exception e) { } sleep(1000); } returnType false; }

      在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里希望使用行级锁,就要给method_name添加索引。这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。   可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:

    public void unlock(){ connection.commit();//通过connection.commit()操作来释放锁 }

      这里还存在另外一个问题,虽然对method_name 使用了唯一索引,并且显式使用for update来使用行级锁。但是,MySQL会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。   还有一个问题,就是要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆。

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

    阻塞锁?for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉

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

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

    Processed: 0.009, SQL: 8