在传统单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。
但是在分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁的由来。
当多个进程不在同一个系统中,就需要用分布式锁控制多个进程对资源的访问。最实际的应用场景应该是控制库存,比如说电商平台防止商品超卖这类场景。
互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
不会死锁:获取锁的客户端因为某些原因(如down机等)而未能释放锁,也能保证后续其他客户端能加锁。
容错:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
实现命令如下
seynx key value
expire key time
setnx其实就是set if not exists的意思,仅当key不存在的时候才能进行插入,执行完操作后可以将锁删除
expire命令为锁设置过期时间
del key
这样就能重新对key的值进行操作了
上面那种实现方式存在一种问题,如果在执行完 setnx 之后,执行expire之前,服务器 crash 或重启了导致加的这个锁没有设置过期时间,就会导致死锁的情况(别的线程就永远获取不到锁了)
代码如下:
jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多人可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到分布式锁要满足的条件时,提到的安全性这一条,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
我们的加锁代码满足我们可靠性里描述的三个条件:
方案二还是可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实问题的关键就在于我们不确定要设置多长时间,时间太短就会导致锁过期释放,业务没执行完,时间太长就会使系统运行效率下降. 我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。Redission框架帮我们实现了此功能,名为看门狗
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
这个时候我们就可以使用RedLock,它的思路是,在多个Redis服务器上保存锁,只需要超过半数的Redis服务器获取到锁,那么就真的获取到锁了,这样就算挂掉一部分节点,也能保证正常运行,保证了容错性。