基于redis实现分布式锁介绍

基于redis实现分布式锁介绍

前言

本文主要用于介绍常规分布式锁的使用及其原理,在主篇中进行了常规分布式锁的扫盲介绍,在子篇中介绍了现主流分布式锁框架的源码以及自写学习demo解析。
全部代码及介绍:https://gitee.com/FWEM/distributed-lock
文章主要分为以下两个部分:

1. 基于Jedis实现分布式锁

首先来讲一下基于jedis实现的分布式锁,主要介绍一下基于redis实现分布式锁的基本思路,该锁没有实现锁续时,可重入的功能,且该锁为不公平锁。

1、基本实现思路

首先来讲一下基于redis实现分布式锁的基本实现思路,其主要包括加锁、解锁、阻塞等待三个部分。

加锁

  1. 基本原理

    • 客户端向redis中插入key、value数据,key表示分布式锁资源名称,value表示当前客户端的标识。

    • 插入时首先查看该分布式锁资源(key)是否已被创建,已创建则返回null表示获取资源失败。

    • 创建成功则返回OK,然后执行相应业务。

    • 为了避免获取到分布式锁资源的客户端卡死导致key一直不释放,因此这里需要设置一个key过期时间。

  2. 指令

    set key value [ex seconds] [nx]
    
    例子:当redis中key为test记录不存在时,插入数据{text,hello},key过期时间为20秒
    set test hello ex 20 nx
    创建成功返回ok,失败返回null
    
  3. Java代码(这里redis的版本为3.0.1)

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey    锁
     * @param clientUUID 请求标识
     * @param expireTime 超期时间,单位:毫秒
     * @return 是否获取成功
     */
    public static boolean tryAndGetDistributedLock(String lockKey, String clientUUID, int expireTime) {
    
         // 设置nx(不存在才创建),px(该key过期时间)
         SetParams setParams = new SetParams().nx().px(expireTime);
         String result = jedis.set(lockKey, clientUUID, setParams);
    
         // 是否获取成功
         return LOCK_SUCCESS.equals(result);
    }
    

    4.存在问题

    由于这里设置的过期时间是固定的,因此可能存在一种情况当客户端业务没有执行完但是key过期释放了,因此这里设置的过期时间不能过短,要和业务常规执行耗时结合。但是较好的方案是开启一个新线程不断地去检查当前key是否存在,存在则对该key进行续时操作。后面讲基于redisson实现的分布式锁时会讲到在redisson中使用了watchdog来不断为key续时解决了这一问题。

阻塞等待

  1. 基本原理

    • 当尝试获取分布式锁资源(插入key)失败后,订阅某个channel信息,然后使程序阻塞
    • 持有锁的客户端释放锁时,同步往某个channel中发布key释放的消息
    • 订阅了该频道的客户端收到了该消息后解除阻塞,重新尝试获取分布式锁
  2. 指令

    // 订阅频道
    SUBSCRIBE channelName
    
    // 发布消息
    PUBLISH channelName message
    
  3. Java代码

    /**
     * 往名为channelName的频道中发送消息
     *
     * @param channelName channel名称
     * @param message     消息
     */
    public static void publish(String channelName, String message) {
         jedis.publish(channelName, message);
    }
    
    /**
     * 订阅channel
     *
     * @param jedisPubSub jedisPubSub实例
     * @param channelName channel名称
     */
    public static void subscribe(JedisPubSub jedisPubSub, String channelName) {
         jedis.subscribe(jedisPubSub, channelName);
    }
    

解锁

  1. 基本原理

    • 首先获取当前分布式资源(key)对应的value值
    • 对比该value值是否和当前执行释放资源的客户端UUID相等
    • 相等则释放资源成功
    • 不相等则抛出异常
  2. 指令

    // 这里为了保证原子性操作使用lua脚本来进行value比较及key删除操作
    if redis.call('get', KEYS[1]) == ARGV[1] then 
        return redis.call('del', KEYS[1]) 
    else 
        return 0 
    end
    
  3. Java代码

    /**
     * 释放分布式锁
     *
     * @param lockKey    锁
     * @param clientUUID 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(String lockKey, String clientUUID) {
    
         String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
         Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(clientUUID));
    
         // 此处包含了key被删除了或者value被修改了的两种情况
         return RELEASE_SUCCESS.equals(result);
    }
    

2、样例

描述: 数据库中设定某商品基本信息(名为外科口罩,数量为10),多进程对该商品进行抢购,当商品数量为0时结束抢购。

创建数据库表

# 创建数据库表
create table `database_lock_2`(
	`id` BIGINT NOT NULL AUTO_INCREMENT,
	`good_name` VARCHAR(256) NOT NULL DEFAULT "" COMMENT '商品名称',
	`good_count` INT NOT NULL COMMENT '商品数量',
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表2';

# 插入原始数据
insert into database_lock_2 (good_name,good_count) values ('医用口罩',10);

代码清单
maven依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.1</version>
</dependency>

JedisService类,内部包含了尝试获取锁、释放锁、发布消息、订阅channel的,初始化jedis连接的逻辑

import lombok.extern.slf4j.Slf4j;
import pers.zifeng.distributed.lock.utils.PropertiesReader;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import redis.clients.jedis.params.SetParams;

import java.io.IOException;
import java.util.Collections;
import java.util.Properties;

/**
 * @author: zf
 * @date: 2021/06/22 16:59:46
 * @version: 1.0.0
 * @description: 提供redis服务
 */
@Slf4j
public class JedisLockService {

    private static Jedis jedis;
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;

    static {
        try {
            Properties properties = PropertiesReader.getProperties("redis.properties");
            // 初始化
            String ip = properties.getProperty("ip");
            int port = Integer.parseInt(properties.getProperty("port"));
            int database = Integer.parseInt(properties.getProperty("database"));
            String password = properties.getProperty("password");

            jedis = new Jedis(ip, port);
            if (!password.isEmpty()) {
                jedis.auth(password);
            }
            jedis.select(database);
        } catch (IOException e) {
            log.error("初始化jedis失败!", e);
        }

    }

    public static void close() {
        jedis.close();
    }

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey    锁
     * @param clientUUID 请求标识
     * @param expireTime 超期时间,单位:毫秒
     * @return 是否获取成功
     */
    public static boolean tryAndGetDistributedLock(String lockKey, String clientUUID, int expireTime) {

        // 设置nx(不存在才创建),px(该key过期时间)
        SetParams setParams = new SetParams().nx().px(expireTime);
        String result = jedis.set(lockKey, clientUUID, setParams);

        return LOCK_SUCCESS.equals(result);
    }

    /**
     * 释放分布式锁
     *
     * @param lockKey    锁
     * @param clientUUID 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(String lockKey, String clientUUID) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(clientUUID));

        return RELEASE_SUCCESS.equals(result);
    }

    /**
     * 往名为channelName的频道中发送消息
     *
     * @param channelName channel名称
     * @param message     消息
     */
    public static void publish(String channelName, String message) {
        jedis.publish(channelName, message);
    }

    /**
     * 订阅channel
     *
     * @param jedisPubSub jedisPubSub实例
     * @param channelName channel名称
     */
    public static void subscribe(JedisPubSub jedisPubSub, String channelName) {
        jedis.subscribe(jedisPubSub, channelName);
    }

}

启动类

import jodd.util.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import pers.zifeng.distributed.lock.common.MysqlService;
import pers.zifeng.distributed.lock.redis.service.JedisLockService;
import redis.clients.jedis.JedisPubSub;

import java.lang.management.ManagementFactory;
import java.util.UUID;

/**
 * @author: zf
 * @date: 2021/06/23 09:40:56
 * @version: 1.0.0
 * @description: 基于jedis实现的分布式锁
 */
@Slf4j
public class JedisLock {
    private static final Object lock = new Object();
    // 当前线程号
    private static final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
    // 锁名称
    private static final String LOCK_KEY = "buyMask";
    // channel名称
    private static final String CHANNEL_NAME = "buyMask-channel";
    // 接收消息处理
    private static final MyJedisPubSub myJedisPubSub = new MyJedisPubSub();//处理接收消息

    public static void buyMaskWithLock() {
        boolean getLock, releaseLock;
        String clientUUID;
        try {
            while (true) {
                // 客户端uuid标识
                clientUUID = UUID.randomUUID().toString();
                log.info("【当前线程:" + pid + "】 尝试获取锁!");
                getLock = JedisLockService.tryAndGetDistributedLock(LOCK_KEY, clientUUID, 10 * 3000);
                if (getLock) {
                    log.info("【当前线程:" + pid + "】 获取锁成功!");
                    if (!MysqlService.buyMask()) {
                        break;
                    }
                    releaseLock = JedisLockService.releaseDistributedLock(LOCK_KEY, clientUUID);
                    if (releaseLock) {
                        log.info("【当前线程:" + pid + "】 释放锁成功!");
                        JedisLockService.publish(CHANNEL_NAME, LOCK_KEY + " release!");
                        ThreadUtil.sleep(10 * 1000);
                    } else {
                        throw new Exception("【当前线程:" + pid + "】 释放锁失败,当前锁持有者已改变!");
                    }
                } else {
                    log.info("【当前线程:" + pid + "】 获取锁失败!阻塞后重新获取!");
                    synchronized (lock) {
                        new Thread(() -> JedisLockService.subscribe(myJedisPubSub, CHANNEL_NAME)).start();
                        lock.wait(20 * 1000);
                        myJedisPubSub.unsubscribe(CHANNEL_NAME);
                    }
                }
            }
        } catch (Exception e) {
            log.error("抢购口罩失败!", e);
        } finally {
            JedisLockService.close();
        }
    }

    // 启动
    public static void main(String[] args) {
        buyMaskWithLock();
    }

    /**
     * 继承JedisPubSub,重写接收消息的方法
     */
    static class MyJedisPubSub extends JedisPubSub {
        @Override
        public void onMessage(String channel, String message) {
            log.info("【当前线程:" + pid + "】 接收到消息:" + message);
            if ((LOCK_KEY + " release!").equals(message)) {
                synchronized (lock) {
                    lock.notify();
                }
            }
        }

    }

}

三个进程执行情况

3、总结

该分布式锁案例主要用于学习redis分布式锁实现的基本原理,可以看到该锁为不公平锁,实现了分布式锁获取、释放、阻塞等待功能。对于可重入、锁续时等功能没有进行实现,后面介绍redisson可重入锁时会介绍其内部是如何实现这一功能的。

2. 基于Redisson实现可重入锁

1、基本实现思路

加锁

  • 在redis的某个hash表key(资源相关字段)中指定某个字段field的值,初始值为1。

  • 为了避免死锁设置key的过期时间

  • 插入操作使用lua脚本,分为以下两种情况:

    【1】首先查看当前哈希表key是否存在,不存在则创建该哈希表并且指定对应的field值为1,这里的field值与当前客户端相关,一般形式为{客户端uuid:线程id},指定成功则表示分布式锁获取成功

    【2】若当前哈希表key存在,则查看当前哈希表key中是否存在表示当前客户端的field,是则可重入次数加一,不存在则返回该锁剩余过期时间ttl

  • 当获取分布式锁失败时会返回当前该锁ttl,此时首先订阅该key的channel,当key删除时获得消息通知

  • 然后程序进入自旋,等待时间为ttl,如果在自旋过程中key释放了则会提前结束自旋状态重新尝试获取锁,或者自旋时间大于ttl后,则重新获取ttl时间以及进入下一次自旋

解锁

  • 解锁则首先获取当前分布式锁的持有者是否为当前客户端,这里通过查看当前hash表中是否存在表示当前客户端的field,存在则检测重入次数,如果不为0则可重入次数-1,如果为0则删除对应哈希表key。不存在当前客户端field则表示当前锁持有者不为当前客户端,抛出异常

2、基本流程

3、代码分析

这里以redisson中的可重入锁来进行分析,值得注意的是这里讨论的为不公平锁,redisson也提供了公平的分布式可重入锁。关于redisson分布式锁更多用法可以看Redisson分布式锁

基本使用方法

首先来看看他的基本使用方法

public static void main(String[] args) {
    // 配置信息
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    // 没密码这里注释掉即可
    config.useSingleServer().setPassword("123456");
    
    // 创建redisson客户端
    final RedissonClient client = Redisson.create(config);  
    // 初始化锁实例
    RLock lock = client.getLock("lock1");
    
    try{
        // 加锁
        lock.lock();
    }finally{
        // 解锁
        lock.unlock();
        client.shutdown();
    }
}

锁实例获取

首先RedissonClient.getLock()方法返回一个RedissonLock对象,然后查看RedissonLock的构造方法里面主要进行一些属性的初始化

// 返回RedissonLock对象
public RLock getLock(String name) {
    return new RedissonLock(connectionManager.getCommandExecutor(), name);
}

// RedissonLock对象构造方法,主要进行属性初始化
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
    super(commandExecutor, name);
    // 指定执行器
    this.commandExecutor = commandExecutor;
    // 指定客户端id,类型uuid
    this.id = commandExecutor.getConnectionManager().getId();
    // 指定内部锁过期时间
    this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
    // 指定名称
    this.entryName = this.id + ":" + name;
}

加锁

当我们调用lock.lock()方法时,内部调用lockInterruptibly()方法,具体的加锁逻辑在这个方法里面

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    // 获取当前线程id
    long threadId = Thread.currentThread().getId();
    // 尝试获取锁,ttl为null则表示获取锁成功,不为null则表示该分布式锁剩余时间
    Long ttl = this.tryAcquire(leaseTime, unit, threadId);
    // 获取锁失败
    if (ttl != null) {
        // 订阅该channel,当key删除时会收到消息,释放下面的自旋,下面会讲里面做了些什么
        RFuture<RedissonLockEntry> future = this.subscribe(threadId);
        this.commandExecutor.syncSubscription(future);

        try {
            while(true) {
                // 尝试获取锁,ttl为null则表示获取锁成功,不为null则表示该分布式锁剩余时间
                ttl = this.tryAcquire(leaseTime, unit, threadId);
                if (ttl == null) {
                    // 获取成功退出
                    return;
                }

                if (ttl >= 0L) {
                    // 尝试去获取当前状态是否能获得锁,尝试时间为ttl,超过ttl则进入下一次循环
                    this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    // 检查当前状态是否能获得锁
                    this.getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            // 取消订阅
            this.unsubscribe(future, threadId);
        }
    }
}

上述的代码有两个最重要的逻辑

  • 实际的加锁逻辑是怎么实现的
  • 该分布式锁的等待获取锁是怎么实现的

实际加锁逻辑

首先看一下实际的加锁逻辑是怎么实现的,其顶层调用为this.tryAcquire(leaseTime, unit, threadId),该方法最后会执行tryAcquireAsync这个方法,下面看一下tryAcquireAsync的代码

// 这里根据lock.lock(leaseTime)时是否指定了leaseTime来执行两段逻辑
// 这里的leaseTime相当于锁的过期时间,获取锁成功后经过leaseTime则锁自动释放
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    // 1、要是获取锁时指定了leaseTime,则直接执行tryLockInnerAsync方法尝试获取锁,且锁过期时间为leaseTime
    if (leaseTime != -1) {
        // 直接获取锁
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 2、要是获取锁没有指定leaseTime,则锁的默认过期时间为30秒。为了避免出现业务没执行完但是锁被释放的情况,因此当没有指定leaseTime时,redisson会开启一个watchdog功能,其主要功能时每隔十秒去对该锁进行续时,每次默认续时30秒,续时时间也可以通过改变Config.lockWatchdogTimeout来更改
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }

            Long ttlRemaining = future.getNow();
            // 获取锁成功后开启watchdog功能
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

// 真正获取锁的逻辑在tryLockInnerAsync中,其内部执行lua脚本,内部分为三段逻辑
// 1、如果锁不存在,则通过hset设置它的值,并设置过期时间
// 2、如果锁已存在,并且获取锁的客户端是当前客户端,则通过hincrby给数值递增1,表示当前所重入次数+1
// 3、如果锁存在,且获取锁的客户端不是当前客户端,则直接返回该锁剩余的过期时间ttl
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
             // 1、如果锁不存在,则通过hset设置它的值,并设置过期时间
             "if (redis.call('exists', KEYS[1]) == 0) then " +
    		 "redis.call('hset', KEYS[1], ARGV[2], 1); " +
    		 "redis.call('pexpire', KEYS[1], ARGV[1]); " +
    		 "return nil; " +
    		 "end; " +
             // 2、如果锁已存在,并且获取锁的客户端是当前客户端,则通过hincrby给数值递增1,表示当前所重入次数+1
    		 "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
    		 "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
    		 "redis.call('pexpire', KEYS[1], ARGV[1]); " +
     		"return nil; " +
    		 "end; " +
             // 3、如果锁存在,且获取锁的客户端不是当前客户端,则直接返回该锁剩余的过期时间ttl
             "return redis.call('pttl', KEYS[1]);",
            Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

// 下面来看一下watchdog功能这个watchdog功能的实现逻辑在scheduleExpirationRenewal()里,内部首先执行hexists命令查询当前客户端分布式锁是否存在,如果存在则使用pexpire指令对该key进行续时,这两个操作由lua脚本执行保证原子性,在方法renewExpirationAsync(threadId)中,执行成功后再重新调用自身方法scheduleExpirationRenewal(),实现不断续时的功能。
private void scheduleExpirationRenewal(final long threadId) {
    if (expirationRenewalMap.containsKey(getEntryName())) {
        return;
    }

    // 启动延时任务
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {

            // 执行lua脚本进行续时操作
            RFuture<Boolean> future = renewExpirationAsync(threadId);

            future.addListener(new FutureListener<Boolean>() {
                @Override
                public void operationComplete(Future<Boolean> future) throws Exception {
                    expirationRenewalMap.remove(getEntryName());
                    if (!future.isSuccess()) {
                        log.error("Can't update lock " + getName() + " expiration", future.cause());
                        return;
                    }

                    // 执行续时成功,重复执行
                    if (future.getNow()) {
                        // reschedule itself
                        scheduleExpirationRenewal(threadId);
                    }
                }
            });
        }

    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
        task.cancel();
    }
}

// renewExpirationAsync(threadId)方法,使用lua脚本先判断该客户端分布式锁是否存在,存在则续时
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.<Object>singletonList(getName()),
            internalLockLeaseTime, getLockName(threadId));
}

等待获取锁实现

首先在谈这个等待获取锁的实现之前,首先说一下Java的Semaphore(信号量)

Semaphore(信号量)
介绍:Semaphore 通常我们叫它信号量,它可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
描述:我们可以把它类比成停车场入口的车位剩余量大屏,当大屏显示车位剩余量为0时则入口栏杆不会抬起,当停车场内的车出来(对应release操作)后则余量大屏显示+1,此前堵在入口的第一辆车能进去(入口汽车的等待及进入操作对应acquire操作)。
基本使用:我们可以通过初始化Semaphore semaphore = new Semaphore(max);来指定当前资源能被max个线程操作,当执行semaphore.acquire()操作时则当前可获取资源线程数-1,当执行semaphore.release()时则当前可获取资源线程数+1,当当前可获取资源数为0时则不可获取资源。

在当前介绍的可重入锁中等待获取锁的实现就是使用这个Java的Semaphore来完成的,首先redisson初始化了一个Semaphore对象,当前可获取资源的数量为0(Semaphore semaphore = new Semaphore(0))。若redis中存在该分布式锁时,当前线程订阅了该分布式锁释放的通知,然后调用semaphore获取资源的方法(tryAcquire\acquire),由于Semaphore初始化时可获取资源数为0,因此这会使自身程序陷入自旋,在自旋过程中假如分布式锁释放了,线程收到通知调用semaphore.release()方法,此时当前可获取资源变为1,则自旋解除,重新获取分布式锁。接下来看看代码

// 尝试获取锁方法
while (true) {
    ttl = tryAcquire(leaseTime, unit, threadId);
    // 获取锁成功
    if (ttl == null) {
        break;
    }

    // 使用Semaphore
    if (ttl >= 0) {
        // 调用semaphore.tryAcquire在ttl时间内重复检查当前可获取资源数是否>0,若>0则获取资源返回
        getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
    } else {
        // 调用semaphore.acquire检查当前可获取资源数是否>0,若>0则获取资源,反之则获取失败,两种情况都直接返回
        getEntry(threadId).getLatch().acquire();
    }
}

// 订阅方法:RFuture<RedissonLockEntry> future = this.subscribe(threadId);
// 该方法一步一步进入最终会调用PublishSubscribe类的subscribe方法,其内部有两个最核心的方法
// 1、createEntry(newPromise);
// 该方法内部初始化一个RedissonLockEntry对象,其构造函数初始化Semaphore对象,Semaphore semaphore = new Semaphore(0)
protected RedissonLockEntry createEntry(RPromise<RedissonLockEntry> newPromise) {
    return new RedissonLockEntry(newPromise);
}

public RedissonLockEntry(RPromise<RedissonLockEntry> promise) {
    super();
    this.latch = new Semaphore(0);
    this.promise = promise;
}
// 2、createListener(channelName, value);
// 绑定监听,当分布式锁释放时会调用onMessage方法,其内部调用了semaphore.release()解除自旋状态
protected void onMessage(RedissonLockEntry value, Long message) {
    if (message.equals(unlockMessage)) {
        Runnable runnableToExecute = value.getListeners().poll();
        if (runnableToExecute != null) {
            runnableToExecute.run();
        }

        value.getLatch().release();
    } else if (message.equals(readUnlockMessage)) { //读取到分布式锁释放通知
        while (true) {
            Runnable runnableToExecute = value.getListeners().poll();
            if (runnableToExecute == null) {
                break;
            }
            runnableToExecute.run();
        }

        // 调用semaphore.release()通知线程可获取资源
        value.getLatch().release(value.getLatch().getQueueLength());
    }
}

释放锁

lock.unlock()最后调用了RedissonLock.unlockAsync()方法,下面来看看这个方法

public RFuture<Void> unlockAsync(final long threadId) {
    final RPromise<Void> result = new RedissonPromise<Void>();
    // 内部执行解锁操作的脚本
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.addListener(new FutureListener<Boolean>() {
        @Override
        public void operationComplete(Future<Boolean> future) throws Exception {
            if (!future.isSuccess()) {
                cancelExpirationRenewal(threadId);
                result.tryFailure(future.cause());
                return;
            }

            // 获取解锁操作返回值
            Boolean opStatus = future.getNow();
            // 当为空时则表示当前解锁线程与持有锁线程不一致,直接抛出异常
            if (opStatus == null) {
                IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                        + id + " thread-id: " + threadId);
                result.tryFailure(cause);
                return;
            }
            // 解锁成功关闭watchdog定时任务
            if (opStatus) {
                cancelExpirationRenewal(null);
            }
            result.trySuccess(null);
        }
    });

    return result;
}

然后再来看一下unlockInnerAsync方法

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 若当前分布式锁已不存在则发布锁释放通知
            "if (redis.call('exists', KEYS[1]) == 0) then " +
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; " +
            "end;" +
            // 若当前分布式锁持有线程不是当前线程则返回null
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
            "return nil;" +
            "end; " +
            // 使用hincrby使锁重入次数自减1,释放一次锁
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            // 若当前重入次数>0则刷新过期事件,否则删除key并发布释放通知
            "if (counter > 0) then " +
            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "return 0; " +
            "else " +
            "redis.call('del', KEYS[1]); " +
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}

3. 关于Redisson分布式锁的争论

上述介绍的基于redisson实现的可重入锁其实对于单机部署的redis来说是适用的。但是在实际生产当中如果仅仅是单机部署redis的话,那么发生单点故障时则整个分布式系统将会存在重大的隐患。

这时候有人就会说使用master-slave架构来进行redis部署,但是这会衍生出另一个问题,由于master-slave数据同步是异步进行的,当A客户端在master节点中成功获取了分布式锁时,此时master节点宕机了且锁信息不能及时同步到salve节点中,此时slave节点晋升为master节点,由于数据同步不及时,该分布式锁可被客户端B重新创建。当然如果我们能够容忍该分布式锁同一时间被两个客户端所持有则可忽略该问题,对于该问题redis作者Antirez在官网给出了一种解决方案,这是基于Redlock(红锁算法)来实现的,下面来介绍一下。

红锁算法

1、原理描述

  • 集群中redis采用多节点部署,节点之间互不相关(一般使用部署5个节点)
  • 每次获取分布式锁时对所有节点进行setnx操作,超过半数节点setnx成功则表示获取分布式锁成功

2、基本使用

public void testRedLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){
    RLock lock1 = redisson1.getLock("lock1");
    RLock lock2 = redisson2.getLock("lock2");
    RLock lock3 = redisson3.getLock("lock3");
    RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
    try {
        // 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
        lock.lock();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        // 释放锁
        lock.unlock();
    }
}

3、核心问题剖析

  1. 重启问题

    描述:假设当前存在节点A\B\C\D\E,某客户端在A\B\C上加锁成功,在D\E上加锁失败,由于成功数量过半则这里会认为获取锁成功。此时如果节点C宕机后重启了,则此时C上关于此分布式锁的的信息由于没有进行数据持久化将会丢失,此时另一个客户端能通过在C\D\E上加锁进行分布式锁的成功获取。

    解决方案:

    1. 数据持久化

      在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync)

      • 因此最坏情况下可能丢失1秒的数据
      • 为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能
      • 即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)
      • 所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的
    2. 延迟重启

      当一个节点崩溃后,先不要马上对这个节点进行重启,而是等到该节点上的锁有效时间全部过期后再重启,此时恢复后以前的锁都失效了

  2. 锁释放问题

    描述:当前存在节点A\B\C\D\E,客户端向A\B\C\D\E都发送了setnx指令且执行成功,但是在节点向客户端返回执行成功ack时,由于网络波动,E节点上的执行成功ack返回失败,此时客户端认位E节点加锁失败,但是由于成功数目过半,成功获取了分布式锁。如果在释放锁时没有对E节点进行对应的key删除的话,那么E节点将在一定时间内长期持有该锁,影响其他客户端的加锁操作。

    解决方案:

    在释放锁时,不管节点前面是否setnx成功,都执行一次key删除操作。

4、番外

​ 最后在关于redlock安全性上,分布式系统的专家马丁·克莱普曼博士(Martin Kleppmann)在一篇文章How to do distributed locking中提出了许多质疑,其中包括了系统gc阻塞对redlock数据安全性的影响,以及服务器时间跃迁对redlock的安全性影响等,而redis作者Antirez也对这些质疑进行了一一反驳。

有兴趣可参考以下文章

官方redlock的讲解

争论过程

4.总结

在某些极端的情况下,redis不能保证数据的一致性,且redis比较消耗资源,但是由于其性能强劲,因此基于redis实现的分布式锁也被广泛用于实际生产中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于 Redis 实现分布式锁可以利用 Redis 的原子性操作和超时特性来实现。下面是一个基本的实现思路: 1. 获取锁:使用 Redis 的 SETNX 命令,如果指定的锁 key 不存在,则设置该 key 的值为当前时间戳加上锁的超时时间,并返回成功;否则,返回失败。 2. 释放锁:使用 Redis 的 EVAL 命令,通过 Lua 脚本来实现原子性的删除锁。脚本的内容是先判断锁是否存在且超时,如果是则删除锁并返回成功;否则,返回失败。 下面是一个简单的 Python 代码示例: ```python import redis import time class RedisLock: def __init__(self, redis_client, lock_key, expire_time): self.redis = redis_client self.lock_key = lock_key self.expire_time = expire_time def acquire(self): while True: timestamp = int(time.time() * 1000) + self.expire_time acquired = self.redis.set(self.lock_key, timestamp, nx=True, px=self.expire_time) if acquired: return True time.sleep(0.001) def release(self): lua_script = """ if redis.call("exists", KEYS[1]) == 1 then local current_value = tonumber(redis.call("get", KEYS[1])) if current_value and current_value <= tonumber(ARGV[1]) then return redis.call("del", KEYS[1]) end end return 0 """ self.redis.eval(lua_script, 1, self.lock_key, int(time.time() * 1000) + self.expire_time) # 使用示例 redis_client = redis.Redis(host='localhost', port=6379, db=0) lock = RedisLock(redis_client, 'my_lock', 1000) # 锁的超时时间为 1000 毫秒 if lock.acquire(): try: # 执行需要加锁的代码 pass finally: lock.release() ``` 需要注意的是,以上代码仅是一个简单的实现示例,实际使用中还需要考虑异常处理、锁的可重入性、锁的可拥有时间等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值