PHP+redis实现的悲观锁机制示例


Posted in PHP onJune 12, 2018

本文实例讲述了PHP+redis实现的悲观锁。分享给大家供大家参考,具体如下:

锁机制

通常使用的锁分为乐观锁,悲观锁这两种,简单介绍下这两种锁,作为本文的背景知识,对这类知识已经有足够了解的同学可以跳过这部分。

乐观锁

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

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

悲观锁

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

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

背景

先说下,本文的开发背景,方便大家了解为什么要使用悲观锁以及文中锁的详细设计。

任务分发系统:任务池(mysql)中存在大量任务(文章),现在需要用户协助编辑,系统基本需求如下(简化版):

1、推送用户感兴趣的分类下的任务到用户编辑器中;
2、用户编辑提交一个任务后,自动推送下一个任务;
3、每次只分配一个任务给用户;
4、如果一个用户占有某任务超过一定时间,则自动释放任务,任务进任务池,重新循环;
5、……

目标

目标有两个:

1、一个任务在同一时间段内只能被一个用户所持有;

2、避免出现死任务,即避免任务被用户长时间占有,无法释放。

思路

由于系统并发量较大,并且有频繁的写操作,所以选择悲观锁来控制每个任务只能同时被一个用户领取。主要思路如下:

1、从任务池中找出一部分可分配的任务;
2、根据一定顺序,选择一个任务,作为候选推送任务;
3、尝试对候选推送任务加锁;
4、如果加锁成功,则推送任务给用户,并修改对应的任务状态和用户状态;
5、如果加锁失败,则任务已被领取,重复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;
}

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

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;
}

这段代码通过$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;
}

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

水平有限,欢迎指正~

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

希望本文所述对大家PHP程序设计有所帮助。

PHP 相关文章推荐
smarty实例教程
Nov 19 PHP
简单的PHP多图上传小程序代码
Jul 17 PHP
php采用curl模仿登录人人网发布动态的方法
Nov 07 PHP
浅谈json_encode用法
Mar 05 PHP
PHP 7的一些引人注目的新特性简单介绍
Nov 08 PHP
twig里使用js变量的方法
Feb 05 PHP
PHP Callable强制指定回调类型的方法
Aug 30 PHP
php array_values 返回数组的所有值详解及实例
Nov 12 PHP
Laravel路由研究之domain解决多域名问题的方法示例
Apr 04 PHP
ThinkPHP框架整合微信支付之Native 扫码支付模式一图文详解
Apr 09 PHP
php函数式编程简单示例
Aug 08 PHP
PHP isset empty函数相关面试题及解析
Dec 11 PHP
thinkPHP5框架auth权限控制类与用法示例
Jun 12 #PHP
thinkPHP5框架实现基于ajax的分页功能示例
Jun 12 #PHP
Laravel框架路由和控制器的绑定操作方法
Jun 12 #PHP
Laravel框架路由设置与使用示例
Jun 12 #PHP
Laravel框架生命周期与原理分析
Jun 12 #PHP
Laravel框架分页实现方法分析
Jun 12 #PHP
php 可变函数使用小结
Jun 12 #PHP
You might like
ThinkPHP之用户注册登录留言完整实例
2014/07/22 PHP
PHP+fiddler抓包采集微信文章阅读数点赞数的思路详解
2019/12/20 PHP
JAVASCRIPT  THIS详解 面向对象
2009/03/25 Javascript
仅IE支持clearAttributes/mergeAttributes方法使用介绍
2012/05/04 Javascript
JavaScript 参数中的数组展开 [译]
2012/09/21 Javascript
JavaScript实现穷举排列(permutation)算法谜题解答
2014/12/29 Javascript
jQuery EasyUI之DataGrid使用实例详解
2016/01/04 Javascript
JavaScript+html5 canvas实现本地截图教程
2020/04/16 Javascript
jQuery基于muipicker实现仿ios时间选择
2016/02/22 Javascript
JS生成不重复的随机数组的简单实例
2016/07/10 Javascript
vue脚手架vue-cli的学习使用教程
2017/06/06 Javascript
老生常谈javascript的面向对象思想
2017/08/22 Javascript
JS实现按钮颜色切换效果
2020/09/05 Javascript
Vue组件和Route的生命周期实例详解
2018/02/10 Javascript
详解PHP后期静态绑定分析与应用
2018/03/21 Javascript
JavaScript函数式编程(Functional Programming)声明式与命令式实例分析
2019/05/21 Javascript
javaScript中indexOf用法技巧
2019/11/26 Javascript
JavaScript如何实现监听键盘输入和鼠标监点击
2020/07/20 Javascript
vue插件--仿微信小程序showModel实现模态提示窗功能
2020/08/19 Javascript
Vue 防止短时间内连续点击后多次触发请求的操作
2020/11/11 Javascript
[01:13:18]Secret vs Infamous 2019国际邀请赛淘汰赛 败者组 BO3 第一场 8.23
2019/09/05 DOTA
python socket 超时设置 errno 10054
2014/07/01 Python
零基础写python爬虫之爬虫的定义及URL构成
2014/11/04 Python
python使用Flask框架获取用户IP地址的方法
2015/03/21 Python
Python实现将一个正整数分解质因数的方法分析
2017/12/14 Python
Python爬虫_城市公交、地铁站点和线路数据采集实例
2018/01/10 Python
python2.x实现人民币转大写人民币
2018/06/20 Python
ffmpeg+Python实现B站MP4格式音频与视频的合并示例代码
2020/10/21 Python
用css3实现当鼠标移进去时当前亮其他变灰效果
2014/04/08 HTML / CSS
简单说下OSPF的操作过程
2014/08/13 面试题
幼儿教师自我鉴定
2013/11/02 职场文书
总经理助理工作职责
2014/02/06 职场文书
学生操行评语大全
2014/04/24 职场文书
第二批党的群众路线教育实践活动个人整改方案
2014/10/31 职场文书
2014年幼儿园教师工作总结
2014/11/08 职场文书
乌镇导游词
2015/02/02 职场文书