如果服务是单机模式的,也就是只有一个服务器,基于java的synchronized关键字,可以实现多个用户之间的并发调用,实际是使用了排它锁实现,保证每一个时刻只有一个用户可以操作。但是在集群环境下,后端存在多个服务器,前端经过负载均衡操作,多个用户并不会访问同一个服务器,所以synchd关键字无法使用,因为他不可能跨JVM使用,这时使用的策略多是采用分布式锁,例如使用数据库,Redis,Zookeeper等。
这里使用Redis实现作为例子。在调用一个接口时,例如Spring里面的RequestMappering指定某一个接口,内部可以使用包装的setnx命令(如果设置的key不存在,就设置一个key-value,如果存在不执行),进入接口时,设置一个key,完成业务代码时,删除这个key。因为该key的唯一性和Redis的单线程工作形式,保证了每一时刻只有一个用户能够获得执行的权限,也就是常说的分布式锁。实际使用时,可以调用java集成的Redis接口,调用相关方法可以获得返回值,如果true代表设置成功,执行代码,返回值为false则失败,判断并返回给用户执行失败的提示等操作。然而这个方法里面还存在很多细节:
使用try{} finally{},在try中包含执行任务代码,finally保证key的删除没有问题,以保证其它用户可以正常调用。
在设置这个key之后,为这个key设置一个超时时间,例如设置个2s,如果2s内还没有删除,则自动删除
将设置key -value和设置过期时间组合为一个原子操作,类似事务的概念,要么一起成功,要么一起失败,java的Spring生态中集成了Redis的相关API可以实现这个功能
在加锁的过程中,实际上就是在给 Key 键设置一个值;为避免死锁,要给 Key 键设置一个过期时间;为避免误删key,还需要在在删除key之前判断是否是拿到锁的客户端执行的操作
SETNX lock_key unique_value PX 10000
lock_key 就是 key 键;
unique_value 是客户端生成的唯一的标识;
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁
解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。这个时候, unique_value 的作用就体现出来,实现方式可以通过 lua 脚本判断 unique_value 是否为加锁客户端。
使用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
If redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
1、如何合理设置超时时间
设置锁的时候需要设置过期时间,防止出现死锁;但过期时间很难预估,具有不确定性,比如某天业务系统负载高,导致了业务处理时间拉的很长等;
可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可。
Redisson的分布式锁也是基于此思路实现;对于一般的分布式场景,可以直接使用redisson提供的分布式锁
2、Redis 如何解决集群情况下分布式锁的可靠性?
由于 Redis 集群(主从)数据同步到各个节点时是异步的,如果在 Redis主节点获取到锁后,在没有同步到从节点时,Redis 主节点宕机了,此时新选举的 Redis 主节点依然可以获取锁,所以多个客户端就可以同时获取到锁。
官方设计了一个分布式锁算法 Redlock 解决了这个问题,可通过Redisson引入
假设目前有 N 个独立的 Redis 实例, 客户端先按顺序依次向 N 个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,需要注意的是,Redlock 算法设置了加锁的超时时间,为了避免因为某个 Redis 实例发生故障而一直等待的情况
当客户端完成了和所有 Redis 实例的加锁操作之后,如果有超过半数的 Redis 实例成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功
3、采用哪种方案做自己的redis分布式锁使用?
脱离了具体业务谈论Redis分布锁的实现方案难免有所欠缺
对于一般业务,只需保证数据的最终一致性,使用普通的方案即可,比如使用set NX PX,再加上后台线程给锁续期的方案;既可以解决死锁问题,也可以处理锁提前释放问题;Redisson组件有这样现成的解决方案,开箱即用,非常简单
对于要求数据强一致性的业务,比如银行金钱相关;需要考虑锁的高可用、互斥性,典型的比如主从redis架构发生主从切换时,由于数据未及时同步导致的多个客户端拿到相同的锁;可以考虑Redlock的相关方案,Redisson组件也提供了Redlock相关的实现
具体的落地实施上面:推荐采用Redisson的redis锁实现方案,不需要自己重复造轮子,考虑的情况更多、异常情况也考虑的相对完善(通用组件的代码一般更鲁棒),如果你还在纠结,不妨试试Redisson
总结
在基于单个 Redis 实例实现分布式锁时,对于加锁操作,需要考虑四个条件:
加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,因此使用 SET 命令带上 NX 选项来实现加锁;
锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,因此在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,因此使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端。
由于过期时间的不确定性,需要考虑给锁定期续约的情况
基于主从或者集群的redis架构,由于数据的异步性,当主从节点切换时,可能出现多个客户端拿到同一把锁的情况。根据实际需求,如果可以容忍,采用类似于单机的实现方案即可;反之可以采用Redlock的实现方案