用redis实现悲观锁(后端语言以php为例)

 

锁机制

一般使用的锁分为乐观锁,悲观锁这两种,简单介绍下这两种锁,做为本文的背景知识,对这类知识已经有足够了解的同窗能够跳过这部分。php

乐观锁

先来看下百度百科上的解释:大可能是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增长一个版本标识,在基于数据库表的版本解决方案中,通常是经过为数据库表增长一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,以后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,若是提交的数据版本号大于数据库表当前版本号,则予以更新,不然认为是过时数据。html

其实说白了,就是比如一个健身房里只有一台跑步机,在健身房门口有个排号机,每一个进健身房的人都得先领一个号码才能进入,若是跑步机上有人,则在一边作作热身、喝喝水,若是跑步机上没人,则确认跑步机上当前显示的号码(上一个用过跑步机的人的号码)是否比本身手持的小,若是小,则可使用;不然,就意味着过号,而过号在现实中咱们的都知道要么走,要么重排,就是不能插队,在系统中也是同样的,一般是返回错误。mysql

悲观锁

一样,来看下百度百科的解释:具备强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其余事务,以及来自外部系统的事务处理)修改持保守态度,所以,在整个数据处理过程当中,将数据处于锁定状态。悲观锁的实现,每每依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,不然,即便在本系统中实现了加锁机制,也没法保证外部系统不会修改数据)。redis

而后,也一样通俗的解释下,仍是那个健身房。此次在门口不须要排号机了,而是挂着把钥匙(只有一把),想进去的人必须拿到这把钥匙才行,拿到钥匙的人能够进入,不论是热身、喝水仍是跑步均可以,直到他出来把钥匙挂回墙上,下一个才能去争取,拿到的才能够再进去。听着好像有点不人性化,因此悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。sql

背景

先说下,本文的开发背景,方便你们了解为何要使用悲观锁以及文中锁的详细设计。 
任务分发系统:任务池(mysql)中存在大量任务(文章),如今须要用户协助编辑,系统基本需求以下(简化版): 
一、推送用户感兴趣的分类下的任务到用户编辑器中; 
二、用户编辑提交一个任务后,自动推送下一个任务; 
三、每次只分配一个任务给用户; 
四、若是一个用户占有某任务超过必定时间,则自动释听任务,任务进任务池,从新循环; 
五、……数据库

目标

目标有两个: 
一、一个任务在同一时间段内只能被一个用户所持有; 
二、避免出现死任务,即避免任务被用户长时间占有,没法释放。markdown

思路

因为系统并发量较大,而且有频繁的写操做,因此选择悲观锁来控制每一个任务只能同时被一个用户领取。主要思路以下: 
一、从任务池中找出一部分可分配的任务; 
二、根据必定顺序,选择一个任务,做为候选推送任务; 
三、尝试对候选推送任务加锁; 
四、若是加锁成功,则推送任务给用户,并修改对应的任务状态和用户状态; 
五、若是加锁失败,则任务已被领取,重复2-5,直到推送成功。并发

实现

这里只介绍下锁的实现机制,其他业务逻辑略过。因为加锁过程应该是不可拆解的,也就是常说的原子型操做,所以这里选择redis中的setnx操做做为加锁的方法。 
简化版的代码以下:编辑器

function lock($strMutex, $intTimeout) { $objRedis = new Redis(); //使用setnx原子型操做加锁 $intRet = $objRedis->setnx($strMutex, 1); if ($intRet) { //设置过时时间,防止死任务的出现 $objRedis->expire($strMutex, $intTimeout); return true; } return false; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这段代码有个问题,就是setnx成功,但expire失败,这就可能存在死任务的状况。解决这个问题的一种通用方法是经过使用incr方法代替setnx,具体以下:post

function lock($strMutex, $intTimeout, $intMaxTimes = 0) { $objRedis = new Redis(); //使用incr原子型操做加锁 $intRet = $objRedis->incr($strMutex); if ($intRet === 1) { //设置过时时间,防止死任务的出现 $objRedis->expire($strMutex, $intTimeout); return true; } if ($intMaxTimes > 0 && $intRet >= $intMaxTimes && $objRedis->ttl($strMutex) === -1) { //当设置了最大加锁次数时,若是尝试加锁次数大于最大加锁次数而且无过时时间则强制解锁 $objRedis->del($strMutex); } return false; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这段代码经过$intMaxTimes来保证即便在expire未成功的时候也能强制解锁,保证系统不会出现死任务。

还有没有更好的方法呢?

其实redis中的set操做已兼容了setnx,而且支持设置过时时间。

function lock($strMutex, $intTimeout) { $objRedis = new Redis(); //使用setnx操做加锁,同时设置过时时间 $strRet = $objRedis->set($strMutex, 1, 'ex', $intTimeout, 'nx'); if ($strRet === 'OK') { return true; } return false; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这个方法是我认为目前最好的,可是为何没有直接介绍这个方法,而是先介绍incr那个方法呢?其实细心的同窗能够看到上面那个方面有两个加粗的字”通用“。之因此这么说是由于set方法是从redis2.6.12版本才开始支持多参数的。

水平有限,欢迎指正~ 
如需转发,请注明出处,thx~

参考资料: 
http://redisdoc.com/string/set.html

相关文章
相关标签/搜索