Redis构建分布式锁

阅读目录

回到顶部

1、前言

  为什么要构建锁呢?因为构建合适的锁可以在高并发下能够保持数据的一致性,即客户端在执行连贯的命令时上锁的数据不会被别的客户端的更改而发生错误。同时还能够保证命令执行的成功率。

  看到这里你不禁要问redis中不是有事务操作么?事务操作不能够实现上面的功能么?

  的确,redis中的事务可以watch可以监控数据,从而能够保证连贯执行的时数据的一致性,但是我们必须清楚的认识到,在多个客户端同时处理相同的数据的时候,很容易导致事务的执行失败,甚至会导致数据的出错。

  在关系型数据库中,用户首先向数据库服务器发送BEGIN,然后执行各个相互一致的写操作和读操作,最后用户可以选择发送COMMIT来确认之前的修改,或者发送ROLLBACK进行回滚。

  在redis中,通过特殊的命令MULTI为开始,之后用户传入一连贯的命令,最后EXEC为结束(在这一过程中可以使用watch进行监控一些key)。进一步分析,redis事务中的命令会先推入队列,等到EXEC命令出现的时候才会将一条条命令执行。假若watch监控的key发生改变,这个事务将会失败。这也就说明Redis事务中不存在锁,其他客户端可以修改正在执行事务中的有关数据,这也就为什么在多个客户端同时处理相同的数据时事务往往会发生错误。

回到顶部

2、简单理解redis的单线程IO多路复用

  Redis采用单线程IO多路复用模型来实现高内存数据服务。何为单线程IO多路复用呢?从字面的意思可以知道redis采用的是单线程、使用的是多个IO。整个过程简单的来讲就是,哪个命令的数据流先到达就先执行。

请看下面的形象理解图:图中是一座窄桥,只能允许一辆车通过,左边是车辆进入的通道,哪一辆车先到达就先进入。即哪个IO流先到达就先处理哪个。

  Linux下网络IO使用socket套接字来通讯,普通IO模型只能监听一个socket,而IO多路复用可同时监控多个socket。IO多路复用避免阻塞在IO上,单线程保存多个socket的状态后轮循处理。

回到顶部

3、并发测试

  我们就模拟一个简单典型的并发测试,然后从这个测试中得出问题,再进一步研究。

  并发测试思路:

  1、在redis中设置一个字符串count,运用程序将其取出来加+1,再存储回去,一直循环十万次

  2、在两个浏览器上同时执行这个代码

  3、将count取出来,查看结果

测试步骤:

1、建立test.php文件

 1 <?php
 2 $redis=new Redis();
 3 $redis->connect(‘192.168.95.11‘,‘6379‘);
 4 for ($i=0; $i < 100000; $i++)
 5 {
 6   $count=$redis->get(‘count‘);
 7   $count=$count+1;
 8   $redis->set(‘count‘,$count);
 9 }
10 echo "this OK";
11 ?>

2、分别在两个浏览器中访问test.php文件

  结果由上图可知,总共执行两次,count原本应该是二十万才对的,但实际上count等于十三万多,远远小于二十万,这是为什么呢?

  由前面的内容可知,redis是采用单线程IO多路复用模型的。因此我们使用两个浏览器即为两个会话(A、B),取出、加1、存入这三个命令并不是原子操作,并且在执行取出、存入这两个redis命令时是哪个客户端先到就先执行。

  例如:1、此时count=120

     2、A取出count=120,紧接着B的取出命令流到了,也将count=120取出

     3、A取出后立即加1,并将count=121存回去

     4、此时B也紧跟着,也将count=121存进去了

注意:

1、设置循环次数尽量大一点,太小的话,当在第一个浏览器执行完毕,第二个浏览器还没开始进行呢

2、必须要两个浏览器同时执行。假若在一个浏览器中同时执行两次test.php文件,不管是否同时执行,最终结果就是count=200000。因为在同一个浏览器中执行,都是属于同一个会话(所有命令都在同一个通道通过),所以redis会让先执行的十万次执行完,再接着执行其他的十万次。

回到顶部

4、事务解决与原子性操作解决

回到顶部

  4.1、事务解决

      更改后的test.php文件

 1 <?php
 2 header("content-type: text/html;charset=utf8;");
 3 $start=time();
 4 $redis=new Redis();
 5 $redis->connect(‘192.168.95.11‘,‘6379‘);
 6
 7 for ($i=0; $i < 100000; $i++)
 8 {
 9   $redis->multi();
10   $count=$redis->get(‘count‘);
11   $count=$count+1;
12   $redis->set(‘count‘,$count);
13   $redis->exec();
14 }
15 $end=time();
16 echo "this OK<br/>";
17 echo "执行时间为:".($end-$start);
18 ?>

执行结果失败,表名使用事务不能够解决此问题。

分析原因:

  我们都知道当redis开启时,事务中的命令是不执行的,而是先将命令压入队列,然后当出现exec命令的时候,才会阻塞式的将所有的命令一个接一个的执行。

  所以当使用PHP中的Redis类进行redis事务的时候,所有有关redis的命令都不会真正的执行,而仅仅是将命令发送到redis中进行存储起来。

  因此下图中所圈到的$count实际上不是我们想要的数据,而是一个对象,因此test.php中11行出错。

查看对象count:

  

  

回到顶部

  4.2、原子性操作incr解决

      #更新test.php文件

 1 <?php
 2 header("content-type: text/html;charset=utf8;");
 3 $start=time();
 4 $redis=new Redis();
 5 $redis->connect(‘192.168.95.11‘,‘6379‘);
 6 for ($i=0; $i < 100000; $i++)
 7 {
 8   $count=$redis->incr(‘count‘);
 9 }
10 $end=time();
11 echo "this OK<br/>";
12 echo "执行时间为:".($end-$start);
13 ?>

  两个浏览器同时执行,耗时14、15秒,count=200000,可以解决此问题。

缺点:

  仅仅只是解决这里的取出加1的问题,本质上还是没能解决问题的,在实际环境中,我们需要做的是一系列操作,不仅仅只是取出加1,因此就很有必要构建一个万能锁了。

回到顶部

5、构建分布式锁  

  我们构造锁的目的就是在高并发下消除选择竞争、保持数据一致性

  构造锁的时候,我们需要注意几个问题:

    1、预防处理持有锁在执行操作的时候进程奔溃,导致死锁,其他进程一直得不到此锁

    2、持有锁进程因为操作时间长而导致锁自动释放,但本身进程并不知道,最后错误的释放其他进程的锁

    3、一个进程锁过期后,其他多个进程同时尝试获取锁,并且都成功获得锁

  我们将不对test.php文件修改了,而是直接建立一个相对比较规范的面向对象Lock.class.php类文件  

  #建立Lock.class,php文件

  1 <?php
  2 #分布式锁
  3 class Lock
  4 {
  5     private $redis=‘‘;  #存储redis对象
  6     /**
  7     * @desc 构造函数
  8     *
  9     * @param $host string | redis主机
 10     * @param $port int    | 端口
 11     */
 12     public function __construct($host,$port=6379)
 13     {
 14         $this->redis=new Redis();
 15         $this->redis->connect($host,$port);
 16     }
 17
 18     /**
 19     * @desc 加锁方法
 20     *
 21     * @param $lockName string | 锁的名字
 22     * @param $timeout int | 锁的过期时间
 23     *
 24     * @return 成功返回identifier/失败返回false
 25     */
 26     public function getLock($lockName, $timeout=2)
 27     {
 28         $identifier=uniqid();       #获取唯一标识符
 29         $timeout=ceil($timeout);    #确保是整数
 30         $end=time()+$timeout;
 31         while(time()<$end)          #循环获取锁
 32         {
 33             if($this->redis->setnx($lockName, $identifier))    #查看$lockName是否被上锁
 34             {
 35                 $this->redis->expire($lockName, $timeout);     #为$lockName设置过期时间,防止死锁
 36                 return $identifier;                             #返回一维标识符
 37             }
 38             elseif ($this->redis->ttl($lockName)===-1)
 39             {                               
 40                 $this->redis->expire($lockName, $timeout);     #检测是否有设置过期时间,没有则加上(假设,客户端A上一步没能设置时间就进程奔溃了,客户端B就可检测出来,并设置时间)
 41             }
 42             usleep(0.001);         #停止0.001ms
 43         }
 44         return false;
 45     }
 46
 47     /**
 48     * @desc 释放锁
 49     *
 50     * @param $lockName string   | 锁名
 51     * @param $identifier string | 锁的唯一值
 52     *
 53     * @param bool
 54     */
 55     public function releaseLock($lockName,$identifier)
 56     {
 57         if($this->redis->get($lockName)==$identifier)   #判断是锁有没有被其他客户端修改
 58         {
 59             $this->redis->multi();
 60             $this->redis->del($lockName);   #释放锁
 61             $this->redis->exec();
 62             return true;
 63         }
 64         else
 65         {
 66             return false;   #其他客户端修改了锁,不能删除别人的锁
 67         }
 68     }
 69
 70     /**
 71     * @desc 测试
 72     *
 73     * @param $lockName string | 锁名
 74     */
 75     public function test($lockName)
 76     {
 77         $start=time();
 78         for ($i=0; $i < 10000; $i++)
 79         {
 80             $identifier=$this->getLock($lockName);
 81             if($identifier)
 82             {
 83               $count=$this->redis->get(‘count‘);
 84               $count=$count+1;
 85               $this->redis->set(‘count‘,$count);
 86               $this->releaseLock($lockName,$identifier);
 87             }
 88         }
 89         $end=time();
 90         echo "this OK<br/>";
 91         echo "执行时间为:".($end-$start);
 92     }
 93
 94 }
 95
 96 header("content-type: text/html;charset=utf8;");
 97 $obj=new Lock(‘192.168.95.11‘);
 98 $obj->test(‘lock_count‘);
 99
100 ?>

测试结果:

  在两个不同的浏览器中执行,最终结果count=200000,但是耗时相对较多,需要近八十多秒左右。但是在高并发下,对同一个数据,二十万次上锁执行释放锁的操作还是可以接受的,甚至已经很不错了。

以上的简单例子仅仅只是为了模拟并发测试并检验而已,实际上我们可以使用Lock.class.php中的锁结合自己的项目加以修改就可以很好地使用这个锁了。例如商城中的疯狂抢购、游戏中虚拟商城玩家买卖东西等等。

时间: 2024-08-29 14:52:21

Redis构建分布式锁的相关文章

用redis构建分布式锁

原文:用redis构建分布式锁 用redis构建分布式锁 单实例的实现 从2.6.12版本开始,redis为SET命令增加了一系列选项: EX seconds – 设置键key的过期时间,单位时秒 PX milliseconds – 设置键key的过期时间,单位时毫秒 NX – 只有键key不存在的时候才会设置key的值 XX – 只有键key存在的时候才会设置key的值 如果有2个进程(可能位于不同机器)需要竞争某个资源,可以为这个资源加锁,锁放在redis里面,这样两个进程都能访问到,例如下

python中,用Redis构建分布式锁

分布式锁 在实际应用场景中,我们可能有多个worker,可能在一台机器,也可能分布在不同的机器,但只有一个worker可以同时持有一把锁,这个时候我们就需要用到分布式锁了. 这里推荐python的实现库,Redlock-py (Python 实现). 正常情况下,worker获得锁后,处理自己的任务,完成后自动释放持有的锁,是不是感觉有点熟悉,很容易想到我们的上下文管理器,这里我们简单的用装饰器实现 with...as... 语法. 安装Redlock-py $ pip install redl

基于Redis的分布式锁到底安全吗(上)?

网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词"Redis 分布式锁"随便到哪个搜索引擎上去搜索一下就知道了.这些文章的思路大体相近,给出的实现算法也看似合乎逻辑,但当我们着手去实现它们的时候,却发现如果你越是仔细推敲,疑虑也就越来越多. 实际上,大概在一年以前,关于Redis分布式锁的安全性问题,在分布式系统专家Martin Kleppmann和Redis的作者antirez之间就发生过一场争论.由于对这个问题一直以来比较关注,所以我前些日子仔细阅读了与这场争

基于redis的分布式锁(不适合用于生产环境)

基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境,另一个是集群环境下的Redis锁实现.在介绍分布式锁的实现之前,先来了解下分布式锁的一些信息. 2 分布式锁 2.1 什么是分布式锁? 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥

基于Redis的分布式锁和Redlock算法

1 前言 前面写了4篇Redis底层实现和工程架构相关文章,感兴趣的读者可以回顾一下: Redis面试热点之底层实现篇-1 Redis面试热点之底层实现篇-2 Redis面试热点之工程架构篇-1 Redis面试热点之工程架构篇-2 今天开始来和大家一起学习一下Redis实际应用篇,会写几个Redis的常见应用. 在我看来Redis最为典型的应用就是作为分布式缓存系统,其他的一些应用本质上并不是杀手锏功能,是基于Redis支持的数据类型和分布式架构来实现的,属于小而美的应用. 结合笔者的日常工作,

基于Redis实现分布式锁(转载)

原文地址:http://blog.csdn.net/ugg/article/details/41894947 Redis命令介绍使用Redis实现分布式锁,有两个重要函数需要介绍 SETNX命令(SET if Not eXists)语法:SETNX key value功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1:若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0. GETSET命令语法:GETSET key value功能:将给定 key 的值设为

利用多写Redis实现分布式锁原理与实现分析

在我写这篇文章的时候,其实我还是挺纠结的,因为我这个方案本身也是雕虫小技拿出来显眼肯定会被贻笑大方,但是我最终还是拿出来与大家分享,我本着学习的态度和精神,希望大家能够给与我指导和改进方案. 一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子: 场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能在某一个时刻会有二笔一样的单子同时到达系统后台. 场景二:在App中下订单的时候,点击确认之后,没反应,就又点击了几次.在这种情况下,如果无法保证该接口的幂

Redis实现分布式锁

http://redis.io/topics/distlock 在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章描述如何用Redis实现一个分布式锁管理器,但是这些库实现的方式差别很大,而且很多简单的实现其实只需采用稍微增加一点复杂的设计就可以获得更好的可靠性. 这篇文章的目的就是尝试提出一种官方权威的用Redis实现分布式锁管理器的算法,我们把这个算法称为RedLock,我们相信这个算法会比一般的普通方法更加安全可靠.我们也希望社区能一起分析这个算法,

基于redis的分布式锁

<?php /** * 基于redis的分布式锁 * * 参考开源代码: * http://nleach.com/post/31299575840/redis-mutex-in-php * * https://gist.github.com/nickyleach/3694555 */ pc_base::load_sys_class('cache_redis', '', 0); class dist_key_redis { //锁的超时时间 const TIMEOUT = 20; const SL