数据库
首页 > 数据库> > redis分布式锁

redis分布式锁

作者:互联网

redis分布式锁的发展过程。

单机情况下,可以使用synchronized(obj),来保证同步代码块。

代码如下:

 其原理是:每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级锁)之后,该对象头Mark World中就被设置指向Monitor对象的指针。

 

对象头在JVM中存储的形式:

 

 

 而synchronized 则和对象头中的Mark Word字段相关

每一个锁都对应一个Monitor对象,每个对象都有一个与之关联的Monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的。

ObjectMonitor 中有几个值得关注的成员变量:

1. waitset :处理wait状态的线程,会被加入到这里。

2. entrylist(等待队列) :处于等待锁block状态的线程,会被加入到这里。

3. owner(monitor拥有者):指向获得ObjectMonitor对象的线程。(即获得锁的线程)

总结:BLOCKED 和WAITING都是非活动线程的状态。WAITING线程是已经分配到了CPU时间,但是需要等待事件发生所以主动释放了CPU,

直到某些事件完成后调用了notify()唤醒,也就是WAITING线程是自己现在不想要CPU时间,但是BLOCKED线程没有获得锁,所以轮不到BLOCKED线程。

当多个线程同时访问一同步代码时,首先会进入_EntryList 集合,进行阻塞等待,当线程获取到对象的Monitor后,会把_owner变量指向该线程,同时Monitor

的计数器_count 自加1。

若线程调用同步对象的方法wait()方法,将释放当前持有Monitor(锁),_owner变量重置为null,且_count 会自减1,同时线程进入_WaitSet中等待唤醒。

当线程执行完同步代码后,也将_owner和_count重置。

markword的结构:

 

当thread0 执行synchronize代码的时候,synchronized(obj)的obj对象的markword中 ptr_to_heavyweight_monitor

会指向一个monitor对象,monitor的owner是thread0

thread1执行到synchroized代码时,发现obj的markword指向了一个monitor,并且owner有人了,这时会进入entrylist进行blocked

thread0执行完同步代码退出synchronized,把obj markword里的数据还原,这些数据是存在monitor对象中的,然后唤醒entrylist的thread1和thread2的blocked线程,两个线程去抢owner

1.6 版本对synchronized进行了优化

  轻量级锁/使用场景是一个对象虽然有多个线程访问,但不出现竞争,这时使用轻量级锁来优化加锁流程:

  1. 偏向锁作用:减少同一线程获取锁的代价

  偏向锁假定将来只有第一个申请锁的线程会使用锁,因此,只需要在Mark Word中CAS记录owner,如果记录成功,则偏向锁成功,记录锁状态为偏向锁,

  以后当前线程等于owner就可以直接获得锁;否则,说明有其他线程竞争,膨胀为重量级锁。

  当一个线程多次获取一个锁时(可重入锁)

  为什么一个线程需要反复加锁?某些场景,某个对象中的synchronized 方法1 调用另一个synchronized方法2,但是方法1和方法2 都是被同一个线程调用

  的,且处于同一个对象,那么相当于这个线程获取了两次相同的锁,这就是可重入锁。

  为什么要引入偏向锁?

  引入偏向锁是为了在无多线程竞争的情况下尽量减少必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换         ThreadID的时候依赖一次CAS原子指令,代价就是一旦出现多线程竞争情况就必须撤销偏向锁。

  一个对象刚开始实例化的时候,没有任何线程来访问它。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时         候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自         己的ID,之后再此访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。

  一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象是偏向状态,这时表明在这个对象上已经存在竞争了,

检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象(锁)的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象(锁)回复成无锁状态,然后重新偏向。
原文链接:https://blog.csdn.net/qq_43061290/article/details/124187639

  2. 轻量级锁

  当有别的线程参与到偏向锁的竞争中时,会先判断markwork中线程ID与这个线程是否一致,如果不一致,则会立即撤销偏向锁,升级为轻量级锁。

  线程拿到锁的底层原理:

  每个线程都会在自己的栈中维护一个LockRecord(LR),然后每个线程在竞争锁时,都试图将锁对象头中的markword设置为指向自己LR的指针,哪个线程设置           成功,则意味着哪个线程成功获取到锁。

  3 . 重量级锁

  当线程自旋次数过长依旧没有获取到锁,为避免CPU无端耗费,锁由轻量级升级为重量级锁。获取锁的同时会阻塞其他正在竞争该锁的线程,依赖对象内部监         视器实现,monitor又依赖操作系统底层;需要从用户态切换到内核态,成本非常高。

  当系统检查到锁时重量级之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,

       这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码时间还要长。

  

  通过实验证明,当多个线程同时竞争变成了重量级锁,线程执行完毕,锁对象变成了无锁。

  此时再有一个线程去争抢锁,就从无锁变成了轻量级锁。当重量级锁释放之后,锁对象是无锁的。

  有新的线程来竞争的话又会从轻量级锁开始。

 

  synchronized(this) 代码块,在分布式集群情况下,并不能保证被同步代码块的原子性,因此需要使用分布式         锁来保证同步代码块的原子性。

  此时你写了第一个版本的redis分布式锁:

  SETNX key value   SET if Not eXists (如果不存在,则SET)的简写。

  Boolean     result   =    setnx key value;

  try{

    if(result) {

    //代码块

    }

  }

  }catch(){

  }finally{

  //释放锁

  }

  此时当机器宕机的时候,此时锁就被永久占用了,因此当释放锁失败的时候,通过自动过期来保证

  此时你加了个过期时间,但为了保证加锁以及设置过期时间的原子性,此时你写了这段代码

  try{

    String lockResult = redis.set(key, value, "NX", "EX", EXPIRE_SECS);

    if("OK".equalsIgnoreCase(lockResult)){

      //业务操作

      redis.del(key);

    }

  }

  这依然存在问题:误删。

  在获取分布式锁的时候,设置value的时候,使用UUID

  在删除的时候,通过判断是否是当前UUID,如果是则删除,如果不是,就不要删除。

  但当出现full GC的时候,由于判断和删除不是原子性的,因此还是会有误删除操作。

  主要原因是:当判断锁的时候,判断通过,当前服务full GC,其他服务获取到超时之后的锁。

        等full gc 结束后,执行删除操作,导致锁被释放,其余线程或者服务再次获取。

  此时可以使用lua脚本。因为其相对于事务,可以在服务端一次性执行更复杂的操作(什么逻辑判断啊、获取

  某一缓存的时候,同时延长其过期时间)。

  

  这种方法也依然会有问题:
  1. 超时释放,此时同步代码块有被同时进行的风险
  2. 不可重试,只尝试一次就返回false

  3. 不可重入,同一个线程无法多次获取同一把锁

  4. 主从一致性,主从同步存在延迟,当主宕机时,如果从没来的同步主的数据,则会出现锁失效

  

  此时祭出大招 Redisson

  其核心内容如下:

  //代码一

  - - 判断是否存在

  if(redis.call(‘exists’,key)==0)then

    - - 不存在,获取锁

    redis.call('hset',key,threadId,'1');

    - - 设置有效期

    redis.call('expire',key,releaseTime);

    return 1; - -返回结果

  end;

  - - 锁已经存在,判断threadId是否是自己

  if(redis.call('hexists',key,threadId)==1) then

    - - 不存在,获取锁,重入次数+1

    redis.call(‘hincrby’,key,threadId,'1');

    - - 设置有效期

    redis.call(‘expire’,key,releaseTime);

    return 1;  - - 返回结果

  end;

  

  1. 解决超时释放,使用看门狗。

    客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,每隔一段时间检查一下,如                 果客户端1还持有锁,那么就会不断延长锁key的生存时间。

  2. 可重试,设置while循环,不停尝试加锁

    客户端2来尝试加锁,执行了同样的一段lua脚本,第一个if判断会执行“exists myLock”,发现myLock这个锁

  已经存在了。

    接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是,           因为那里包含的是客户端1的ID。

    此时客户端2会进入一个while循环,不停的尝试加锁。

  3. 可重入锁加锁机制,第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个          ID,就是客户端1的那个ID,此时就会执行可重入加锁的逻辑,他会用:

  incrby key ,value:1 1 通过这个命令,对客户端1的加锁次数,累加1。

  4. 主从一致性

    设立多个redis作为主节点。

    只有每个都获取成功的时候,才会去执行。

  redission作为分布式锁,其读写情况会有
  1. 
  2.

  

  

  

  

  

 

标签:加锁,对象,redis,线程,key,偏向,分布式
来源: https://www.cnblogs.com/followers/p/16376033.html