基本要求:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
Redis --Jedis实现
- jedis.set(lockKey, lockId, “NX”,“PX”, 15000)。格式 - String set(String
key, String value, String nxxx, String expx, long time); 功能 -
存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。参数 - key :redis key value :redis值
nxxx:只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
expx:只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。time:过期时间,单位是expx所代表的单位。 - String script = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then
return redis.call(‘del’, KEYS[1]) else return 0 end”; Object result
= jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
用一个Lua代码执行解锁,保证了命令的原子性。该命令执行时会检查锁的value是否和解锁传的id一致,相同才能完成解锁,即删除key。 - 高并发优化思路:对锁的资源进行拆分加锁。比如限制库存超卖,把1000库存锁改为10个100库存,每次对其中的100个库存进行加锁和释放锁,这样可以提高并发性能,这种方法需要在其中一个库存组返回库存不够时释放锁,并对其他库存组再次进行加锁操作,保证1000个库存都卖光。
- 此方式有个弊端,若加锁设置过期时间为10s,但是业务处理时间超过10s时,redis会主动释放锁,此时可能由其他线程获取锁并进行业务操作,这样会造成业务逻辑错乱。
- 推荐使用Redisson框架,所有的指令都是通过Lua脚本执行,保证了命令的原子性,并且有Watchdog存在,他会在获取锁之后每隔10s延迟当前锁的过期时间,保证持有锁的线程能在完成业务逻辑之后才释放锁。即使宕机,Watchdog也就没了,超过了过期时间会自动释放锁。
zookeeper --curator框架
-
通过在zookeeper里其中一个节点作为锁节点,创建临时顺序节点来实现。
-
客户端A先在锁节点下创建一个临时顺序节点,节点名类似于xxxx-00001最后的数字是依次顺序递增的,创建成功后会查询这个锁节点下所有的子节点,然后判断子节点集合里的第一个节点是否为自己创建的这个,如果是的话则说明可以进行加锁,客户端A加锁成功。
-
在客户端A加锁成功,且并未释放锁期间,客户端B尝试加锁,也在这个锁节点下创建一个临时顺序节点,节点名类似于xxxx-00002,并获取当前锁节点下的所有子节点,发现第一个子节点并不是自己创建的,此时,客户端B会通过zk的api对自己节点的上一个顺序子节点添加一个监听器。
-
客户端A在执行完逻辑后进行解锁,即把自己创建的这个xxxx-00001临时顺序节点给删除,节点被删除会触发监听器,zk会通知监听这个节点的客户端B锁已被释放。
-
此时客户端B会再次获取锁节点下所有的子节点,并判断第一个顺序节点是否为自己创建的,是则进行加锁成功,执行逻辑后再释放锁。
-
使用临时顺序节点的另外一个用意是,当客户端创建了临时顺序节点后不小心自己宕机了,zk会自动感知到这个客户端已宕机,会自动删除这个临时顺序节点,相当于自动释放锁。
redis和zookeeper的优劣
- redis方式其他线程竞争锁资源时,会不断尝试进行加锁,比较消耗性能。而zookeeper方式通过监听器,只有在其他线程释放锁之后才会再次尝试进行加锁。
- redis方式在某些极端情况下加锁解锁可能会出现问题。
- zookeeper方式若有较多客户端进行加锁,对zk的leader压力会比较大。