这一次总结和分享用Redis实现分布式锁来完成电商的秒杀功能。先扯点个人观点,之前我看了一篇博文说博客园的文章大部分都是分享代码,博文里强调说分享思路比分享代码更重要(貌似大概是这个意思,若有误请谅解),但我觉得,分享思路固然重要,但有了思路,却没有实现的代码,那会让人觉得很浮夸的,在工作中的程序猿都知道,你去实现一个功能模块,一段代码,虽然你有了思路,但是实现的过程也是很耗时的,特别是代码调试,还有各种测试等等。所以我认为,思路+代码,才是一篇好博文的主要核心。

  直接进入主题。

  一、前言

  双十一刚过不久,大家都知道在天猫、京东、苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价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 后继续 请求。 这就是加锁的整一个代码分析。

/**
* 加锁
* @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;
}

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

/**
* 解锁
* @param  [type] $name [description]
* @return [type]       [description]
*/
public function unlock($name) {
    //先判断是否存在此锁
    if ($this->isLocking($name)) {
        //删除锁
        if ($this->redisString->deleteKey("Lock:$name")) {
            //清掉lockedNames里的锁标志
            unset($this->lockedNames[$name]);
            return true;
        }
    }
    return false;
}

   

再贴上删除掉所有锁的方法,其实都一个样,多了个循环遍历而已。

/**
* 释放当前所有获得的锁
* @return [type] [description]
*/
public function unlockAll() {
    //此标志是用来标志是否释放所有锁成功
    $allSuccess = true;
    foreach ($this->lockedNames as $name => $expireAt) {
    if (false === $this->unlock($name)) {
    $allSuccess = false;
    }
    }
    return $allSuccess;
}

  

以上就是用Redis实现分布式锁的整一套思路和代码实现的总结和分享,这里我附上正一个实现类的代码,代码里我基本上对每一行进行了注释,方便大家快速看懂并且能模拟应用。想要深入了解的请看整个类的代码:

/**
*在redis上实现分布式锁
*/
class RedisLock {
    private $redisString;
    private $lockedNames = [];
    public function __construct($param = NULL) {
        $this->redisString = RedisFactory::get($param)->string;
    }
    
    /**
    * 加锁
    * @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;
    }
    /**
    * 解锁
    * @param  [type] $name [description]
    * @return [type]       [description]
    */
    public function unlock($name) {
        //先判断是否存在此锁
        if ($this->isLocking($name)) {
            //删除锁
            if ($this->redisString->deleteKey("Lock:$name")) {
                //清掉lockedNames里的锁标志
                unset($this->lockedNames[$name]);
                return true;
            }
        }
        return false;
    }
    /**
    * 释放当前所有获得的锁
    * @return [type] [description]
    */
    public function unlockAll() {
        //此标志是用来标志是否释放所有锁成功
        $allSuccess = true;
        foreach ($this->lockedNames as $name => $expireAt) {
            if (false === $this->unlock($name)) {
                $allSuccess = false;
            }
        }
        return $allSuccess;
    }
    /**
    * 给当前所增加指定生存时间,必须大于0
    * @param  [type] $name [description]
    * @return [type]       [description]
    */
    public function expire($name, $expire) {
        //先判断是否存在该锁
        if ($this->isLocking($name)) {
            //所指定的生存时间必须大于0
            $expire = max($expire, 1);
            //增加锁生存时间
            if ($this->redisString->expire("Lock:$name", $expire)) {
                return true;
            }
        }
        return false;
    }
    /**
    * 判断当前是否拥有指定名字的所
    * @param  [type]  $name [description]
    * @return boolean       [description]
    */
    public function isLocking($name) {
        //先看lonkedName[$name]是否存在该锁标志名
        if (isset($this->lockedNames[$name])) {
            //从redis返回该锁的生存时间
            return (string)$this->lockedNames[$name] = (string)$this->redisString->get("Lock:$name");
        }
        
        return false;
    }
}

php+redis实现电商秒杀功能的更多相关文章

  1. TP5使用Redis处理电商秒杀

    本篇文章介绍了ThinkPHP使用Redis实现电商秒杀的处理方法,具有一定的参考价值,希望对学习ThinkPHP的朋友有帮助! TP5使用Redis处理电商秒杀 1.首先在TP5中创建抢购活动所需要 ...

  2. 使用Redis搭建电商秒杀系统

    背景 秒杀活动是绝大部分电商选择的低价促销.推广品牌的方式.不仅可以给平台带来用户量,还可以提高平台知名度.一个好的秒杀系统,可以提高平台系统的稳定性和公平性,获得更好的用户体验,提升平台的口碑,从而 ...

  3. Web系统大规模并发——电商秒杀与抢购

    电商的秒杀和抢购,对我们来说,都不是一个陌生的东西.然而,从技术的角度来说,这对于Web系统是一个巨大的考验.当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定至关重要.这次我们 ...

  4. Web系统大规模并发——电商秒杀与抢购 【转】

    电商的秒杀和抢购,对我们来说,都不是一个陌生的东西.然而,从技术的角度来说,这对于Web系统是一个巨大的考验.当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定至关重要.这次我们 ...

  5. 徐汉彬:Web系统大规模并发——电商秒杀与抢购(转)

    [导读]徐汉彬曾在阿里巴巴和腾讯从事4年多的技术研发工作,负责过日请求量过亿的Web系统升级与重构,目前在小满科技创业,从事SaaS服务技术建设. 电商的秒杀和抢购,对我们来说,都不是一个陌生的东西. ...

  6. 【问底】徐汉彬:Web系统大规模并发——电商秒杀与抢购

    [导读]徐汉彬曾在阿里巴巴和腾讯从事4年多的技术研发工作,负责过日请求量过亿的Web系统升级与重构,目前在小满科技创业,从事SaaS服务技术建设. 电商的秒杀和抢购,对我们来说,都不是一个陌生的东西. ...

  7. 徐汉彬:Web系统大规模并发——电商秒杀与抢购

    [导读]徐汉彬曾在阿里巴巴和腾讯从事4年多的技术研发工作,负责过日请求量过亿的Web系统升级与重构,目前在小满科技创业,从事SaaS服务技术建设. 电商的秒杀和抢购,对我们来说,都不是一个陌生的东西. ...

  8. 从电商秒杀与抢购谈Web系统大规模并发

    从电商秒杀与抢购谈Web系统大规模并发 http://www.iamlintao.com/4242.html 一.大规模并发带来的挑战 在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程 ...

  9. Web系统大规模并发:电商秒杀与抢购-----面试必问

    一.大规模并发带来的挑战 在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战.如果Web系统不做针对性的优化,会轻而易举地陷入到异常状态.我们现在 ...

随机推荐

  1. 2016年团体程序设计天梯赛-决赛 L2-3. 互评成绩(25)

    学生互评作业的简单规则是这样定的:每个人的作业会被k个同学评审,得到k个成绩.系统需要去掉一个最高分和一个最低分,将剩下的分数取平均,就得到这个学生的最后成绩.本题就要求你编写这个互评系统的算分模块. ...

  2. tomcat bio nio apr 模式性能测试

    转自:tomcat bio nio apr 模式性能测试与个人看法 11.11活动当天,服务器负载过大,导致部分页面出现了不可访问的状态.那后来主管就要求调优了,下面是tomcat bio.nio.a ...

  3. KindeEditor图片上传插件用法

    因业务需要找了款插件 KindeEditor编辑器确认挺好用,但无奈技术有限,上传配置不知,故问度娘! 图片上传对于部分新手来说有时候是一件非常头疼的事,今天来分享一下项目中使用到的这个插件Kinde ...

  4. C# 语言规范_版本5.0 (第1章 介绍)

    1. 介绍 C#(读作“See Sharp”)是一种简洁.现代.面向对象且类型安全的编程语言.C# 起源于 C 语言家族,因此,对于 C.C++ 和 Java 程序员,可以很快熟悉这种新的语言.C# ...

  5. Openjudge-计算概论(A)-人民币支付

    描述: 从键盘输入一指定金额(以元为单位,如345),然后输出支付该金额的各种面额的人民币数量,显示100元,50元,20元,10元,5元,1元各多少张,要求尽量使用大面额的钞票. 输入一个小于100 ...

  6. IE/Chrome背景图片居中1px偏移解决方法

    最近在支持行业运营的一个推广页面,遇到了非常规的页面banner图居中的问题,为了解决此问题,做了简单的测试,做了一个小结,为经常做大促页面的兄弟姐妹们提供参考解决方案. 首先来看看现象.最经典的页面 ...

  7. .net dynamic动态加属性

    class Test : System.Dynamic.DynamicObject { public override bool TryGetMember(System.Dynamic.GetMemb ...

  8. virtualbox+centos 7 实现宿主机器互通

    1.centos7iso文件 2.安装教程地址,www.aiplaypc.com/102.html 3.修改ip地址 使用命令 vi /etc/sysconfig/network-scripts/if ...

  9. POJ 2062 HDU 1528 ZOJ 2223 Card Game Cheater

    水题,感觉和田忌赛马差不多 #include<cstdio> #include<cstring> #include<cmath> #include<algor ...

  10. 如何更改mysql可执行路径及更改mysql数据库文件路径

    一.如何更改服务中MySQL的可执行文件路径: 解决方法:到注册表里HKEY_LOCAL_MECHINE---SYSTEM ---CurrentControlSet 更改查找一. MySQL项值,然后 ...