Redis的分布式锁

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

原文地址:https://www.cnblogs.com/hld123/p/14107053.html