前言
在工作中,我们或多或少都用到过锁,今天我们就来讨论分布式场景下,我们可以通过哪种方式来解决锁的问题,这也是我在面试中经常遇到的一个问题,搞定他,非常重要。
为什么需要锁
首先我们要搞懂,为什么需要锁?这是因为在同一时刻可能会有两个或两个以上的线程执行同一段代码,最经典的场景就是秒杀扣减库存,如果不对其加以控制,可能会出现期望之外的结果,比如超卖问题出现。
所以就算对并发编程没有系统学习过小伙伴,也会直接掏出万能方法-synchronized来,但是这种方式(JVM锁)只能解决单台服务器下的线程安全,如果是分布式场景下,这种方式肯定是无法满足的,这时候就需要用到分布式锁。
如何实现分布式锁
实现分布式锁的方式有很多,我们常见的有数据库、redis、zookeeper,但无论哪种方式,其核心思想是共同的,就是同一时刻只能有一个线程能够获取到锁。
比如现在有一个【下单系统】,分别在三台服务器上都部署一个实例,在同一时刻,每台服务器都想操作同一个订单的状态,但是这个时候只能有一台服务器能够操作成功,这时候就需要分布式锁的帮助了。
Redis分布式锁
redis用来做分布式锁是最常见的一种方式,之所以redis能够实现分布式锁,首先是因为它是单线程的,使用一个线程来处理所有的网络请求,因此也就不需要担心并发安全问题。
如上图,系统A在三台服务器分别部署一台实例,如果他们在同一时候都想修改某一个订单信息,那redis是通过哪种方式来实现分布式锁呢。
熟悉redis的小伙伴都知道,redis有一个命令【SET key 随机值 NX PX 1000】:
- NX:当key不存在的时,会设置成功。
- PX 1000:过期时间1000毫秒,当超过该时间,会自动释放。
通过这个命令,当第一个线程设置key时,redis服务返回OK,表示获取锁成功,在超时时间内,如果有其他服务器线程通过该命令,也来尝试获取锁时,redis服务会直接返回nil,表示当前锁被其他线程占用,获取失败。
面临的问题及解决
上述方式虽然可以满足分布式锁的需求,但是有几点问题需要我们注意:
第一点就是在设置value值时,必须使用随机值。
这是因为线程一拿到锁之后,在处理完自身业务后,会将该锁进行释放(主动删除redis中的key),但是有可能该线程阻塞了很长时间才处理完成,此时该可能已经被自动释放,并且被其他线程获取到,此时线程一直接删除key,必然会导致问题出现。
因此建议设置value为随机值,这样在删除key时通过lua脚本实现,删除前判断要删除的key的value值是否与当前线程的value值相同,只有在相同情况下才进行删除操作。
第二点就是redis单点故障。
因为如果是普通的redis单实例,那就是单点故障。或者是redis普通主从,那redis主从异步复制,如果主节点挂了,key还没同步到从节点,此时从节点切换为主节点,别人就会拿到锁。
RedLock原理介绍
该方案是redis官方支持的分布式锁算法,也是对上述方式的优化,这里花哥简单介绍一下原理。
如上图,假设现在有5个redis节点,节点之间相互独立,彼此之间不进行同步,某个线程如果想要成功获取到锁,需要完成以下几个步骤:
- 获取当前时间戳,单位是毫秒;
- 使用相同的key和随机value,轮流尝试在每个master节点上创建锁;
- 当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。;
- 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
- 如果由于某些原因未能获得锁,比如无法在至少N/2+1个Redis实例获取锁或获取锁的时间超过了有效时间,客户端应该在所有的Redis实例上进行解锁。
RedLock存在的问题
- 如果线程1从3个实例获取到了锁,但是这3个实例中的某个实例的系统时间走得稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有3个实例是空闲的,则线程2也可以获取到锁,则可能出现两个线程同时持有锁了。
- 如果线程1从3个实例获取到了锁,但是万一其中有1台重启了,则此时又有3个实例是空闲的,则线程2也可以获取到锁,此时又出现两个线程同时持有锁了。
总结
今天和大家分享了分布式锁中redis的实现方式,介绍了它的实现原理和需要注意点,不过说实话,redis用来做分布式锁个人认为并不是很完美,一般我也不这么用。至于为什么,除了redis自身的不足外,还要和其他实现方式进行对比取舍。