Redis构建分布式锁
1.前言
为什么要建锁?因为建立适当的锁可以在高并发下保持数据的一致性,也就是说,当其他客户端执行一致的命令时,锁定的数据不会被更改。同时可以保证命令执行的成功率。
看到这里,你不禁要问:redis不是有交易操作吗?交易操作不能实现以上功能吗?
事实上,redis中的事务可以监视和监控数据,从而确保连贯执行时数据的一致性。但是,我们必须清醒地认识到,当多个客户端同时处理同一个数据时,很容易造成事务执行失败甚至数据错误。
在关系数据库中,用户首先向数据库服务器发送BEGIN,然后执行所有一致的写和读操作。最后,用户可以选择发送COMMIT来确认之前的修改,或者发送ROLLBACK来回滚。
在redis中,使用一个特殊的命令MULTI作为开始,然后用户发送一个连贯的命令,最后EXEC作为结束(在这个过程中,可以使用watch来监控一些键)。进一步的分析表明,redis事务中的命令将首先被推入队列,直到EXEC命令出现才会被执行。如果手表监控的密钥发生变化,此交易将失败。这意味着Redis事务没有锁,其他客户端可以修改正在执行的事务中的相关数据,这就是为什么当多个客户端同时处理相同数据时,事务经常出错的原因。
2.简单理解redis的单线程IO复用
Redis采用单线程IO复用模式,实现高内存数据服务。什么是单线程IO复用?从字面上看,我们可以知道redis使用单个线程,使用多个IO。简而言之,整个过程就是哪个命令的数据流先到达,哪个先执行。
请看下图理解图:图为窄桥,只能让一辆车通过,左侧为车辆进入通道,哪辆车先进入。也就是说,当哪个IO流首先到达时,将首先被处理。
在Linux下,网络IO使用socket进行通信。普通IO模式只能监控一个插座,而IO复用可以同时监控多个插座。IO复用避免了IO上的阻塞,单个线程保存多个套接字的状态,然后进行处理。
3.并行测试
我们将模拟一个简单而典型的并发测试,然后从这个测试中找出问题,然后进一步研究。
并行测试思想:
1.在redis中设置一个字符串计数,用程序取出加1,然后存储回去,循环10万次。
2.在两个浏览器上同时执行这段代码
3.取出计数并检查结果
测试步骤:
1、test.php档案的建立
?PHP $ Redis=new Redis();$redis-connect('192.168.95.11 ',' 6379 ');for($ I=0;10万美元;$ I){ $ count=$ redis-get(' count ');$ count=$ count 1;$redis-set('count ',$ count);}回显“这个OK”;2.分别在两种浏览器中访问test.php文件
从上图可以看出,一共执行了两次,计数应该是20万,但实际上计数是13万多,远低于20万。为什么呢?
从上面可以看出,redis采用的是单线程IO复用模型。因此,我们使用两种浏览器,即两个会话(A和B)。取数、加1、存储三个命令不是原子操作,哪个客户端先执行两个redis命令先来。
例如:
1.此时计数=120
2.a取出计数=120,然后B的取出命令流,也取出计数=120
3.取出A后立即加1,保存计数=121
4.这时,B跟着来了,计数=121也被保存了
注意:
1.设置尽可能大的循环数。如果太小,当第一个浏览器完成时,第二个浏览器还没有启动
2.两个浏览器必须同时执行。如果test.php文件在一个浏览器中同时执行两次,无论是否同时执行,最终结果都是count=200000。因为在同一个浏览器中执行的所有命令都属于同一个会话(所有命令都经过同一个通道),所以redis会让前100,000个执行完成,然后执行另外100,000个执行。
4.事务解析和原子操作解析
4.1.交易结算
已更改test.php文件
?phpheader(' content-type : text/html;charset=utf8');$ start=time();$ Redis=new Redis();$redis-connect('192.168.95.11 ',' 6379 ');for($ I=0;10万美元;$ I){ $ redis-multi();$ count=$ redis-get(' count ');$ count=$ count 1;$redis-set('count ',$ count);$ redis-exec();} $ end=time();回显“this OKBr/”;Echo的执行时间为:“”。($ end-$ start);执行结果失败,使用事务的表名无法解决这个问题。
分析原因:
我们都知道,redis开启时,事务中的命令不会被执行,而是先将命令推入队列,然后当exec命令出现时,所有的命令都会以阻塞的方式逐个执行。
因此,当PHP中的redis类用于redis事务时,与redis相关的所有命令都不会实际执行,而只会将命令发送到Redis进行存储。
因此,下图中圈出的$count实际上不是我们想要的数据,而是一个对象,因此test.php中的11行是错误的。
检查对象计数:
4.2、原子操作incr解决方案
#更新test.php文件
?phpheader(' content-type : text/html;charset=utf8');$ start=time();$ Redis=new Redis();$redis-connect('192.168.95.11 ',' 6379 ');for($ I=0;10万美元;$ I){ $ count=$ redis-incr(' count ');} $ end=time();回显“this OKBr/”;Echo的执行时间为:“”。($ end-$ start);两个浏览器同时运行,需要14或15秒。count=20万可以解决这个问题。
缺点:
仅仅解决这里的取出加1的问题,并不能从本质上解决问题。在实际环境中,我们需要做一系列的操作,而不仅仅是取出加1,所以需要构建一个通用锁。
5.构建分布式锁
构造锁的目的是消除选择竞争,维护高并发下的数据一致性
在构造锁时,我们需要注意几个问题:
1.防止锁被持有时进程崩溃,导致死锁,其他进程不能一直得到这个锁
2.持有锁的进程因为操作时间长而自动释放锁,但进程本身并不知道,最后误释放了其他进程的锁
3.在一个进程的锁过期后,许多其他进程会尝试同时获取锁,并且它们都成功获取了锁
我们不会修改test.php文件,而是直接建立一个相对标准化的面向对象的Lock.class.php文件
#创建Lock.class,php文件
?Php#分布式锁类Lock { private $ redis=# store redis对象/** * @desc构造函数* * @ param $ host string | redis host * @ param $ port int | port */public function _ _ construct($ host,$ port=6379){ $ this-redis=new redis();$this-redis-connect($host,$ port);} /** * @desc锁定方法* * @param $lockName字符串|锁名* @param $timeout int |锁过期时间* * @return成功返回标识符/false */public函数getlock ($ lockname,$ time out=2){ $ identifier=uniqid();# get唯一标识符$ time out=ceil($ time out);#确保它是一个整数$ end=time()$ time out;While(time()$end) #循环获取锁{ if($ this-redis-set NX($lockName,$ identifier)) #检查$ lockName是否被锁定{$ this-redis-expire ($ lockname,$ time out);#为$lockName设置到期时间,以防止死锁返回$ identifier#返回一维标识符} else if($ this-redis-TTL($ lock name)==-1){ $ this-redis-expire($ lock name,$ time out);#检测是否有设定的到期时间,如果没有,添加(假设进程在客户端A未能设置时间最后一步时崩溃,客户端B可以检测到并设置时间)} usleep(0.001);# stop 0.001ms}返回false} /** * @desc释放锁* * @param $lockName字符串|锁名* @ param $标识符字符串|锁的唯一值* * @ param bool */公共函数释放锁($lockName,$ identifier){ if($ this-redis-get($ lockName)=$ identifier)#判断锁是否被其他客户端修改过{ $ this-redis-multi();$ this-redis-del($ lockName);#释放锁$ this-redis-exec();返回真;} else { return false#其他客户端修改了锁,无法删除其他客户端的锁}} /** * @desc测试* * @ param $ lock name string | lock name */public function test($ lock name){ $ start=time();for($ I=0;一万美元;$ I){ $ identifier=$ this-getLock($ lockName);if($ identifier){ $ count=$ this-redis-get(' count ');$ count=$ count 1;$this-redis-set('count ',$ count);$this-releaseLock($lockName,$ identifier);} } $ end=time();回显“this OKBr/”;Echo的执行时间为:“”。($ end-$ start);} }页眉(' content-type : text/html;charset=utf8');$ obj=新锁(' 192 . 168 . 95 . 11 ');$ obj-test(' lock _ count ');测试结果:
它在两个不同的浏览器中执行,最终的结果是count=200000,但大约需要80秒。但是,在高并发下,将同一数据锁定20万次是可以接受的,甚至是非常好的。
上面这个简单的例子只是为了模拟并发测试和检查。事实上,我们可以结合自己的项目使用Lock.class.php的锁来修改它,这样我们就可以很好地使用这个锁。比如在商场疯狂购买,虚拟商场玩家在游戏中买卖东西等等。
以上就是本文的全部内容。希望本文的内容能给大家的学习或工作带来一些帮助,也希望多多支持我们!
版权声明:Redis构建分布式锁是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。