java 分布式任务队列_Redis_分布式锁和任务队列

本文详细介绍了如何使用Redis实现分布式锁和任务队列。首先阐述了分布式锁的概念及其重要性,接着讲解了Redis实现分布式锁的原理和代码实现,包括加锁、解锁和异常处理。然后,文章探讨了任务队列的应用场景,如秒杀活动,展示了如何利用Redis的有序集合数据结构实现任务的入队和出队操作,保证操作的一致性。最后,提供了完整的PHP代码示例,帮助读者理解和实现这两个功能。
摘要由CSDN通过智能技术生成

先了解下什么是分布式锁,在百科上是这么定义的:

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

简单的理解就是:分布式锁是一个在很多环境中非常有用的原语,它是不同的系统或是同一个系统的不同主机之间互斥操作共享资源的有效方法。

项目:

项目中,任务队列用到分布式锁的情况比较多,在将业务逻辑中可以异步处理的操作放入队列,在其他线程中处理后出队,此时队列中使用了分布式锁,保证入队和出队的一致性。

Redis实现DLM(Distributed Lock Manager)

使用Redis锁住资源的最简单的方法是创建一对key-value值。利用Redis的超时机制,key被创建为有一定的生存期,因此它最终会被释放。而当客户端想要释放时,直接删除key就行了。

一般来说这工作得很好,但有个问题: 这是系统的一个单点。如果Redis主节点挂了呢?当然,我们可以加个子节点,主节点出问题时可以切换过来。不过很可惜,这种方案不可行,因为Redis的主-从复制是异步的,我们无法用其实现互斥的安全特性。

这明显是该模型的一种竞态条件:

客户端A在主节点获得了一个锁。

主节点挂了,而到从节点的写同步还没完成。

从节点被提升为主节点。

客户端B获得和A相同的锁。注意,锁安全性被破坏了

单实例的正确实现方案

在尝试解决上文描述的单实例方案的缺陷之前,先让我们确保针对这种简单的情况,怎么做才是无误的,因为这种方案对某些程序而言也是可以接受的,而且这也是我们即将描述的分布式方案的基础。

为了获取锁,方法是这样的:

这条指令将设置key的值,仅当其不存在时生效(NX选项), 且设置其生存期为30000毫秒(PX选项)。和key关联的value值是”my_random_value”。这个值在所有客户端和所有加锁请求中是必须是唯一的。

使用随机值主要是为了能够安全地释放锁,这要同时结合这么个处理逻辑:删除key值当且仅当其已存在并且其value值是我们所期待的。看看以下lua代码:

这么做很重要,可以避免误删其他客户端创建的锁。例如某个客户端获得了一个锁,但它的处理时长超过了锁的有效时长,之后它删除了这个锁,而此时这个锁可能又被其他客户端给获得了。仅仅做删除是不够安全的,很可能会把其他客户端的锁给删了。结合上面的代码,每个锁都有个唯一的随机值,因此仅当这个值依旧是客户端所设置的值时,才会去删除它。

那么应该怎样生成这个随机值呢?我们使用的是从/dev/urandom读取的20个字节,但你也可以找个更简单的方法,只要能满足任务就行。例如,可以使用/dev/urandom初始化RC4算法,然后用其产生随机数流。更简单的方法是组合unix时间戳和客户端ID, 这并不安全,但对很多环境而言也够用了。

我们所说的key的时间,是指”锁的有效时长“. 它代表两种情况,一种是指锁的自动释放时长,另一种是指在另一个客户端获取锁之前某个客户端占用这个锁的时长,这被限制在从锁获取后开始的一段时间窗口内。

现在我们已经有好的办法获取和释放锁了。在单实例非分布式系统中,只要保证节点没挂掉,这个方法就是安全的。那么让我们把这个概念扩展到分布式的系统中吧,那里可没有这种保证。

双十一刚过不久,大家都知道在天猫、京东、苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形下会对数据库服务器或者是文件服务器应用服务器造成巨大的压力,严重时说不定就宕机了,另一个问题是,秒杀的东西都是有量的,例如一款手机只有10台的量秒杀,那么,在高并发的情况下,成千上万条数据更新数据库(例如10台的量被人抢一台就会在数据集某些记录下 减1),那次这个时候的先后顺序是很乱的,很容易出现10台的量,抢到的人就不止10个这种严重的问题。那么,以后所说的问题我们该如何去解决呢? 接下来我所分享的技术就可以拿来处理以上的问题: 分布式锁 和 任务队列。

二、实现思路

1.Redis实现分布式锁思路

思路很简单,主要用到的redis函数是setnx(),这个应该是实现分布式锁最主要的函数。首先是将某一任务标识名(这里用Lock:order作为标识名的例子)作为键存到redis里,并为其设个过期时间,如果是还有Lock:order请求过来,先是通过setnx()看看是否能将Lock:order插入到redis里,可以的话就返回true,不可以就返回false。

2.Redis实现任务队列

这里的实现会用到上面的Redis分布式的锁机制,主要是用到了Redis里的有序集合这一数据结构。例如入队时,通过zset的add()函数进行入队,而出对时,可以用到zset的getScore()函数。另外还可以弹出顶部的几个任务。

以上就是实现 分布式锁 和 任务队列 的简单思路,如果你看完有点模棱两可,那请看接下来的代码实现。

三、代码分析

(一)先来分析Redis分布式锁的代码实现

(1)为避免特殊原因导致锁无法释放,在加锁成功后,锁会被赋予一个生存时间(通过lock方法的参数设置或者使用默认值),超出生存时间锁会被自动释放锁的生存时间默认比较短(秒级),因此,若需要长时间加锁,可以通过expire方法延长锁的生存时间为适当时间,比如在循环内。

(2)系统级的锁当进程无论何种原因时出现crash时,操作系统会自己回收锁,所以不会出现资源丢失,但分布式锁不用,若一次性设置很长时间,一旦由于各种原因出现进程crash 或者其他异常导致unlock未被调用时,则该锁在剩下的时间就会变成垃圾锁,导致其他进程或者进程重启后无法进入加锁区域。

先看加锁的实现代码:这里需要主要两个参数,一个是$timeout,这个是循环获取锁的等待时间,在这个时间内会一直尝试获取锁知道超时,如果为0,则表示获取锁失败后直接返回而不再等待;另一个重要参数的$expire,这个参数指当前锁的最大生存时间,以秒为单位的,它必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放。这个参数的最要作用请看上面的(1)里的解释。

这里先取得当前时间,然后再获取到锁失败时的等待超时的时刻(是个时间戳),再获取到锁的最大生存时刻是多少。这里redis的key用这种格式:”Lock:锁的标识名”,这里就开始进入循环了,先是插入数据到redis里,使用setnx()函数,这函数的意思是,如果该键不存在则插入数据,将最大生存时刻作为值存储,假如插入成功,则对该键进行失效时间的设置,并将该键放在$lockedName数组里,返回true,也就是上锁成功;如果该键存在,则不会插入操作了,这里有一步严谨的操作,那就是取得当前键的剩余时间,假如这个时间小于0,表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用,这时可以直接设置expire并把锁纳为己用。如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出循环,反之则 隔 $waitIntervalUs 后继续 请求。  这就是加锁的整一个代码分析。

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

/**

* 加锁

* @param [type] $name 锁的标识名

* @param integer $timeout 循环获取锁的等待超时时间,在此时间内会一直尝试获取锁直到超时,为0表示失败后直接返回不等待

* @param integer $expire 当前锁的最大生存时间(秒),必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放

* @param integer $waitIntervalUs 获取锁失败后挂起再试的时间间隔(微秒)

* @return [type] [description]*/

public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) {if ($name == null) return false;//取得当前时间

$now = time();//获取锁失败时的等待超时时刻

$timeoutAt = $now + $timeout;//锁的最大生存时刻

$expireAt = $now + $expire;$redisKey = "Lock:{$name}";while (true) {//将rediskey的最大生存时刻存到redis里,过了这个时刻该锁会被自动释放

$result = $this->redisString->setnx($redisKey, $expireAt);if ($result != false) {//设置key的失效时间

$this->redisString->expire($redisKey, $expireAt);//将锁标志放到lockedNames数组里

$this->lockedNames[$name] = $expireAt;return true;

}//以秒为单位,返回给定key的剩余生存时间

$ttl = $this->redisString->ttl($redisKey);//ttl小于0 表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)

//如果出现这种状况,那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用

//这时可以直接设置expire并把锁纳为己用

if ($ttl < 0) {$this->redisString->set($redisKey, $expireAt);$this->lockedNames[$name] = $expireAt;return true;

}/*****循环请求锁部分*****/

//如果没设置锁失败的等待时间 或者 已超过最大等待时间了,那就退出

if ($timeout <= 0 || $timeoutAt < microtime(true)) break;//隔 $waitIntervalUs 后继续 请求

usleep($waitIntervalUs);

}return false;

}

View Code

接着看解锁的代码分析:解锁就简单多了,传入参数就是锁标识,先是判断是否存在该锁,存在的话,就从redis里面通过deleteKey()函数删除掉锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值