如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。
比如/lock/001释放了,/lock/002监听到时间,此时节点集合为[/lock/002,/lock/003],则/lock/002为最小序号节点,获取到锁。
整个过程如下:
Curator
Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。
他的使用方式也比较简单:
InterProcessMutex ipm = new InterProcessMutex(client,"/d_lock"); ipm.acquire(); ipm.release();其实现分布式锁的核心源码如下:
while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) { // 获取当前所有节点排序后的集合 List<String> children = getSortedChildren(); // 获取当前节点的名称 String sequenceNodeName = ourPath.substring(basePath.length() + 1); // 判断当前节点是否是最小的节点 PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases); if ( predicateResults.getsTheLock() ) { // 获取到锁 haveTheLock = true; } else { // 没获取到锁,对当前节点的上一个节点注册一个监听器 …… } } 2.3 基于数据库实现 2.3.1 基于数据库表的排它锁锁信息存储表结构如下:
CREATE TABLE `t_ms_lock` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT \'主键\', `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_name` (`name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=\'锁定中的方法\';当我们想要锁住某个方法时,执行以下SQL:
insert into t_ms_lock(name,desc) values (\'name\',\'desc\');
因为我们对name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:
delete from t_ms_lock where name =\'name\';
上面这种简单的实现有以下几个问题:
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用;
对于数据库是单点可以搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上;但切换也是单点?
这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁;
对于没有失效时间,只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;
这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作;
对于非阻塞:搞一个while循环,直到insert成功再返回成功;
这把锁是不可重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
对于非重入, 在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
2.3.2 基于数据库的排它锁除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。
基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:
public boolean lock(){ connection.setAutoCommit(false) while(true){ result = select * from t_ms_lock where name=xxx for update; if(result==null){ return true; } } return false; }在查询语句后面增加forupdate,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上)
我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:
public void unlock(){ connection.commit(); }