基于数据库表

要实现分布式锁,最简单的方式就是直接创建一张锁表,然后通过操作该表中的数据来实现加锁和解锁。当要锁住某个方法或资源时,就向该表插入一条记录,表中设置方法名为唯一键,这样多个请求同时提交数据库时,只有一个操作可以成功,判定操作成功的线程获得该方法的锁,可以执行方法内容;想要释放锁的时候就删除这条记录,其他线程就可以继续往数据库中插入数据获取锁。
创建这样一张表:

CREATE TABLE `method_lock` (
    `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `method_name` VARCHAR(64) NOT NULL COMMENT '方法名',
    `method_desc` VARCHAR(1024) NOT NULL COMMENT '方法描述',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE INDEX `uniq_method_name` (`method_name`)
)
COMMENT='分布式锁'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;

加锁:

insert into method_lock(method_name, method_desc) values("methodName", "desc");

解锁:

delete from method_lock where method_name = "methodName";

缺陷:

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

改进:

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

基于数据库排他锁

还是用上面那个数据表,这里通过数据库的排他锁来实现分布式锁,基于mysql的InnoDB引擎,使用如下方法来加锁:

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

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

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

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

阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。
但是还是无法直接解决数据库单点和可重入问题。

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




 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐