分布式锁的核心思想,就是使用外部的一块共享的区域,来完成锁的实现。

一、使用mysql数据库实现(基本不用)

1、使用数据库悲观锁

可以使用select ... for update 来实现分布式锁。

例如:建一个lock表,获取锁就是插入一条数据,移除锁就是删除掉这条数据,使用mysql的for update来保证原子性。

2、使用数据库乐观锁

增加一个version字段,每次更新修改,都会自增加一。

例如:为id是1的用户余额加10

select version,balance from account where user_id ='1';

进行更新时,where条件附带上版本号:

update account set balance = balance+10 ,version = version+1 where version 
= #{version} and user_id ='1';

如果更新失败,则循环上面两步

二、使用redis实现(常用)

1、使用setnx+expire命令实现

setnx是set if not exists 的缩写,也就是只有不存在的时候才设置rediskey 设置成功时返回 1 , 设置失败时返回 0 

if(jedis.setnx(key,lock_value) == 1){ //setnx加锁
    expire(key,100); //设置过期时间
    try {
        do something  //业务处理
    }catch(){
    }
  finally {
       jedis.del(key); //释放锁
    }
}

 缺点:

  • 加锁操作和设置超时时间是分开的。假设在执行完setnx加锁后,正要执行expire设置过期时间时,出现问题,则锁永远无法释放。所以需要使用lua脚本来使setnx+expire成为原子操作
  • 如果超时时间过短,会出现业务还未结束,锁就被释放的情况。

2、Redisson框架(常用)

Redisson是一个知名的、优秀的redis客户端类库,封装了大量的基于redis的复杂的一些操作

redission原理图:

当线程加锁成功后,会启动一个后台线程,会每隔10秒检查一下,还持有锁,那么就会不断的延长锁key的30秒生存时间。因此,Redisson就是使用watch dog解决了锁过期释放,业务没执行完问题

Redisson底层大量使用lua脚本来保证原子性操作,如下面的尝试加锁代码:

简单使用方法:

1、引入依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
</dependency>

2、配置redisson 

     @Bean(name = "redisClient")
    public RedissonClient redisClient() {
        Config config = new Config();
        config.useSingleServer()
                .setPassword("123456")
                .setTimeout(1000000)
                .setAddress("redis://127.0.0.1:6379");
        ;
        return  Redisson.create(config);
    } 

3、业务中使用 

//1、获取锁
RLock rLock = redisClient.getLock("lock_key");
try{
    //2、加锁
    rLock.lock();
    //业务代码……
}finally{
    //3、释放锁
    rLock.unlock();
}

3、RedLock

redis主从复制架构的问题:

  1. 客户端 A 从 master 获取到锁
  2. 在 master 将锁同步到 slave 之前,master 宕掉了。
  3. slave 节点被晋级为 master 节点
  4. 客户端 B 取得了同一个资源被客户端 A 已经获取到的另外一个锁。安全失效!

正式为了解决上面的问题,才有了基于redis实现的分布式锁——RedLock

它能够保证以下特性:

  • 互斥性:在任何时候,只能有一个客户端能够持有锁;
  • 避免死锁:当客户端拿到锁后,即使发生了网络分区或者客户端宕机,也不会发生死锁;(利用key的存活时间)
  • 容错性:只要多数节点的redis实例正常运行,就能够对外提供服务,加锁或者释放锁;

而非redLock是无法满足互斥性的。

三、使用zookeeper实现

Zookeeper的节点Znode有四种类型:

  • 持久节点:默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在。

  • 持久顺序节点:所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号,持久节点顺序节点就是有顺序的持久节点。

  • 临时节点:和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

  • 临时顺序节点:有顺序的临时节点(用于实现分布式锁)

zookeeper分布式锁原理步骤:

  1. zookeeper首先创建一个/lock节点
  2. 当有节点获取锁时,先为这个节点创建临时节点,例如lock-702564158761685-000001,序列号按创建顺序递增。
  3. zookeeper会检查 lock-702564158761685-000001 是否/lock下的最小节点,如果是该节点得到锁,否则监听 lock-702564158761685-000001 前一个节点状态
  4. 当前一个节点的释放锁时会通知(唤醒)后一个节点,(这样只监听前一个节点的方式,避免了线程“惊群效应”)。

简单使用方法:

1、引入依赖

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
        </dependency>

 2、创建zk客户端CuratorFramework

CuratorFramework client = CuratorFrameworkFactory.builder()
                    .connectString(CONNECT_ADDR)
                    .retryPolicy(retryPolicy)
                    .connectionTimeoutMs(CONNECTION_TIMEOUT)
                    .sessionTimeoutMs(SESSION_TIMEOUT)
                    .build();
client.start();

 3、业务中使用 


//1、创建临时顺序节点
InterProcessMutex mutex = new InterProcessMutex(client, "/locks/" + workNumber) 
try{
    //2、加锁
    mutex.acquire();
    //业务代码
} finally {
    //3、解锁
    mutex.release();
}

扩展:curator recipes 中的各种锁:

  • InterProcessMutex:可重入、独占锁
  • InterProcessSemaphoreMutex:不可重入、独占锁
  • InterProcessReadWriteLock:读写锁
  • InterProcessSemaphoreV2 : 共享信号量
  • InterProcessMultiLock:多重共享锁 (将多个锁作为单个实体管理的容器)

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐