全局锁

在系统访问单个资源时或多或少都会要使用到锁,如Java的Lock等,但多个系统访问资源,或在集群中各个实例需要访问资源时,就需要建立全局的锁,这里讲三种全局锁的方法。

数据库

利用ACID

使用关系型数据库的ACID可以创建一个锁

UPDATE LOCKTABLE SET INSTANCE=<instance_name> WHERE RESOURCE=XXXX AND INSTANCE IS NULL;

当返回更新了相应记录后就代表获得了锁
对应的可以使用以下sql来释放获得的锁

UPDATE LOCKTABLE SET INSTANCE='' WHERE RESOURCE=XXXX AND INSTANCE=<instance_name>;

上面的方法看上去很好,但是当获得锁的实例宕机那么这个锁就一直被占用着

利用行锁

为了解决实例非正常退出而没有释放锁可以使用数据库(ORACLE)的行锁

SELECT 1 FROM LOCKTABLE WHERE RESOURCE=XXXX FOR UPDATE;

这样在commit/rollback之前就能持有这个锁,如果调用方断开,数据库也会自动rollback。可以使用NOWAIT+循环查询的方式防止阻塞

REDIS

数据库固然可以,但应对大量的资源需要长期持有大量锁也不是很恰当,下面看下Redis如何创建全局锁

SETNX

SETNX是set if not exist的缩写,也就是当值不存在时再进行赋值

SETNX lock.resource 1
//hold the lock
DEL lock.resource

以上伪代码简单演示了如何获得锁和释放锁,和数据库的方法一样,这种方法同同样存在实例宕机的风险导致死锁

SET

可惜Redis上没有像数据库中的for update。一个替代方法是使用expire。即给锁设定一个超时时间,如果时间超过自动释放锁,这里超时时间要合适不能过长让其他实例空等,也不能过短实例没有结束就自动释放了。
幸运的是Redis 2.6.12之后SET命令可以使用expire和notexist

SET lock.resource <instance_name> NX EX timeout
//hold the lock
WATCH lock.resource
GET lock.resource
MULTI
if(getResult==<instance_name>)
	DEL lock.resource
EXEC

使用watch/multi确保竞态条件

RedLock

防止单Redis不可用,可以使用多个redis,在半数以上节点获得锁的情况下代表获得锁,否则就释放所有获得的锁。

Redison

如果不想自己造轮子,已经有现成的类库可以使用Redis创建全局锁了
Redison封装了锁的实现,提供可重入的锁的一系列实现,可以方便地使用
https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

Zookeeper

redis的不足就是只能通过expire来控制锁持有者失联的情况。
zookeeper在这方面就有一定的优势,再加上zookeeper天生自带集群,在可靠性上优于redis
zookeeper可以创建ephemeral节点,当客户端断开连接节点自动删除,可以创建一个节点,最小值持有当前锁

create -e -s /LOCK/RESOURCE/REQUEST 1

之后判断如果当前节点最小就获得锁,如果没有就在前一个节点上加watch,在watch中再进行判断,这样就实现了等待获得锁的队列。

总结

全局锁在集群上的应用有不少,最常见的就如集群内CRON任务执行的管理等。这里主要介绍的还是悲观锁,在某些场景也可以使用乐观锁进行优化。