分布式锁含义
分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
分布式锁特点
- 互斥性:在任意一个时刻,只有一个客户端持有锁,但是分布式锁需要保证在不同节点的不同线程的互斥。
- 可重入性: 同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
- 锁超时:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取。
- 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
- 支持阻塞和非阻塞(可选):和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
- 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
分布式锁的实现
MySql
- 基于表主键唯一做分布式锁(乐观锁):利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。
- 基于表字段版本号做分布式锁(悲观锁)
- 基于数据库排他锁做分布式锁:在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,通过connection.commit()操作来释放锁。但是如果MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁,会有问题;排他锁也会占用数据库连接数。
Memcache(add)
ZooKeeper(临时节点)
Redis(setnx),衍生开源库Redisson
原生redis(setnx):
value=当前线程id,同时设置过期时间。如果设置失败,判断值是否当前线程id
1
set lockName value ex 5 nx
释放锁的时候需要先比对锁值是否等于当前线程id,是才del锁。(保证原子操作,lua)
- 保证锁过期时间大于业务处理时间。(开一个线程刷新过期时间)
Redisson库:
自研分布式锁:如谷歌的Chubby。
分布式锁的安全问题
- 长时间的GC pause 利用自增序列的方案。
- 时钟发生跳跃
对于Redis服务器如果其时间发生了向跳跃,那么肯定会影响我们锁的过期时间,那么我们的锁过期时间就不是我们预期的了,也会出现client1和client2获取到同一把锁,那么也会出现不安全。
通过人为调整、NTP自动调整。 - 长时间的网络I/O
和GC的STW很像,也就是我们这个获取了锁之后我们进行网络调用,其调用时间由可能比我们锁的过期时间都还长,那么也会出现不安全的问题。
可以通过控制网络调用的超时时间解决这个问题。
参考:
redis系列:分布式锁
再有人问你分布式锁,这篇文章扔给他
拜托,面试请不要再问我Redis分布式锁的实现原理!【石杉的架构笔记】