Redis笔记III| 青训营笔记

71 阅读6分钟

Redis分布式问题

分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源,一般来说,分布式锁需要满足的特性有这么几点:

  • 互斥性: 在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
  • 高可用性: 在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
  • 防止锁超时: 如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
  • 独占性: 加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。
  1. Redis如何实现分布式锁?

    setnx单机

    • 加锁:setnx key value,key不存在,设置value(加锁成功)。

    • 解锁:del key,通过删除键值释放锁。释放锁之后,其他客户端可以通过setnx命令进行加锁。

    • 独占性:value可以使用uuid保证唯一,用于标识加锁的客户端。保证加锁和解锁都是同一个客户端。

    • 解决死锁:设置键的过期时间来解决死锁的问题,防止持有的锁宕机后无法解锁从而引发死锁。

      使用watchDog机制实现锁的续期。当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。

    • 可重入性:使用Redis的哈希表存储可重入次数。

    • 自旋重试:利用Redis的发布订阅机制,订阅锁的释放。

    为了实现多节点Redis的分布式锁,Redis的作者提出了RedLock算法。

    RedLock主要解决主从切换后,锁失效的问题。

    1. 不再需要部署从库哨兵实例,只部署主库
    2. 但主库要部署多个,官方推荐至少 5 个实例
    1. 客户端先获取「当前时间戳T1」
    2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁。
    3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
    4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
    5. 加锁失败,向「全部节点」发起释放锁请求( Lua 脚本释放锁)

    场景:客户端A在成功获取锁后,如果所有Redis重启,这时客户端B就可以再次获取到锁,违背了互斥性。

    解决方法:开启AOF持久化,可以解决这个问题,但是AOF同步到磁盘上的方式默认是每秒一次,如果1秒内断电,会导致1秒内的数据丢失,如果客户端是在这1秒内获得的锁,立即重启可能会导致锁的互斥性失效,解决方法是每次Redis无论因为什么原因停掉都要等key的过期时间到了再重启(延迟重启),这么做的缺点就是在等待重启这段时间内Redis处于关闭的状态。

  2. 基于MySQL数据库实现分布式锁?

    主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。

    • 加锁: insert into distributed_lock(unique_mutex, holder_id) values (‘unique_mutex’, ‘holder_id’); 如果当前sql执行成功代表加锁成功,如果抛出唯一索引异常(DuplicatedKeyException)则代表加锁失败,当前锁已经被其他竞争者获取。
    • 解锁: delete from methodLock where unique_mutex=‘unique_mutex’ and holder_id=‘holder_id’; 解锁很简单,直接删除此条记录即可。
    • 可重入: 加锁时判断记录中是否存在unique_mutex的记录,如果存在且holder_id和当前竞争者id相同,则加锁成功。这样就可以解决不可重入问题。
    • 锁释放时机: 每次加锁之前我们先判断已经存在的记录的创建时间和当前系统时间之间的差是否已经超过超时时间,如果已经超过则先删除这条记录,再插入新的记录。另外在解锁时,必须是锁的持有者来解锁,其他竞争者无法解锁。这点可以通过holder_id字段来判定。
    • 利用主从复制解决单点问题。
  3. Zookeeper实现分布式锁?

    • 客户端 1 和 2 都尝试创建「临时节点」,例如 /lock
    • 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败
    • 客户端 1 操作共享资源
    • 客户端 1 删除 /lock 节点,释放锁

    优点:

    • 不需要考虑锁的过期时间
    • watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁

    缺点:

    • 性能不如 Redis、部署和运维成本高
    • 客户端与 Zookeeper 的长时间失联,锁被释放问题
  4. Redis并发竞争key问题应该如何解决?

    Redis并发竞争key就是多个客户端操作一个key,可能会导致数据出现问题,主要有以下几种解决办法:

    • 乐观锁,watch 命令可以方便地实现乐观锁。watch 命令会监视给定的每一个key,当 exec 时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。不能在分片集群中使用。
    • 分布式锁,适合分布式场景
    • 时间戳,适合有序场景,比如A想把key设置为1,B想把key设置为2,C想把key设置为3,对每个操作加上时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入了。
    • 消息队列,串行化处理。