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中的锁结合自己的项目加以修改就可以很好地使用这个锁了。例如商城中的疯狂抢购、游戏中虚拟商城玩家买卖东西等等。

转载于:https://www.cnblogs.com/qq350760546/p/6669560.html

一、数据采集层:多源人脸数据获取 该层负责从不同设备 / 渠道采集人脸原始数据,为后续模型训练与识别提供基础样本,核心功能包括: 1. 多设备适配采集 实时摄像头采集: 调用计算机内置摄像头(或外接 USB 摄像头),通过OpenCV的VideoCapture接口实时捕获视频流,支持手动触发 “拍照”(按指定快捷键如Space)或自动定时采集(如每 2 秒采集 1 张),采集时自动框选人脸区域(通过Haar级联分类器初步定位),确保样本聚焦人脸。 支持采集参数配置:可设置采集分辨率(如 640×480、1280×720)、图像格式(JPG/PNG)、单用户采集数量(如默认采集 20 张,确保样本多样性),采集过程中实时显示 “已采集数量 / 目标数量”,避免样本不足。 本地图像 / 视频导入: 支持批量导入本地人脸图像文件(支持 JPG、PNG、BMP 格式),自动过滤非图像文件;导入视频文件(MP4、AVI 格式)时,可按 “固定帧间隔”(如每 10 帧提取 1 张图像)或 “手动选择帧” 提取人脸样本,适用于无实时摄像头场景。 数据集对接: 支持接入公开人脸数据集(如 LFW、ORL),通过预设脚本自动读取数据集目录结构(按 “用户 ID - 样本图像” 分类),快速构建训练样本库,无需手动采集,降低系统开发与测试成本。 2. 采集过程辅助功能 人脸有效性校验:采集时通过OpenCV的Haar级联分类器(或MTCNN轻量级模型)实时检测图像中是否包含人脸,若未检测到人脸(如遮挡、侧脸角度过大),则弹窗提示 “未识别到人脸,请调整姿态”,避免无效样本存入。 样本标签管理:采集时需为每个样本绑定 “用户标签”(如姓名、ID 号),支持手动输入标签或从 Excel 名单批量导入标签(按 “标签 - 采集数量” 对应),采集完成后自动按 “标签 - 序号” 命名文件(如 “张三
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值