Redis的分布式锁
1、分布式锁要解决的问题?
比如说:某个查询数据库的接口,因为调用量比较大,所以加了缓存,并设定缓存过期后刷新,问题是当并发量比较大的时候,如果没有锁机制,那么缓存过期的瞬间,大量并发请求会穿透缓存直接查询数据库,造成雪崩效应,如果有锁机制,那么就可以控制只有一个请求去更新缓存,其它的请求视情况要么等待,要么使用过期的缓存。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。其中,分布式锁是用来解决分布式应用中并发冲突的一种常用手段,实现方式有:基于zookeeper、redis、MySQL等。
这篇文章主要分析redis分布式锁的使用以及注意事项。
2、分布式锁设计目标
可以保证在分布式部署的应用集群中互斥性,即同一个方法在同一操作只能被一台机器上的一个线程执行。程序出现异常锁能自动释放,避免死锁,锁超时
3、redis的setnx
1)基础使用
查阅Redis的使用手册,可以看到:Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
基本语法:SETNX KEY_NAME VALUE
2)容易遇到的坑
比如下面这段代码
1 <?php 2 3 $ok = $redis->setNX($key, $value); 4 5 if ($ok) { 6 $cache->update(); 7 $redis->del($key); 8 } 9
我们来分析这样写可能会产生的问题:
1)因为 SetNX 不具备设置过期时间的功能,所以我们需要借助 Expire 来设置,同时我们需要把两者用 Multi/Exec 包裹起来以确保请求的原子性,以免 SetNX 成功了 Expire 却失败了
1 <?php 2 $redis->multi(); 3 $redis->setNX($key, $value); 4 $redis->expire($key, $ttl); 5 $redis->exec();
2)当多个请求到达时,虽然只有一个请求的 SetNX 可以成功,但是任何一个请求的 Expire 却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求比较密集的话,那么过期时间会一直被刷新,导致锁一直有效。于是乎我们需要在保证原子性的同时,有条件的执行 Expire,接着就需要写一个Lua 脚本来控制。
3)但是这样一个简单的功能还需要写个Lua脚本,实在有些麻烦。其实 Redis从 2.6.12版本开始 ,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。
1 <?php 2 3 $ok = $redis->set($key, $value, array('nx', 'ex' => $ttl)); 4 5 if ($ok) { 6 $cache->update(); 7 $redis->del($key); 8 }
4、应用场景
不同的应用场景,redis分布式锁的使用也是有区别的。
1)单用户并发(对同一操作发起多次请求)
单用户并发的场景有:支付、抽奖、领取奖励、刷新页面初始化用户数据等
全局唯一锁(只有用户自己能拿到这把锁)
1 public function singleTest($openid) 2 { 3 $redis = new RedisServer(REDIS_HOST, REDIS_PORT); //获取redis实例化对象 4 $key = 'single_test'.$openid; //这里openid是指用户唯一标识 5 //设置锁 6 $ok = $redis->set($key,1, array('nx', 'ex' => 10)); 7 if ($ok) { 8 //更新缓存 9 //$cache->update(); 10 if ($redis->get($key)) { 11 $redis->del($key); 12 } 13 } 14 }
2) 多用户并发(多用户同时对有限的公共资源进行修改)
多用户并发的场景有:秒杀、抢购等(短时间内多用户争夺数量有限的物品)
全局锁(也叫互斥锁、排他锁):任何一个时刻只有1人能够持有这把锁,其他人等待锁的释放,重新抢锁。
实现:Redis setnx ,Memcached add ,MySQL 行锁、表锁。
下面是用Redis来实现的一段代码:
1 public function multiTest() 2 { 3 $redis = new RedisServer(REDIS_HOST, REDIS_PORT); //获取redis实例化对象 4 $key = 'multi_test'; 5 $random = Common::generateRandom(); //引入一个随机值:唯一的字符串 7 $ok = $redis->set($key, $random, array('nx', 'ex' => 10)); 8 if ($ok) { 9 //更新缓存 10 //... 11 12 //先判断随机数,是同一个则删除锁 13 if ($redis->get($key) == $random) { 14 $redis->del($key); 15 } 16 } 17 }
这里引入随机值的原因:
如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新过程中,锁就失效了,此时另一个请求会获取锁。
但前一个请求在缓存更新完毕的时候, 如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况。所以我们在创建锁的时候需要引入一个随机值。
参考链接:https://blog.huoding.com/2015/09/14/463