redis经典面试问题(六)

259 阅读5分钟

常见面试问题

  • 讲讲redis分布式锁的实现?

为什么要使用分布式锁

为什么要使用分布式锁?在java单进程多线程(单机服务)情况下,为了防止多个线程共同竞争同一个资源,我们需要使用锁,例如使用synchronized或者reentrantlock,但是我们现在大部分的应用都基本上是分布式多节点的,那这个时候就是多进程了,普通的单机锁是没办法满足我们的应用场景的。因此我们需要分布式锁,常用的分布式锁解决方案有三种,分别是基于数据库、redis、zookeeper,这里我们主要讨论redis分布式锁。

  • 小插曲

Martin Kleppmann 是英国剑桥大学的分布式系统的研究员,之前和Redis之父Antirez进行过关于RedLock(红锁,后续有讲到)是否安全的激烈讨论。Martin 认为一般我们使用分布式锁有两个场景:

  • 效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
  • 正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。

分布式锁需满足条件

我们如何保证分布式锁的可靠性呢?那需要满足以下几个条件:

  • 互斥性:分布式锁需要保证在不同节点的不同线程的互斥。
  • 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
  • 锁超时:和本地锁一样支持锁超时,防止死锁。
  • 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
  • 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
  • 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。

redis分布式锁的简单实现

redis分布式锁主要是利用setnx这个命令来实现的,这里有个问题,加锁了之后如果机器宕机那么这个锁就不会得到释放,所以我们需要加上过期时间。

  • 加锁:在 Redis 2.8 之前我们需要使用 Lua 脚本达到我们的目的,但是 Redis 2.8 之后 Redis 支持 nx 和 ex 操作是同一原子操作(set命令)。
result = ((Jedis) nativeConnection).set(key, uuid, "NX", "PX", expire);

这里要注意的是我们set的value为什么是uuid,是因为在我们释放锁的时候我们需要判断是不是那个拿到锁的人,如果设置同样的value就有可能发生a线程加锁,b线程进行解锁了。(温馨提示:考虑严密一点还需要考虑分布式环境下uuid的唯一性,例如使用雪花算法)

  • 释放锁:注意释放锁并不是直接使用del就ok了,前面我们说了我们释放锁的时候需要判断是不是加锁的那个人,所以直接用del是没办法保持原子性的,这个时候我们还是需要使用lua脚本。
UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == ARGV[1] " +
                "then " +
                "    return redis.call(\"del\",KEYS[1]) " +
                "else " +
                "    return 0 " +
                "end ";

Redission

Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持。Redission 也是 Redis 的客户端,相比于 Jedis 功能简单。Jedis 简单使用阻塞的 I/O 和 Redis 交互,Redission 通过 Netty 支持非阻塞 I/O,Redission 封装了锁的实现,其继承了 java.util.concurrent.locks.Lock 的接口,让我们像操作我们的本地 Lock 一样去操作 Redission 的 Lock,在实际应用中我们可以考虑使用Redission。

其他问题

上面的实现在单机的redis环境下是没有任何问题的,但是实际生产中我们的redis往往都是集群部署,那么问题又来了,如果Redis的master节点在锁未同步到Slave节点的时候宕机了怎么办呢?在这种情况下,redis官方为我们提供了另一种解决方案:RedLock算法。RedLock 基本原理是利用多个 Redis集群,用多数的集群加锁成功,减少Redis某个集群出故障,造成分布式锁出现问题的概率。这里我们具体不展开聊了,感兴趣可以去官网看看https://redis.io/topics/distlock。其实RedLock也不是完全安全的,前面我们说到Martin Kleppmann和redis之父产生过激烈的讨论,Martin Kleppmann指出了不安全的一些情况,例如长时间gc,时钟发生跳跃等等都会使RedLock不安全,这里我们也不深入讨论了,感兴趣可以到这里看看。(https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

使用总结

如果不需要特别复杂的要求,自己就可以利用 set和lua脚本 进行实现,如果自己需要复杂的需求的话,可以利用或者借鉴 Redission。对于一些要求比较严格的场景可以使用RedLock。对于不同的业务需要的安全程度完全不同,我们需要根据自己的业务场景,通过不同的维度分析,选取最适合自己的方案,总而言之,使用Redis分布式锁实在不是一个好的选择,Redis设计的初衷也并不是满足分布式锁的需求.对于需求性能的分布式锁应用它太重了且成本高;对于需求正确性的应用来说它不够安全.如果你的应用只需要高性能的分布式锁并且不要求多高的正确性,那么单节点的Redis分布式锁足够了;如果你的应用想要保证正确性,那么不建议 RedLock,建议使用一个合适的一致性协调系统,比如基于Zookeeper的分布式锁。