什么是分布式锁,如何使用Redis实行分布式锁

一、什么是分布式锁?

        如果服务是单机模式的,也就是只有一个服务器,基于java的synchronized关键字,可以实现多个用户之间的并发调用,实际是使用了排它锁实现,保证每一个时刻只有一个用户可以操作。但是在集群环境下,后端存在多个服务器,前端经过负载均衡操作,多个用户并不会访问同一个服务器,所以synchd关键字无法使用,因为他不可能跨JVM使用,这时使用的策略多是采用分布式锁,例如使用数据库,Redis,Zookeeper等。

这里使用Redis实现作为例子。在调用一个接口时,例如Spring里面的RequestMappering指定某一个接口,内部可以使用包装的setnx命令(如果设置的key不存在,就设置一个key-value,如果存在不执行),进入接口时,设置一个key,完成业务代码时,删除这个key。因为该key的唯一性和Redis的单线程工作形式,保证了每一时刻只有一个用户能够获得执行的权限,也就是常说的分布式锁。实际使用时,可以调用java集成的Redis接口,调用相关方法可以获得返回值,如果true代表设置成功,执行代码,返回值为false则失败,判断并返回给用户执行失败的提示等操作。然而这个方法里面还存在很多细节:

  1. 执行过程中出现了异常怎么办?

使用try{}  finally{},在try中包含执行任务代码,finally保证key的删除没有问题,以保证其它用户可以正常调用。

  1. 执行过程中一个服务器突然宕机,这时key一直没有删除,怎么办?

在设置这个key之后,为这个key设置一个超时时间,例如设置个2s,如果2s内还没有删除,则自动删除

  1. 在SetNX和set expire设置过期时间的过程中宕机怎么办?

将设置key -value和设置过期时间组合为一个原子操作,类似事务的概念,要么一起成功,要么一起失败,java的Spring生态中集成了Redis的相关API可以实现这个功能

什么是分布式锁,如何使用Redis实行分布式锁_第1张图片

二、分布式锁-Redis实现方式?

        在加锁的过程中,实际上就是在给 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提供的分布式锁

2Redis 如何解决集群情况下分布式锁的可靠性?

由于 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的实现方案

 

你可能感兴趣的:(面试,redis,redis,分布式,java)