使用Redis实现分布式锁

  在天猫、京东、苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形下会对数据库服务器、文件服务器、应用服务器造成巨大的压力,严重时甚至宕机了。另一个问题是,秒杀的东西都是有量的,例如一款手机只有10台的量秒杀,那么,在高并发的情况下,成千上万条数据更新数据库(例如10台的量被人抢一台就会在数据集某些记录下减1),那这个时候的先后顺序是很乱的,很容易出现10台的量,抢到的人就不止10个这种严重的问题。那么,以后所说的问题我们该如何去解决呢?使用 分布式锁任务队列,本节主要阐述基于redis的分布式锁实现思路:

  思路很简单,主要用到的redis函数是setnx(),这个应该是实现分布式锁最主要的函数。首先是将某一任务标识名(这里用Lock:order作为标识名的例子)作为键存到redis里,并为其设个过期时间,如果是还有Lock:order请求过来,先是通过setnx()看看是否能将Lock:order插入到redis里,可以的话就返回true,不可以就返回false。

 (1)为避免特殊原因导致锁无法释放,在加锁成功后,锁会被赋予一个生存时间(通过lock方法的参数设置或者使用默认值),超出生存时间锁会被自动释放,锁的生存时间默认比较短(秒级),因此,若需要长时间加锁,可以通过expire方法延长锁的生存时间为适当时间,比如在循环内。

 (2)系统级的锁当进程无论何种原因时出现crash时,操作系统会自己回收锁,所以不会出现资源丢失,但分布式锁不用,若一次性设置很长时间,一旦由于各种原因出现进程crash 或者其他异常导致unlock未被调用时,则该锁在剩下的时间就会变成垃圾锁,导致其他进程或者进程重启后无法进入加锁区域。

  先看加锁的实现代码:这里需要主要两个参数,一个是$timeout,这个是循环获取锁的等待时间,在这个时间内会一直尝试获取锁直到超时,如果为0,则表示获取锁失败后直接返回而不再等待(非阻塞);另一个重要参数的$expire,这个参数指当前锁的最大生存时间,以秒为单位的,它必须大于0,如果超过生存时间锁仍未被释放,则系统会自动强制释放。

  有一步严谨的操作,那就是取得当前键的剩余时间,假如这个时间小于0,表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)如果出现这种状况,那就是进程的某个实例setnx成功后crash导致紧跟着的expire没有被调用,这时可以直接设置expire并把锁纳为己用。如果没设置锁失败的等待时间或者已超过最大等待时间了,那就退出循环,反之则隔 $waitIntervalUs 后继续 请求。

public boolean lock(long timeout, int expireSecs) {
         long nano = System.nanoTime();
         timeout *= MILLI_NANO_CONVERSION;
         String lockStart = String.valueOf(System.currentTimeMillis());
         Jedis jedis = JedisUtil.getResource();
         try {
              while ((System.nanoTime() - nano) < timeout) {
                   if (jedis.setnx(this.key, lockStart) == 1) {
                       jedis.expire(this.key, expireSecs);
                       this.locked = true;
                       return this.locked;
                   }
                   // 短暂休眠,避免出现活锁
                   Thread.sleep(3, RANDOM.nextInt(500));
              }
              String expireStr = jedis.get(key);
              Long now = System.currentTimeMillis();
              String nowStr = String.valueOf(now);
              if (!StringUtils.isNumeric(expireStr)){
                   return false;
              }
             //ttl小于0 表示key上没有设置生存时间(key是不会不存在的,因为前面setnx会自动创建)           
            //如果出现这种状况,那就是进程的某个实例setnx成功后crash 导致紧跟着的expire没有被调用                    
            //这时可以直接设置expire并把锁纳为己用
            //In Redis 2.6 or older, if the Key does not exists or does not have an associated expire, -1 is returned.
            //In Redis 2.8 or newer, if the Key does not have an associated expire, -1 is returned or if the Key does not exists, -2 is returned.
            long ttlValue = jedis.ttl(this.key);
            if(ttlValue<0){
                jedis.setnx(key, nowStr);
                jedis.expire(key, expireSecs);
                return true;
            }   

            Long expireLong = Long.parseLong(expireStr);
            if (now - expireLong > expireSecs * 1000){
                   jedis.del(key);
                   jedis.setnx(key, nowStr);
                   jedis.expire(key, expireSecs);
                   return true;
              }
         } catch (Exception e) {
              if (jedis != null) {
                   JedisUtil.returnBrokenResource(jedis);
              }
              throw new RuntimeException("Locking error", e);
         } finally {
              if (jedis != null) {
                   JedisUtil.returnResource(jedis);
              }
         }
         return false;
     }

 

posted @ 2017-03-04 21:57  简单爱_wxg  阅读(1193)  评论(0编辑  收藏  举报