zoukankan      html  css  js  c++  java
  • 使用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;
         }
  • 相关阅读:
    蠢货之对闭包表的扩展
    蠢货之TaskCompletionSource 带事件的同步调用
    SQLSERVER新建存储过程模板
    缓存更新
    写给”源码爱好者“
    区块链-一个不神秘却总能骗人的东西
    graceful-upgrades-in-go
    谁也逃不过C++
    Go的问题
    面试
  • 原文地址:https://www.cnblogs.com/wxgblogs/p/6502868.html
Copyright © 2011-2022 走看看