悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。redis分布式锁 (原文来自:)
一、分布式锁的作用:
redis写入时不带锁定功能,为防止多个进程同时进行一个操作,出现意想不到的结果,so...对缓存进行插入更新操作时自定义加锁功能。
二、Redis的NX后缀命令
Redis有一系列的命令,其特点是以NX结尾,NX的意思可以理解为 NOT EXISTS(不存在),SETNX命令 (SET IF NOT EXISTS) 可以理解为如果不存在则插入,Redis分布式锁的实现主要就是使用SETNX命令。
三、实现原理
在进程请求执行操作前进行判断,加锁是否成功,加锁成功允许执行下步操作;
如果不成功,则判断锁的值(时间戳)是否大于当前时间,如果大于当前时间,则获取锁失败不允许执行下步操作;
如果锁的值(时间戳)小于当前时间,并且GETSET命令获取到的锁的旧值依然小于当前时间,则获取锁成功允许执行下步操作;
如果锁的值(时间戳)小于当前时间,并且GETSET命令获取到的锁的旧值大于当前时间,则获取锁失败不允许执行下步操作;
四、$redis->setnx() 设置锁
$expire = 10;//有效期10秒$key = 'lock';//key$value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期$lock = $redis->setnx($key, $value);//判断是否上锁成功,成功则执行下步操作if(!empty($lock)){ //下步操作... }
如果返回 1 ,则表示当前进程获得锁,并获得了当前插入/更新缓存的操作权限。
如果返回 0,表示锁已被其他进程获取,这是进程可以返回结果或者等待当前锁失效再请求。
五、解决死锁
如果只用SETNX命令设置锁的话,如果当持有锁的进程崩溃或删除锁失败时,其他进程将无法获取到锁,问题就大了。
解决方法是在获取锁失败的同时获取锁的值,并将值与当前时间进行对比,如果值小于当前时间说明锁以过期失效,进程可运用Redis的DEL命令删除该锁。
$expire = 10;//有效期10秒$key = 'lock';//key$value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期$status = true;while($status){ $lock = $redis->setnx($key, $value); if(empty($lock)) { $value = $redis->get($key); if($value < time()) { $redis->del($key); } }else{ $status = false; //下步操作.... }}
但是,简单粗暴的用DEL命令删除锁再SETNX命令上锁也会出现问题。比如,进程1获得锁后崩溃或删除锁失败,这时进程2检测到锁存在当已过期,用DEL命令删除锁并用SETNX命令设置锁,进程3也检测到锁过期,也用DEL命令删除锁也用SETNX命令设置了锁,这时进程2和进程3同时获得了锁。问题大了!
为了解决这个问题,这里用到了Redis的GETSET命令,GETSET命令在给锁设置新值的同时返回锁的旧值,这里利用了GETSET命令同时获取和赋值的特性,在此期间其他进程无法修改锁的值。
例如:
进程1获得锁后操作超时/崩溃/删除锁失败,
进程2检测到锁已存在,但获取锁的值对比当前时间发现锁已过期,
进程2通过GETSET命令重新给锁赋予新的值,并获取到的锁的旧值,再次对比锁的旧值与当前时间,如果锁的旧值依然小于当前时间的话,这时进程2就可以忽略进程1余留下的废锁进行下步操作了。
进程2完成下步操作后返回前应该删除锁,但在删除锁时可以先检测锁是否还未过期,未过期才做删除操作,已过期的就没必要在去删除锁了,因为很有可能其他进程检测到锁过期时已经去获取锁了。
这里要说明的是,如果有其他进程在进程2之前获取到锁,那么进程2将获取锁失败,但是进程2在用GETSET获取锁的旧值时也赋予了锁新的值,改写了其他进程赋予锁的超时值。看到这大家可能会有疑问了,进程2没获取到锁怎么能改变锁的值呢?是的,进程2改变了锁的原有值,但这一点小小的时间误差带来的影响是可以忽略。
以下是Redis实现分布式锁的完整PHP代码:
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秒后再尝试执行操作 } }}