什么是分布式锁
我们在开发应用的时候,如果需要对某一个共享数据进行多线程同步访问(增加、减少等)的时候,可以使用锁进行处理,防止在多线程环境下数据表现不一致。如果该应用部署在多个节点上面,我们就不能运用只是在单机上面表现的锁。解决分布式应用下数据一致性问题的锁称为分布式锁。
对变量A的操作锁定就需要用到分布式锁
常见的实现方案
- 基于数据库实现分布式锁基于数据库实现分布式锁我们可以有两种方案
- 乐观锁:通过数据的版本号进行判断是否给与操作
- 悲观锁:可以用for update语句数据库自带的排它锁,也可以自行设计逻辑标识,例如创建一个表,给一个字段上唯一约束,加锁就是向该表插入数据,该字段就是锁的标识,同一时间多个请求提交到数据库由于有唯一约束也只会有一个请求数据插入成功,也就代表加锁成功,解锁就是删除数据。
- 基于Redis实现分布式锁
基于Redis实现的锁机制,主要是依赖redis自身的原子操作。可以通过向Redis里面存入一个值来代表加锁,值如果存入成功表示加锁成功,存入的时候如果值已经存在就不能存入,代表加锁失败,解锁就是删除改值。可以给值设置过期时间来防止死锁。 - Zookeeper分布式锁
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。
当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录(locker目录)下生成一个唯一的临时有序节点(locker/node_N),然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。
当释放锁的时候,只需将这个临时节点删除即可。
三种方案的对比
- 数据库
实现简单,数据库性能存在瓶颈,不适合高并发场景,锁的失效时间难以控制,删除锁失败容易导致死锁。即这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。一般在分布式系统中使用这种机制实现分布式锁时,需要业务侧增加控制锁超时和重试的流程。 - Redis
性能好,实现起来较为方便。单点问题。这里的单点指的是单master,就算是个集群,如果加锁成功后,锁从master复制到slave的时候挂了,也是会出现同一资源被多个client加锁的。redis的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题,不够健壮。即便使用redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题。 - Zookeeper
能有效的解决单点问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。性能上不如使用缓存实现分布式锁,如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。
如何选择?
如果系统不想引入过多网元,可以采用数据库锁实现,好处就是比较容易理解,但是这种方案业务层控制逻辑多且复杂,需要对业务侧足够了解,易于理解但是实现复杂度最高。
如果追求高性能,Redis是最佳选择,但是redis是有可能存在隐患的,可能会导致数据不对的情况,可靠性不如ZK。
如果系统已经存在ZK集群,优先选用ZK实现,实现最简单,且可以提供高可靠性,性能稍逊Redis缓存方案。
Redis实现分布式锁的原理以及流程
如何通过Redis来进行加锁?我们其实可以通过向Redis里面存一个值(Key-Value)来表示一把锁,key就是锁的标识(一般可以通过业务中的数据ID跟上前缀或则后缀的方式),Value可以是一个唯一值(可以用UUID),用来标识加锁方是谁。
具体加锁步骤可以如下:
- 准备向Redis里面存入一个值
- 判断如果该值存在就说明已经有其他线程加锁成功,现在不能存入只能等待或则加锁失败
- 如果该值不存在则当前线程存入成功(加锁成功)
- 对该值设置过期时间(锁可以在一定时间之后自动释放)
我们需要注意的是以上步骤的执行必须具备原子性,不然加锁逻辑自身就会导致冲突,如何保证原子性,我们可以使用set命令参数如下:
SET KEY VALUE EX 20 NX
如果是在SpringBoot里面我们可以调用方法:
stringRedisTemplate.opsForValue().setIfAbsent(key, value, 20, TimeUnit.SECONDS)
Redis加锁思路其实就是存入代表锁的值,谁存入了谁就拥有这把锁。解锁就是删除该值。
