1、了解分布式锁
1.1、分布式锁的作用
在单进程的系统中,当遇到并发情况下,会出现一些数据异常的问题,但是如果这些数据是需要保证唯一性的话,那我们就希望在同一时刻,只能有一个线程在执行这块代码,通常我们一般都是通过简单的加锁或同步来实现并解决这个问题。
但是以上都是单进程多线程的情况,如果出现多进程多线程,显然会出现问题。因为多线程之间是可以共享内存的,但是多进程之间是不行的,所以这个时候需要用到分布式锁。
1.2、常用的分布式锁实现方案
分布式锁通常是借助于一个第三方组件并利用它自身的排他性来达到多进程的互斥。如下:
(1)基于数据库:锁实现也有两种方式,一是基于数据库表(创建一张锁表),另一种是基于数据库排他锁。
(2)基于 zookeeper
:锁的实现是依靠临时有序节点,每个客户端对某个方法加锁时,在 zookeeper
上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
(3)基于缓存:下面我们要重点讲的就是 redis
。基于 Redis 的 NX
EX
参数
1.3、基于redis的分布式锁实现
Redis有一系列的命令,主要运用到以下几个命令
SETNX key value 命令
只在键key
不存在的情况下, 将键key
的值设置为value
。
若键key
已经存在, 则SETNX
命令不做任何动作。
SETNX
是『SET If Not Exists』(如果不存在,则 SET)的简写。
GET key 命令
如果键key
不存在, 那么返回特殊值nil
; 否则, 返回键key
的值。
如果键key
的值并非字符串类型, 那么返回一个错误, 因为GET
命令只能用于字符串值。
DEL key 命令
为给定key
设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。
GETSET key value 命令
返回给定键key
的旧值。
如果键key
没有旧值, 也即是说, 键key
在被设置之前并不存在, 那么命令返回nil
。
当键key
存在但不是字符串类型时, 命令返回一个错误。
EXPIRE key seconds 命令
为给定key
设置生存时间,当key
过期时(生存时间为 0 ),它会被自动删除。在 Redis 中,带有生存时间的
key
被称为『易失的』(volatile)。生存时间可以通过使用
DEL
命令来删除整个key
来移除,或者被SE
T 和GETSET
命令覆写(overwrite),这意味着,如果一个命令只是修改(alter)一个带生存时间的 key 的值而不是用一个新的key
值来代替(replace)它的话,那么生存时间不会被改变。比如说,对一个
key
执行INCR
命令,对一个列表进行LPUSH
命令,或者对一个哈希表执行HSET
命令,这类操作都不会修改key
本身的生存时间。另一方面,如果使用
RENAME
对一个key
进行改名,那么改名后的key
的生存时间和改名前一样。
RENAME
命令的另一种可能是,尝试将一个带生存时间的key
改名成另一个带生存时间的another_key
,这时旧的another_key
(以及它的生存时间)会被删除,然后旧的key
会改名为another_key
,因此,新的another_key
的生存时间也和原本的key
一样。使用
PERSIST
命令可以在不删除key
的情况下,移除key
的生存时间,让key
重新成为一个『持久的』(persistent)key
。
TTL key 命令
当key
不存在时,返回 -2 。 当key
存在但没有设置剩余生存时间时,返回 -1 。 否则,以秒为单位,返回key
的剩余生存时间。
<?php /** * 实现Redis分布锁 */ $key = 'test'; //要更新信息的缓存KEY $lockKey = 'lock:'.$key; //设置锁KEY $lockExpire = 5; //设置锁的有效期为5秒 //获取缓存信息 $result = $redis->get($key); //判断缓存中是否有数据 if(empty($result)) { $status = TRUE; while ($status) { //设置锁值为当前时间戳 + 有效期 $lockValue = time() + $lockExpire; /** * 创建锁 * 试图以$lockKey为key创建一个缓存,value值为当前时间戳 * 由于setnx()函数只有在不存在当前key的缓存时才会创建成功 * 所以,用此函数就可以判断当前执行的操作是否已经有其他进程在执行了 * @var [type] */ $lock = $redis->setnx($lockKey, $lockValue); /** * 满足两个条件中的一个即可进行操作 * 1、上面一步创建锁成功; * 2、 1)判断锁的值(时间戳)是否小于当前时间 $redis->get() * 2)同时给锁设置新值成功 $redis->getset() */ if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() )) { //给锁设置生存时间 $redis->expire($lockKey, $lockExpire); //****************************** //此处执行插入、更新缓存操作... //****************************** //以上程序走完删除锁 //检测锁是否过期,过期锁没必要删除 if($redis->ttl($lockKey)) $redis->del($lockKey); $status = FALSE; }else{ /** * 如果存在有效锁这里做相应处理 * 等待当前操作完成再执行此次请求 * 直接返回 */ sleep(2);//等待2秒后再尝试执行操作 } } }