分布式锁的特点
原子性:同一时刻,只能有一个机器的一个线程得到锁;
可重入性:同一对象(如线程、类)可以重复、递归调用该锁而不发生死锁;
可阻塞:在没有获得锁之前,只能阻塞等待直至获得锁;
高可用:哪怕发生程序故障、机器损坏,锁仍然能够得到被获取、被释放;
高性能:获取、释放锁的操作消耗小。
目前主流的有三种
- 基于数据库实现
- 基于Redis实现
- 基于ZooKeeper实现
基于数据库实现:
基于数据库来做分布式锁的话,通常有两种做法:
- 基于数据库的乐观锁
- 基于数据库的悲观锁
乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。
当要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。
悲观锁也叫作排它锁,在Mysql中是基于 for update 来实现加锁的,例如:
//锁定的方法-伪代码
public boolean lock(){
connection.setAutoCommit(false)
for(){
result =
select * from user where
id = 100 for update;
if(result){
//结果不为空,
//则说明获取到了锁
return true;
}
//没有获取到锁,继续获取
sleep(1000);
}
return false;
}
//释放锁-伪代码
connection.commit();
上面的示例中,user表中,id是主键,通过 for update 操作,数据库在查询的时候就会给这条记录加上排它锁。需要注意的是,在InnoDB中只有字段加了索引的,才会是行级锁,否者是表级锁,所以这个id字段要加索引。当这条记录加上排它锁之后,其它线程是无法操作这条记录的。
那么,这样的话,就可以认为获得了排它锁的这个线程是拥有了分布式锁,然后就可以执行想要做的业务逻辑,当逻辑完成之后,再调用上述释放锁的语句即可。
基于Redis实现
基于Redis实现的锁机制,主要是依赖redis自身的原子操作,例如:
SET user_key user_value NX PX 100
上述代码示例是指,当redis中不存在user_key这个键的时候,才会去设置一个user_key键,并且给这个键的值设置为 user_value,且这个键的存活时间为100ms。
为什么这个命令可以帮实现锁机制呢?
因为这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。
解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。
另外,针对redis集群模式的分布式锁,可以采用redis的Redlock机制。
基于ZooKeeper实现
其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。
原理就是:当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。
当释放锁的时候,只需将这个临时节点删除即可。