分布式锁的基本原理和实现以及synchronized底层原理

1.1Synchronized 

Synchronized的重点级锁,底层是基于锁监督器(Monitor)来实现,简单来说就是锁对象头会指向一个锁监督器,而在监督器中则会记录一些信息,比如:

  • _owner:持有锁的线程
  • _recursions:锁重入次数

因此每一个锁对象,都会指向一个锁监视器,而每一个锁监视器,同一时刻只能被一个线程持有,这样就实现了互斥效果。但前提是,多个线程使用的是同一把锁

比如有三个线程来争抢锁资源,线程1获取锁成功,如图所示:

分布式锁的基本原理和实现以及synchronized底层原理_第1张图片

此时其它线程想要获取锁,会发现监视器中的_owner已经有值了,就会获取锁失败。由于咱们代码在锁对象是用户id的字符串常量,因此同一个用户肯定是同一把锁,线程是绝对安全的。

但问题来了,我们的服务将来肯定会多实例不是,形成集群。每一个实例都会有一个自己的JVM运行环境,因此即便是同一个用户,如果并发的发起了多个请求,由于请求进入了多个JVM,就会出现多个锁对象(用户id对象),自然就有多个锁监视器。此时就会出现每个JVM内部都有一个线程获取锁成功的情况,没有达到互斥的效果,并发安全问题就可能再次发生了:

分布式锁的基本原理和实现以及synchronized底层原理_第2张图片

 可见,在集群环境下,JVM提供的传统锁机制就不再安全了。

那么改如果解决这个问题呢?

显然,我们不能让每个实例去使用各自的JVM内部锁监视器,而是应该在多个实例外部寻找一个锁监视器,多个实例争抢同一把锁

分布式锁的基本原理和实现以及synchronized底层原理_第3张图片

像这样的锁,就称为分布式锁

分布式锁必须要满足的特征:

  • 多JVM实例都可以访问

  • 互斥

能满足上述特征的组件有很多,因此实现分布式锁的方式也非常多,例如:

  • 基于MySQL

  • 基于Redis

  • 基于Zookeeper

  • 基于ETCD

但目前使用最广泛的还应该是基于Redis的分布式锁。

1.2.简单分布式锁

Redis本身可以被任意JVM实例访问,同时Redis中的setnx命令具备互斥性,因此符合分布式锁的需求。不过实现分布式锁的时候还有一些细节需要考虑,绝不仅仅是setnx这么简单。

 

1.2.1.基本原理

Redis的setnx命令是对string类型数据的操作,语法如下:

# 给key赋值为value
SETNX key value

当前仅当key不存在的时候,setnx才能执行成功,并且返回1,其它情况都会执行失败,并且返回0.我们就可以认为返回值是1就是获取锁成功,返回值是0就是获取锁失败,实现互斥效果。

而当业务执行完成时,我们只需要删除这个key即可释放锁。这个时候其它线程又可以再次获取锁(执行setnx成功)了。

# 删除指定key,用来释放锁
DEL key

假设说一开始lock不存在,有很多线程同时对lock执行setnx命令。由于Redis命令本身是串行执行的,也就是各个线程是串行依次执行。因此当第一个线程执行setnx时,会成功添加这个lock。但其余的线程会发现lock已经存在,自然就执行失败。自然就实现了互斥效果。

当业务执行完毕,直接删除lock,自然就释放锁了

# 释放锁
DEL lock

不过我们要考虑一种极端情况,比如我们获取锁成功,还未释放锁呢当前实例突然宕机了!那么释放锁的逻辑自然就永远不会被执行,这样lock就永远存在,再也不会有其它线程获取锁成功了!出现了死锁问题。

怎么办?

我们可以利用Redis的KEY过期时间机制,在获取锁时给锁添加一个超时时间:

# 获取锁,并记录持有锁的线程
SETNX lock thread1
# 设置过期时间,避免死锁
EXPIRE lock 20

令来释放锁。

但是如果当前服务实例宕机,DEL无法执行。但由于我们设置了20秒的过期时间,当超过这个时间时,锁会因为过期被删除,因此就等于释放锁了,从而避免了死锁问题。这种策略就是超时释放锁策略。

但新的问题来了,SETNX和EXPIRE是两条命令,如果我执行完SETNX,还没来得急执行EXPIRE时服务已经宕机了,这样加锁成功,但锁超时时间依然没能设置!死锁问题岂不是再次发生了?!

所以,为了解决这个问题,我们必须保证SETNX和EXPIRE两个操作的原子性。事实上,Redis中的set命令就能同时实现setnx和expire的效果:

# NX 等同于SETNX lock thread1效果;
# EX 20 等同于 EXPIRE lock 20效果
SET lock thread1 NX EX 20

综上,利用Redis实现的简单分布式锁流程如下:

分布式锁的基本原理和实现以及synchronized底层原理_第4张图片

1.2.2代码实现

 

package com.tianji.promotion.utils;

import com.tianji.common.utils.BooleanUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

@RequiredArgsConstructor
public class RedisLock {

    private final String key;
    private final StringRedisTemplate redisTemplate;

    /**
     * 尝试获取锁
     * @param leaseTime 锁自动释放时间
     * @param unit 时间单位
     * @return 是否获取成功,true:获取锁成功;false:获取锁失败
     */
    public boolean tryLock(long leaseTime, TimeUnit unit){
        // 1.获取线程名称
        String value = Thread.currentThread().getName();
        // 2.获取锁
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, leaseTime, unit);
        // 3.返回结果
        return BooleanUtils.isTrue(success);
    }

    /**
     * 释放锁
     */
    public void unlock(){
        redisTemplate.delete(key);
    }
}

分布式锁的基本原理和实现以及synchronized底层原理_第5张图片

分布式锁的基本原理和实现以及synchronized底层原理_第6张图片 经测试,确实解决了集群下的并发安全问题。

你可能感兴趣的:(分布式,redis,缓存,数据库,java)