分布式锁 之 数据库实现

一、基于数据库实现

1、方案1:基于数据库唯一性约束实现

说明:可以基于数据库唯一键特性实现分布式锁。

步骤1:建表

CREATE TABLE `methodLock` (

`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',

`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',

`desc1` 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='锁定中的方法';

此方案中InnoDB 特性不是必须的

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

insert into methodLock(method_name,desc1) values ('method_name','desc1')

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

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

delete from methodLock where method_name ='method_name'

方案总结

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

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

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

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

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

问题5:这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。

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

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

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

问题3:非阻塞的?搞一个while循环,直到insert成功再返回成功。

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

问题5:非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁

2、方案2:基于数据库排他锁实现

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

步骤1:建表(建表语句中绿色的是关键)

CREATE TABLE `methodLock` (

`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',

`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',

`desc1` 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='锁定中的方法';

步骤2:使用下面的方式加锁

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;

}

//注意1:上面是伪代码

//注意2:经过测试,如果select未能获取锁,会阻塞,直到成获取锁,但是好像有获取不到锁时阻塞等待的超时时间(所以上面的java代码会进行循环获取):

分布式锁 之 数据库实现

 

 

上面是在java代码中通过connection.setAutoCommit(false)+connection.commit()不进行自动提交,而自己控制事务,如果使用客户端测试,可以:

start TRANSACTION;

select * from methodLock where method_name='test' for update;

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

步骤3:执行完方法之后,再通过以下方法解锁:

public void unlock(){

connection.commit();

}

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

方案总结

这种实现可以解决【基于数据库唯一性约束实现】方案中以下几个问题:

这种方法可以有效的解决上面提到的

1、无法释放锁的问题(问题2):

(如果由于异常等原因未能执行commit操作,也会自动释放排他锁,释放后可以继续由其他事务获取):

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

2、阻塞锁的问题(问题3)

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

这种实现仍然存在的问题

问题1:无法直接解决数据库单点

问题4:可重入

问题5:公平锁的问题。

二、基于数据库实现总结

总结一下使用数据库来实现分布式锁的方式:

1、这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况(唯一键)确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。

2、数据库实现分布式锁的   优点

1)直接借助数据库,容易理解。

3、数据库实现分布式锁的   缺点

1)会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。

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