提到锁,我们最先想到的是Java的synchronized关键字和JUC包中的ReentrantLock,这两个锁可以满足我们在多线程中对共享资源的安全访问,但是随着分布式的发展,本地锁已经没办法满足我们的需求了。为了在分布式环境也能对一个共享资源进行安全访问,我们需要引入分布式锁。
分布式锁特点
- 互斥性:和本地锁一样,必须保证只有一个线程能够获取到锁。
- 可重入性:同一个节点的同一个线程获取到锁之后可以再次获取同一把锁。
- 锁超时:分布式锁应该有一个超时时间,这样可以防止死锁。
- 高效,高可用:加锁和解锁必须性能高效,同时也需要保证高可用防止分布式锁失效。
- 支持阻塞和非阻塞(可选):和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
- 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般可以不用实现。
分布式锁实现方式
分布式锁的实现方式有很多种,一般主要有以下几种:
- 基于数据库实现
- 基于Redis实现
- 基于Zookeeper实现
- 自研分布式锁(如谷歌的Chubby)
基于数据库实现
基于数据库实现分布式锁主要有两张方案,但是他们也都有一些缺陷。
- 利用主键唯一规则:
利用数据库主键唯一规则,当有多个插入请求同时提交到数据库时,数据库可以保证只有一条数据能插入成功,这样可以认为插入数据成功的那个线程获取到了锁,当方法执行完毕后,删除这条数据库记录即可释放锁。
这种方式依赖数据库的可用性,所以要保证高可用就必须部署数据库集群。其次这把锁没有失效时间,一旦解锁失败,会导致锁一直存在,其他线程不能再次获取锁,解决方案是有定时任务一直去删除过期的锁。另外要保证阻塞的话,需要我们手动去循环获取锁。这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了,要解决这个需要记录获取锁的主机信息以及线程信息,并同时用一个count字段记录获取锁的次数。最后这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁,要实现成公平锁需要另外建一张表顺序记录获取锁的线程。
- 利用数据库行锁特性:
在查询语句后面显示加for update,这样可以利用行级锁的排他性来实现分布式锁,需要释放锁的时候直接commit就可以了。
这种方式有两个比较大的问题:一是利用事务进行加锁的时候,query需要占用数据库连接,在行锁的时候连接不释放,这就会导致连接池爆满。二是mysql使用行锁时默认要走索引,但是有时mysql根据执行计划认为全表扫描效率更高的时候就会将行锁升级为表锁,解决这个问题的话需要我们查询的时候显式制定索引。
基于Redis实现
基于redis来实现分布式锁主要方案就是:
- 加锁:
使用SET key value [EX seconds] [PX milliseconds] [NX] 命令来实现加锁。这个命令一共五个参数:第一个为key,我们使用key来当锁,因为key是唯一的;第二个为value,我们传的是requestId,这个requestId在解锁时需要用到,保证解锁的是加锁的那个线程;第三四个代表设置过期时间;最后一个参数需要使用NX,代表当key不存在的时候写入。
有些人会使用setnx和expire两个命令代替上面的方式,但是这样会有一个问题,由于这两个操作不是原子的,如果在setnx之后expire失败了,就会导致锁没有过期时间,这样会造成死锁。
- 解锁:
我们使用Lua脚本来实现解锁操作
1 | String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; |
解锁操作使用eval命令执行一段Lua脚本,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。使用Lua脚本可以保证原子性;
基于redis实现分布式还有其他方式,比如基于Redlock和基于redisson来实现,这两种方式没做研究,这里不做过多介绍。
基于Zookeeper实现
Zookeeper是一个分布式一致性协调框架,主要可以实现选主、配置管理和分布式锁等常用功能,因为Zookeeper的写入都是顺序的,在一个节点创建之后,其他请求再次创建便会失败,同时可以对这个节点进行Watch,如果节点删除会通知其他节点抢占锁。Zookeeper实现分布式锁虽然是比较重量级的,但实现的锁功能十分健全。
总结
基于数据库实现的方式还是比较复杂,而且性能也不高,不推荐使用。
基于redis实现的方式比较简单,性能也比较好,引入redis集群可以保证高可用,推荐大家使用redis的方式实现。
基于Zookeeper实现的方式比较重,同时还需要维护Zookeeper集群,实现起来还是比较复杂的,实现不好的话还会引起“羊群效应”。如果不是原有系统就依赖Zookeeper,同时压力不大的情况下。一般不使用Zookeeper实现分布式锁。