php多线程抓取网页(一下如何使用缓存(Redis)实现分布式锁(图))

优采云 发布时间: 2022-01-11 17:04

  php多线程抓取网页(一下如何使用缓存(Redis)实现分布式锁(图))

  分布式锁是指在分布式部署环境中,多个客户端可以通过锁机制互斥访问共享资源。

  目前比较常见的分布式锁实现方案有以下几种:

  下面介绍如何使用缓存(Redis)实现分布式锁。

  使用 Redis 实现分布式锁的最简单方法是使用命令 SETNX。SETNX(SET if Not eXist)的用法是:SETNX键值。只有当key key 不存在时,key key 的value 才设置为value。如果 key key 存在,SETNX 什么也不做。SETNX 设置成功返回,设置失败返回0。当你想获取锁时,直接使用SETNX获取锁。当要解除锁定时,使用 DEL 命令删除对应的键键。

  上述方案有一个致命的问题,就是线程由于一些异常因素(如宕机)而获得锁后,无法正常进行解锁操作,锁永远不会被释放。为此,我们可以为此锁添加超时。第一次我们会想到Redis的EXPIRE命令(EXPIRE键秒)。但是这里我们不能使用EXPIRE来实现分布式锁,因为它是和SETNX一起的两个操作,这两个操作之间可能会出现异常,所以还是达不到预期的效果。示例如下:

  // STEP 1<br style="-webkit-tap-highlight-color: transparent;">SETNX key value<br style="-webkit-tap-highlight-color: transparent;">// 若在这里(STEP1和STEP2之间)程序突然崩溃,则无法设置过期时间,将有可能无法释放锁<br style="-webkit-tap-highlight-color: transparent;">// STEP 2<br style="-webkit-tap-highlight-color: transparent;">EXPIRE key expireTime<br style="-webkit-tap-highlight-color: transparent;">

  正确的手势是使用命令“SET key value [EX seconds] [PX milliseconds] [NX|XX]”。

  从 Redis 2.6.12 开始,可以通过一系列参数来修改 SET 命令的行为:

  比如我们需要创建一个分布式锁,并将过期时间设置为10s,那么我们可以执行如下命令:

  SET lockKey lockValue EX 10 NX<br style="-webkit-tap-highlight-color: transparent;">或者<br style="-webkit-tap-highlight-color: transparent;">SET lockKey lockValue PX 10000 NX<br style="-webkit-tap-highlight-color: transparent;">

  注意EX和PX不能同时使用,否则会报错:ERR syntax error。

  解锁时,使用 DEL 命令解锁。

  修改后的方案看起来很完美,但实际上还是有问题。想象一下,线程A获取了一个锁,设置过期时间为10s,然后执行业务逻辑需要15s。此时线程A获取的锁已经被Redis的过期机制自动释放了。线程A获取锁后10s后,锁可能已经被其他线程获取。当线程 A 执行完业务逻辑并准备解锁时(DEL 键),就可以删除其他线程已经获得的锁。

  所以最好的办法就是在开锁的时候判断锁是不是你自己的。我们可以在设置key的时候将value设置为唯一值uniqueValue(可以是随机值,UUID,也可以是机器号+线程号,签名等的组合)。解锁时,即删除key时,首先判断key对应的值是否与之前设置的值相等。如果相等,则可以删除密钥。伪代码示例如下:

  if uniqueKey == GET(key) {<br style="-webkit-tap-highlight-color: transparent;"> DEL key<br style="-webkit-tap-highlight-color: transparent;">}<br style="-webkit-tap-highlight-color: transparent;">

  这里我们一眼就能看出问题所在:GET和DEL是两个独立的操作,在GET执行之后和DEL执行之前的间隙中可能会出现异常。如果我们只是确保解锁代码是原子的,我们就可以解决问题。这里我们介绍一种新的方式,即Lua脚本,示例如下:

  if redis.call("get",KEYS[1]) == ARGV[1] then<br style="-webkit-tap-highlight-color: transparent;"> return redis.call("del",KEYS[1])<br style="-webkit-tap-highlight-color: transparent;">else<br style="-webkit-tap-highlight-color: transparent;"> return 0<br style="-webkit-tap-highlight-color: transparent;">end<br style="-webkit-tap-highlight-color: transparent;">

  其中 ARGV[1] 表示设置密钥时指定的唯一值。

  由于 Lua 脚本的原子性,当 Redis 执行脚本时,其他客户端的命令需要等待 Lua 脚本执行后才能执行。

  下面用Jedis来演示获取锁和解锁的实现,如下:

  public boolean lock(String lockKey, String uniqueValue, int seconds){<br style="-webkit-tap-highlight-color: transparent;"> SetParams params = new SetParams();<br style="-webkit-tap-highlight-color: transparent;"> params.nx().ex(seconds);<br style="-webkit-tap-highlight-color: transparent;"> String result = jedis.set(lockKey, uniqueValue, params);<br style="-webkit-tap-highlight-color: transparent;"> if ("OK".equals(result)) {<br style="-webkit-tap-highlight-color: transparent;"> return true;<br style="-webkit-tap-highlight-color: transparent;"> }<br style="-webkit-tap-highlight-color: transparent;"> return false;<br style="-webkit-tap-highlight-color: transparent;">}<br style="-webkit-tap-highlight-color: transparent;">public boolean unlock(String lockKey, String uniqueValue){<br style="-webkit-tap-highlight-color: transparent;"> String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +<br style="-webkit-tap-highlight-color: transparent;"> "then return redis.call('del', KEYS[1]) else return 0 end";<br style="-webkit-tap-highlight-color: transparent;"> Object result = jedis.eval(script, <br style="-webkit-tap-highlight-color: transparent;"> Collections.singletonList(lockKey), <br style="-webkit-tap-highlight-color: transparent;"> Collections.singletonList(uniqueValue));<br style="-webkit-tap-highlight-color: transparent;"> if (result.equals(1)) {<br style="-webkit-tap-highlight-color: transparent;"> return true;<br style="-webkit-tap-highlight-color: transparent;"> }<br style="-webkit-tap-highlight-color: transparent;"> return false;<br style="-webkit-tap-highlight-color: transparent;">}<br style="-webkit-tap-highlight-color: transparent;">

  这是万无一失的吗?明显不是!

  从表面上看,这种方法似乎行得通,但是有一个问题:我们的系统架构存在单点故障,如果 Redis 主节点宕机了怎么办?可能有人会说:加个从节点!只在主人宕机时使用奴隶!

  但实际上,这种方案显然是不可行的,因为 Redis 复制是异步的。例如:

  线程 A 获得主节点上的锁。在将 A 创建的密钥写入从节点之前,主节点已关闭。从节点成为主节点。线程 B 也获得了与 A 相同的锁。(因为在原来的slave中没有A持有锁的信息)

  当然,在某些场景下,这个方案是没有问题的,比如业务模型允许同时持有锁的情况,所以使用这个方案也不是没有道理。

  例如,一个服务有 2 个服务实例:A 和 B。最初,A 获取锁,然后对资源进行操作(可以假设此操作非常占用资源),而 B 不获取锁并执行不执行任何操作。时间B可以看成是A的热备份,当A异常时,B可以“正”。当锁异常时,比如 Redis master 宕机了,那么 B 可能会持有锁,同时对资源进行操作。如果运算的结果是幂等的(或其他情况),也可以使用这种方案。这里引入分布式锁可以让服务避免正常情况下重复计算造成的资源浪费。

  针对这种情况,antriez 提出了 Redlock 算法。Redlock算法的主要思想是:假设我们有N个Redis主节点,这些节点是完全独立的,我们可以使用前面的方案来获取和解锁之前的单个Redis主节点,如果我们一般可以在在合理的范围内或者N/2+1个锁,那么我们可以认为锁已经被成功获取了,否则锁没有被获取(类似于Quorum模型)。Redlock的原理虽然很好理解,但其内部实现细节非常复杂,需要考虑的因素很多。

  Redlock 的算法不是“灵丹妙药”。除了条件苛刻之外,它的算法本身也受到了质疑。关于 Redis 分布式锁的安全性,分布式系统专家 Martin Kleppmann 和 Redis 的作者 antirez 有过争论。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线