五、Redis底层原理 - 分布式锁

一、简单的分布式锁

实现方式:setnx 命令,依据返回值为 0 或 1 判断是否抢到了资源。

# redis全局添加一个key为lock的元素,该关键字不可再被使用
setnx lock 1

# 释放锁,删除对该关键字的占用
del lock

在redis中对某个资源加锁,可以控制其他系统访问这个资源的权限,于是实现了分布式锁。

二、死锁问题

如果客户端的应用程序发生异常或者直接挂掉,不能及时释放锁会产生死锁问题。聪明的我当然想到给锁设置过期时间。

# 占用锁
setnx lock 1

# 设置过期时间 10s
expire lock 10 

但是,这里发现是需要两条命令才能实现锁过期时间,不具备原子性,还是有可能发生第一条命令执行成功,第二条命令执行失败的情况。

可以使用一条命令实现加锁设置过期时间吗?当然可以。

set lock 1 ex 10 nx

redis 2.6 之后的版本支持使用单条命令实现加锁设置过期时间。

三、锁被其他客户端释放了怎么办?

这里有一个简单的权限校验方法,uuid。

以下是参杂着java代码的实现逻辑,核心思想是这样;

// 生成一个uuid
String uuid = UUID.randomUUID().toString();

// redis中保存lock的value为这个uuid
set lock uuid;

// 判断保存的uuid是否和当前客户端拥有的uuid相同
if(lock.value == uuid) {
    del lock;
}

但是缺点也很明显,不但参杂着java代码的逻辑,而且校验和删除在redis中是两步操作,不保证原子性;

这时我们想到可以使用Lua脚本实现权限校验和删除锁的操作:

if (redis.call("GET", KEYS[1]) == ARGV[1])
then 
    return redis.call("DEL", KEYS[1])
else 
    return 0
end

四、在Java代码中实现锁

1、单机锁

public class AloneLock implements Lock {

    // 维护一个原子对象,内部是Thread
    AtomicReference<Thread> owner = new AtomicReference<>();

    // 维护一个阻塞队列
    LinkedBlockingQueue<Thread> waiter = new LinkedBlockingQueue<>();

    /**
     * 加锁
     */
    @Override
    public void lock() {
        // 判断当前线程是否能进入原子对象
        while (!owner.compareAndSet(null, Thread.currentThread())) {
            // 如果不能进入
            // 就进入阻塞队列
            waiter.add(Thread.currentThread());
            // 并且阻塞当前线程
            LockSupport.park();
            // 线程唤醒后移出阻塞队列,重新争抢资源
            waiter.remove(Thread.currentThread());
        }
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        // 将原子对象内部的线程替换成null -> 原子对象内部的线程就是当前线程
        if (owner.compareAndSet(Thread.currentThread(), null)) {
            // 如果释放成功
            // 唤醒阻塞队列中所有的线程
            for (Thread thread : waiter) {
                LockSupport.unpark(thread);
            }
        }
    }
}

可以看到,在单应用的情况下加锁只需要在Java程序中控制资源的占有和释放即可。

2、分布式锁

分布式锁用到了redis中的共享资源,多个应用客户端共同争抢redis中的同一个资源,我们先配置Jedis,再操作redis中的资源。

配置文件:

# redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=5000

# jedisPool 
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-wait=3000
spring.redis.JmxEnabled=true
spring.redis.blockWhenExhausted=true

RedisConfig:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Jedis 配置
 *
 * @Date: 2024/07/22/14:37
 */
@Configuration
@PropertySource("classpath:application.properties")
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;

    @Value("${spring.redis.jedis.pool.max-wait}")
    private int maxWaitMillis;

    @Value("${spring.redis.blockWhenExhausted}")
    private Boolean blockWhenExhausted;

    @Value("${spring.redis.JmxEnabled}")
    private Boolean JmxEnabled;

    @Bean
    public JedisPool jedisPool() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        // 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
        jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
        // 是否启用pool的jmx管理功能, 默认true
        jedisPoolConfig.setJmxEnabled(JmxEnabled);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);
        return jedisPool;
    }
}

 实现简单的分布式锁:

@Component
@Data
public class RedisDistLock implements Lock {

    // 设置锁过期时间
    private static final Long LOCK_TIME = 5 * 1000L; // 5s
    // 设置redis锁名称前缀
    private String lockName = "lock";
    // 设置解锁用的Lua脚本
    private static final String RELEASE_LOCK_LUA =
            "if redis.call('get', KEYS[1]) == ARGV[1]\n" +
            "then\n" +
            "   return redis.call('del', KEYS[1])\n" +
            "else\n" +
            "   return 0\n" +
            "end";
    // 保存每个线程的uuid值
    private ThreadLocal<String> lockerId = new ThreadLocal<>();
    // 保存锁对象,处理锁重入
    private Thread ownerThread;

    @Resource
    private JedisPool jedisPool;

    /**
     * 加锁
     */
    @Override
    public void lock() {
        while (!tryLock()) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 尝试获取锁
     */
    @Override
    public boolean tryLock() {
        // 获取当前线程对象
        Thread thread = Thread.currentThread();
        // 前置校验
        if (ObjectUtils.nullSafeEquals(ownerThread, thread)) {
            // 如果当前线程持有锁 - 处理锁重入
            return true;
        } else if (!ObjectUtils.nullSafeEquals(ownerThread, null)){
            // 如果其他线程持有锁 - 抢锁失败
            return false;
        }
        // 获取jedis
        Jedis jedis = jedisPool.getResource();
        try {
            // 获取当前线程唯一校验密码
            String lockValue = UUID.randomUUID().toString();
            // 组装redis命令参数
            SetParams redisParams = new SetParams();
            redisParams.px(LOCK_TIME);  // 设置过期时间
            redisParams.nx();           // 设置redis - key为不可修改
            // 同步代码块实现抢锁
            synchronized (this) {
                // 两层 if 增强可读性
                // 进一步校验锁持有状态
                if (ObjectUtils.nullSafeEquals(ownerThread, null)) {
                    // 给redis加锁
                    if ("OK".equals(jedis.set(lockName, lockValue, redisParams))) {
                        // 如果在redis中加锁成功
                        lockerId.set(lockValue);    // 记录当前线程的密码
                        setOwnerThread(thread);     // 记录当前线程对象,给本地加锁
                        return true;
                    }
                    return false;
                }
            }
        } catch (RuntimeException e) {
            throw new RuntimeException("分布式锁尝试加锁失败!");
        } finally {
            jedis.close();
        }
        return false;
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        // 校验当前线程是否有权限解锁
        Thread thread = Thread.currentThread();
        if (!ObjectUtils.nullSafeEquals(ownerThread, thread)) {
            throw new RuntimeException("当前线程无权限解锁");
        }
        // 获取jedis
        Jedis jedis = jedisPool.getResource();
        try {
            // 允许Lua脚本实现解锁 - 在redis中校验密码
            Long result = (Long) jedis.eval(RELEASE_LOCK_LUA, Arrays.asList(lockName), Arrays.asList(lockerId.get()));
            // 校验是否解锁成功
            if (0L != result) {
                // 如果成功了
                System.out.println("redis锁已释放!");
            } else {
                System.out.println("redis锁释放失败!");
            }
        } catch (RuntimeException e) {
            throw new RuntimeException("锁释放失败");
        } finally {
            if (!ObjectUtils.isEmpty(jedis)) jedis.close();
            lockerId.remove();      // 不记录当前线程密码
            setOwnerThread(null);   // 不记录当前线程对象
            System.out.println("本地锁释放成功!");
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

在分布式环境下,由于Java程序只能在自己的范围内控制资源加锁,不能控制redis这种共享数据源的资源。所以加锁操作使用了一个Jedis提供的类SetParams实现资源的加锁校验和加锁,而解锁操作则使用到了Lua脚本实现锁资源的校验和解锁。

加锁/解锁 = 校验 + 操作。

五、锁过期时间如何评估?

1、如果业务处理时间超过锁过期时间怎么办?

这里有两个办法:

① 设置一段很长的过期时间。

② 锁快要过期时还没有完成业务逻辑,给锁延期。

明显是第二个办法更好,我们叫这种办法为看门狗续期。

2、看门狗模型 - 使用守护线程实现可延期分布式锁

如图,获取锁的线程可以开启一个守护线程替主线程定时监测锁的状态,假如主线程一直没有主动放弃锁,那么守护线程则帮助主线程给锁续期,直到主线程主动放弃锁。

如果主线程出现异常停掉,那么守护线程也会随之消亡,不会继续给锁续期,锁达到过期时间后自动释放,不会出现死锁问题。

下面来看手写代码实现:

/**
 * 包装一个延迟队列的实体类
 * 维护一个到期时刻字段,比标准的delay的实现要提前一点时间
 * 存放一个data,保存redis中锁的key-value
 * 
 * @Date: 2024/07/02/9:06
 */
public class ItemVO<T> implements Delayed {

    private long activeTime; // 到期时刻

    private T data; // 具体业务数据

    /**
     * 构造方法
     * @param expirationTime 过期时长 -> 需要转换为到期时间后,再稍微提前一点
     * @param data           业务数据
     */
    public ItemVO(long expirationTime, T data) {
        super();
        this.activeTime = expirationTime + System.currentTimeMillis() - 100;
        this.data = data;
    }

    public long getActiveTime() {
        return activeTime;
    }

    public T getData() {
        return data;
    }

    /**
     * 返回当前元素到激活时刻的剩余存活时长
     * @param unit the time unit
     * @return 剩余存活时长
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.activeTime - System.currentTimeMillis(), unit);
    }

    /**
     * 按剩余存活时长排序
     * @param o the object to be compared.
     * @return 0-相等,-1-当前元素存活时间短,1-入参元素存活时间短
     */
    @Override
    public int compareTo(Delayed o) {
        long d = (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        if (d == 0) {
            return 0;
        } else {
            if (d < 0) {
                return -1;
            } else {
                return 1;
            }
        }
    }
}
/**
 * 延迟队列实际元素
 * Redis的key-value结构
 *
 * @Date: 2024/07/02/9:05
 */
public class LockItem {
    private final String key;
    private final String value;

    public LockItem(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public String getKey() {
        return key;
    }

    public String getValue() {
        return value;
    }
}
/**
 * redis 续期分布式锁
 *
 * @Date: 2024/06/27/8:42
 */
@Component
public class RedisDistLockWithDog implements Lock {

    // 记录本系统当前获取锁的线程
    private Thread ownerThread;
    // 设置锁自动释放时长
    private static final int LOCK_TIME = 1 * 1000;
    // 设置锁名称
    private static final String LOCK_NAME = "lock";
    // 保存每个线程的uuid - 密码
    private ThreadLocal<String> lockerId = new ThreadLocal<>();
    // 共享的看门狗线程
    private Thread dogThread;
    // 创建一个延迟队列 避免无用的轮询,减少看门狗轮询次数
    private static DelayQueue<ItemVO<LockItem>> delayQueue =  new DelayQueue<>();
    // redis续锁Lua脚本
    private static final String DELAY_LOCK_LUA =
            "if redis.call('get', KEYS[1]) == ARGV[1]\n" +
            "    then\n" +
            "        return redis.call('pexpire', KEYS[1],ARGV[2])\n" +
            "    else \n" +
            "        return 0 \n" +
            "    end";
    private static final String RELEASE_LOCK_LUA =
            "if redis.call('get', KEYS[1]) == ARGV[1] \n" +
            "    then\n" +
            "        return redis.call('del', KEYS[1])\n" +
            "    else\n" +
            "        return 0\n" +
            "    end";

    // 注入JedisPool
    @Resource
    private JedisPool jedisPool = new JedisPool();

    public void setOwnerThread(Thread ownerThread) {
        this.ownerThread = ownerThread;
    }

    @Override
    public void lock() {
        while (!tryLock()) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public boolean tryLock() {
        Thread thread = Thread.currentThread();
        // 校验本线程是否已持有锁
        if (thread == ownerThread) {
            return true;
        } else if (null != ownerThread) {
            // 如果是别的线程持有锁
            return false;
        }
        // 如果没有任何线程持有锁 - 开始加锁
        Jedis jedis = null;
        try {
            // 获取jedis实例
            jedis = jedisPool.getResource();
            // 获取一个唯一校验密码
            String uuid = UUID.randomUUID().toString();
            // 设置redis加锁参数
            SetParams params = new SetParams();
            params.nx();
            params.px(LOCK_TIME);
            // 同步代码块实现抢锁
            synchronized (this) {
                // 再次判断锁是否被占用 & 在redis中加上分布式锁
                if (null == ownerThread && "OK".equals(jedis.set(LOCK_NAME, uuid, params))) {
                    // 如果加锁成功
                    // 记录当前线程的uuid
                    lockerId.set(uuid);
                    // 记录获取锁的线程实例
                    setOwnerThread(thread);
                    // 启动看门狗线程
                    if (null == dogThread) {
                        dogThread = new Thread(null, new DogTask(), "dogTask");
                        dogThread.setDaemon(true);  // 设置为守护线程
                        dogThread.start();          // 启动线程,执行其run方法
                    }
                    // 向延迟阻塞队列中加入元素,让看门狗在锁过期时间之前一点的时间去做锁的续期
                    delayQueue.add(new ItemVO<>(Integer.valueOf(LOCK_TIME), new LockItem(LOCK_NAME, uuid)));
                    System.out.println("已获取锁---");
                    return true;
                } else {
                    System.out.println("无法获取锁---");
                    return false;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("分布式锁加锁失败", e);
        } finally {
            if (null != jedis) jedis.close();
        }
    }

    // 看门狗线程实体类
    private class DogTask implements Runnable {

        @Override
        public void run() {
            System.out.println("看门狗线程已启动...");
            // 创建一个循环,只要守护线程存活就一直执行
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Jedis jedis = null;
                    // 只有元素快到期了才能take到,否则一直阻塞,直到take到元素
                    LockItem data = delayQueue.take().getData();
                    try {
                        jedis = jedisPool.getResource();
                        Long result = (Long) jedis.eval(DELAY_LOCK_LUA,
                                Arrays.asList(data.getKey()),
                                Arrays.asList(data.getValue(), String.valueOf(LOCK_TIME)));
                        if (result == 0L) {
                            System.out.println("Redis上的锁已释放,无需续期!");
                        } else {
                            delayQueue.add(new ItemVO<>(Integer.valueOf(LOCK_TIME), new LockItem(data.getKey(), data.getValue())));
                            System.out.println("Redis上的锁已续期:" + LOCK_TIME);
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("锁续期失败!", e);
                    } finally {
                        if (jedis != null) jedis.close();
                    }
                } catch (Exception e) {
                    System.out.println("看门狗线程被中断!");
                    break;
                }
            }
            System.out.println("看门狗线程准备关闭...");
        }
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        if (ownerThread != Thread.currentThread()) {
            throw new RuntimeException("试图释放无所有权的锁!");
        }
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            Long result = (Long) jedis.eval(RELEASE_LOCK_LUA, Arrays.asList(LOCK_NAME), Arrays.asList(lockerId.get()));
            if (result != 0L) {
                System.out.println("redis上的锁已释放");
            } else {
                System.out.println("redis上的锁释放失败");
            }
        } catch (Exception e) {
            throw new RuntimeException("锁释放失败!");
        } finally {
            if (null != jedis) jedis.close();
            lockerId.remove();
            setOwnerThread(null);
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        throw new UnsupportedOperationException("不支持可中断获取锁!");
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        throw new UnsupportedOperationException("不支持等待尝试获取锁!");
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

 使用看门狗实现锁的续期完美解决了其他线程释放本线程的锁、死锁、锁过期时间不好设置的问题,但缺点是实现过于复杂,写代码不友好;

六、使用Redisson实现分布式锁

前面提到的手写分布式锁代码就是Redisson看门狗的大概执行流程,但是手写代码太复杂,不适合在生产环境使用,下面我们用Redisson感受一下分布式锁的使用:

首先需要配置RedissonConfig,设置一些重要的参数,注意看注释:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RedisClient 配置
 *
 * @Date: 2024/07/05/7:54
 */
@Configuration
public class RedissonConfig {
    /**
     * 所有业务代码都使用RedissonClient
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        // 创建配置 - Redisson.Config
        Config config = new Config();
        // 连接到本地redis数据库
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 设置主线程的锁过期时间,单位(毫秒)
        // Redisson默认的超时时间是30s
        // 如果主线程还在执行,那么看门狗线程就会在 1/3 * outTime 的时刻刷新锁过期时间,所以默认每10s刷新一次
        config.setLockWatchdogTimeout(10000L); // 这里的过期时间是10s,每3.3s刷新一次锁过期时间
        // 返回redissonClient
        return Redisson.create(config);
    }
}

配置好RedissonConfig后,我们来写测试代码:

import lock.config.RedisConfig;
import lock.config.RedissonConfig;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.Resource;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 测试redissonLock
 *
 * @Date: 2024/07/05/8:24
 */
@SpringBootTest(classes = {RedissonConfig.class, RedisConfig.class})
public class TestRedissonLock {

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private JedisPool jedisPool;

    private static final String LOCK_NAME = "lock";

    // 共享资源
    private int count = 0;

    @Test
    public void testRedissonLock() throws InterruptedException {
        // 线程池线程数量
        int clientCount = 3;
        // .getLock() 获取一个锁,key在redis里面叫"lock"
        RLock redissonLock = redissonClient.getLock(LOCK_NAME);
        // 创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(clientCount);
        // 使用CountDownLatch让线程同时运行,模拟并发
        CountDownLatch countDownLatch = new CountDownLatch(clientCount);
        // 获取jedis
        Jedis jedis = jedisPool.getResource();
        // 执行每个线程的逻辑
        for (int i = 0; i < clientCount; i++) {
            executorService.execute(() -> {
                try {
                    // 调用.lock()方法获取分布式锁
                    // 不传任何参数,启用看门狗线程,自动过期时间就是在RedissonConfig配置的10s
                    redissonLock.lock();
                    System.out.println("======================================================");
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                    for (int j = 0; j < 10; j++) {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + ":" + j);
                        System.out.println("分布式锁剩余存活时间:" + jedis.ttl(LOCK_NAME));
                        System.out.println("==================");
                    }
                    // 共享资源++
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "执行结束");
                    // 判断锁是否属于当前线程,避免释放掉其他线程的锁
                    // 如果不手动释放锁,且当前线程一直存活,那么就会死锁
                    if (redissonLock != null && redissonLock.isHeldByCurrentThread()) {
                        System.out.println(redissonLock);
                        redissonLock.unlock();
                    }
                    // 关闭redis连接
                    if (jedis != null) jedis.close();
                    System.out.println("======================================================");
                }
                // 开始执行
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println(count);
    }
}

可以看到相比手写看门狗分布式锁,使用Redisson可以简化很多代码,只需要注意触发看门狗的条件即可;

执行结果部分截图:

结论:

1、在RedissonConfig中配置锁过期时间;

2、使用.lock()方法触发看门狗线程;

3、释放锁需要判断锁是否属于本线程,否则会抛异常;

4、每隔 1/3 * outTime 的时间后刷新过期时间;

5、如果不手动释放锁,线程执行结束后,那么看门狗也随之消亡,redis中的锁会在到期后自动释放,不会死锁;

七、Redis集群下的分布式锁

以上Redis分布式锁的知识还只是基于单机Redis实现的,那么如果在Redis集群情况下,又该如何实现呢?

如果在主从结构中,我们可以把锁加在主节点上,如果主节点挂掉,会自动选出一个从节点转变成主节点,锁对应的信息会自动同步到新的主节点,从而实现故障转移,降低锁丢失的风险。但是这样就万事大吉了吗?显然不是。

因为在哨兵集群中,主从的切换是使用一个异步线程实现数据的转移,具有一定的延迟,而且集群节点之间的通信依赖网络,如果网络不好也会造成延迟,任然具有一定的锁丢失风险。那我们想要更加完善一点该怎么办呢?

——红锁。

1、红锁 - RedLock

RedLock实现的整体流程:

客户端先获取【当前时间戳T1】

客户端依次向这5台Redis实例(非集群)发起加锁请求

如果>=3个成功(大部分)成功,且当前时间戳 T2-T1 < 锁的过期时间则加锁成功

加锁成功,操作共享资源

加锁失败/释放锁,向所有Redis节点发起释放锁的请求

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance1.getLock("lock2");
RLock lock3 = redissonInstance1.getLock("lock3");
RLock lock4 = redissonInstance1.getLock("lock4");
RLock lock5 = redissonInstance1.getLock("lock5");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);

// 同时加5个锁
// 大部分成功就算加锁成功
lock.lock();
... 业务代码 ...
lock.unlock();

2、红锁的一些问题

为什么使用最少5台redis节点?

提高容错,5台redis同时宕机的概率小。

为什么要超过一半的节点加锁成功才算加上锁了?

在分布式系统中,可能会出现异常节点,比如没宕机但是响应慢,这时只要满足超过一半节点加锁成功就不必等待慢响应。

为什么要计算加锁时间?

因为可能出现网络延迟,比如设置的加锁时间是10s,但是接收到加锁成功的信号时已经到了第11s,在这台redis中的锁已经过期了,所以只能算作加锁失败。

RedLock的NPC问题:

N:NetWork Delay   网络延迟

P:Process Pause   进程暂停

C:Clock Drift           时钟漂移

N和P的问题可以通过计算时间来解决,但是C的时钟漂移问题没法解决,在一个分布式集群中,没办法保证所有服务器的系统时间一致,这就没法判断在某个redis节点加上的锁是否已经过期。

RedLock的性能不高:由于加锁访问服务器很多,且加锁判断步骤太长,解锁也需要向所有redis节点发送解锁请求,所以RedLock的性能不高。

3、使用redis分布式锁的建议

 在大部分的业务场景下,使用看门狗+主从+哨兵基本可以满足分布式锁的功能,即使出现问题也是个小概率事件,可以容忍少量的锁丢失问题。

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis分布式锁是一种常用的分布式系统中实现互斥访问的机制。其底层原理可以通过以下几个步骤来介绍: 1. 获取锁:客户端通过执行SET命令尝试在Redis中设置一个特定的键值对作为锁。如果该键不存在,则表示获取到了锁,可以执行后续操作。如果该键已经存在,则表示锁已经被其他客户端持有,需要等待或者进行重试。 2. 设置过期时间:为了避免锁被持有后无法释放的情况,需要为锁设置一个过期时间。客户端在获取到锁之后,通过执行EXPIRE命令为锁设置一个合适的过期时间。这样即使持有锁的客户端发生故障或者意外退出,锁也会在一定时间后自动释放。 3. 释放锁:当客户端完成了对共享资源的操作后,需要释放锁。客户端通过执行DEL命令来删除锁对应的键值对,从而释放锁供其他客户端使用。 需要注意的是,Redis分布式锁的实现需要考虑以下几个问题: 1. 锁竞争:多个客户端同时尝试获取锁时可能会发生竞争。为了避免多个客户端同时获取到锁,可以使用SETNX命令来保证只有一个客户端能够成功获取到锁。 2. 锁误释放:如果客户端在获取到锁之后发生故障或者意外退出,锁可能无法正常释放。为了解决这个问题,可以为锁设置一个合适的过期时间,确保即使持有锁的客户端发生故障,锁也会在一定时间后自动释放。 3. 锁重入:某些场景下,同一个客户端可能需要多次获取同一个锁。为了支持锁的重入,可以为每个客户端维护一个计数器,记录该客户端获取锁的次数,并在释放锁时递减计数器。只有当计数器为0时,才真正释放锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值