分布式锁实现方案-基于Redis实现的分布式锁

目录

一、基于Lua+看门狗实现

[1.1 缓存实体](#1.1 缓存实体)

[1.2 延迟队列存储实体](#1.2 延迟队列存储实体)

[1.3 分布式锁RedisDistributedLockWithDog](#1.3 分布式锁RedisDistributedLockWithDog)

[1.4 看门狗线程续期](#1.4 看门狗线程续期)

[1.5 测试类](#1.5 测试类)

[1.6 测试结果](#1.6 测试结果)

[1.7 总结](#1.7 总结)

二、RedLock分布式锁

[2.1 Redlock分布式锁简介](#2.1 Redlock分布式锁简介)

[2.2 RedLock测试例子](#2.2 RedLock测试例子)

[2.3 RedLock 加锁核心源码分析](#2.3 RedLock 加锁核心源码分析)

[2.4 RedLock的问题与解决方案](#2.4 RedLock的问题与解决方案)

[2.4.1 问题](#2.4.1 问题)

[2.4.1.1 时钟偏移](#2.4.1.1 时钟偏移)

[2.4.1.2 单点故障](#2.4.1.2 单点故障)

[2.4.1.3 性能瓶颈](#2.4.1.3 性能瓶颈)

[2.4.2 解决方案](#2.4.2 解决方案)

[2.4.2.1 引入重试机制](#2.4.2.1 引入重试机制)

[2.4.2.2 时钟校准](#2.4.2.2 时钟校准)

[2.4.2.3 引入冗余节点](#2.4.2.3 引入冗余节点)

[2.5 总结](#2.5 总结)


一、基于Lua+看门狗实现

1.1 缓存实体

java 复制代码
package com.ningzhaosheng.distributelock.redis;

/**
 * @author ningzhaosheng
 * @date 2024/4/18 15:37:16
 * @description 类说明:Redis的key-value结构
 */
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;
    }
}

1.2 延迟队列存储实体

java 复制代码
package com.ningzhaosheng.distributelock.redis;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author ningzhaosheng
 * @date 2024/4/18 15:35:43
 * @description 说明:存放到延迟队列的元素,比标准的delay的实现要提前一点时间
 */
public class ItemVo<T> implements Delayed {
    /*到期时刻  20:00:35,234*/
    private long activeTime;
    /*业务数据,泛型*/
    private T data;

    /*传入的数值代表过期的时长,单位毫秒,需要乘1000转换为毫秒和到期时间
     * 同时提前100毫秒续期,具体的时间可以自己决定*/
    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;
    }

    /**
     * 返回元素到激活时刻的剩余时长
     */
    public long getDelay(TimeUnit unit) {
        long d = unit.convert(this.activeTime
                - System.currentTimeMillis(), unit);
        return d;
    }

    /**
     * 按剩余时长排序
     */
    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;
            }
        }
    }
}

1.3 分布式锁RedisDistributedLockWithDog

java 复制代码
package com.ningzhaosheng.distributelock.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author ningzhaosheng
 * @date 2024/4/18 15:38:07
 * @description Redis分布式锁,附带看门狗线程的实现:加锁,保持锁1秒
 */
public class RedisDistributedLockWithDog implements Lock {
    private final static int LOCK_TIME = 1 * 1000;
    private final static String RS_DISTLOCK_NS = "tdln2:";
    /*
     if redis.call('get',KEYS[1])==ARGV[1] then
        return redis.call('del', KEYS[1])
    else return 0 end
     */
    private final static String RELEASE_LOCK_LUA =
            "if redis.call('get',KEYS[1])==ARGV[1] then\n" +
                    "        return redis.call('del', KEYS[1])\n" +
                    "    else return 0 end";
    /*还有并发问题,考虑ThreadLocal*/
    private ThreadLocal<String> lockerId = new ThreadLocal<>();

    private Thread ownerThread;
    private String lockName = "lock";


    private Jedis jedis = null;

    // 看门狗线程
    private Thread expireThread;
    private WatchDogThead watchDogThead;

    public String getLockName() {
        return lockName;
    }

    public void setLockName(String lockName) {
        this.lockName = lockName;
    }

    public Thread getOwnerThread() {
        return ownerThread;
    }

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

    public RedisDistributedLockWithDog(Jedis jedis) {
        this.jedis = jedis;
        watchDogThead = new WatchDogThead(jedis);
        closeExpireThread();
    }

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

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

    @Override
    public boolean tryLock() {
        Thread t = Thread.currentThread();
        /*说明本线程正在持有锁*/
        if (ownerThread == t) {
            return true;
        } else if (ownerThread != null) {/*说明本进程中有别的线程正在持有分布式锁*/
            return false;
        }
        try {
            /*每一个锁的持有人都分配一个唯一的id,也可采用snowflake算法*/
            String id = UUID.randomUUID().toString();

            SetParams params = new SetParams();
            params.px(LOCK_TIME); //加锁时间1s
            params.nx();
            synchronized (this) {
                if ((ownerThread == null) &&
                        "OK".equals(jedis.set(RS_DISTLOCK_NS + lockName, id, params))) {
                    lockerId.set(id);
                    setOwnerThread(t);
                    if (expireThread == null) {
                        //看门狗线程启动
                        expireThread = new Thread(watchDogThead, "expireThread");
                        expireThread.setDaemon(true);
                        expireThread.start();
                    }
                    //往延迟阻塞队列中加入元素(让看门口可以在过期之前一点点的时间去做锁的续期)
                    watchDogThead.addDelayDog(new ItemVo<>((int) LOCK_TIME, new LockItem(lockName, id)));
                    System.out.println(Thread.currentThread().getName() + "已获得锁----");
                    return true;
                } else {
                    System.out.println(Thread.currentThread().getName() + "无法获得锁----");
                    return false;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("分布式锁尝试加锁失败!", e);
        }
    }

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

    @Override
    public void unlock() {
        Thread t = Thread.currentThread();
        if (ownerThread != t) {
            throw new RuntimeException("试图释放无所有权的锁!");
        }
        try {
            Long result = (Long) jedis.eval(RELEASE_LOCK_LUA,
                    Arrays.asList(RS_DISTLOCK_NS + lockName),
                    Arrays.asList(lockerId.get()));
            System.out.println(result);
            if (result.longValue() != 0L) {
                System.out.println(t.getName()+" Redis上的锁已释放!");
            } else {
                System.out.println(t.getName()+" Redis上的锁释放失败!");
            }
        } catch (Exception e) {
            throw new RuntimeException("释放锁失败!", e);
        } finally {
            lockerId.remove();
            setOwnerThread(null);
            // 关闭看门狗
            closeExpireThread();
            // 关闭jedis连接
            jedis.close();
        }
    }

    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException("不支持等待通知操作!");
    }

    /**
     * 中断看门狗线程
     */
    public void closeExpireThread() {
        if (null != expireThread) {
            expireThread.interrupt();
        }
    }
}

1.4 看门狗线程续期

java 复制代码
package com.ningzhaosheng.distributelock.redis;

import redis.clients.jedis.Jedis;

import java.util.Arrays;
import java.util.concurrent.DelayQueue;

/**
 * @author ningzhaosheng
 * @date 2024/4/18 16:18:10
 * @description 看门狗线程
 */
public class WatchDogThead implements Runnable {
    private final static int LOCK_TIME = 1 * 1000;
    private final static String LOCK_TIME_STR = String.valueOf(LOCK_TIME);
    private final static String RS_DISTLOCK_NS = "tdln2:";
    /*看门狗线程*/
    private Thread expireThread;
    //通过delayDog 避免无谓的轮询,减少看门狗线程的轮序次数   阻塞延迟队列   刷1  没有刷2
    private static DelayQueue<ItemVo<LockItem>> delayDog = new DelayQueue<>();
    //续锁逻辑:判断是持有锁的线程才能续锁
    private final static String DELAY_LOCK_LUA =
            "if redis.call('get',KEYS[1])==ARGV[1] then\n" +
                    "        return redis.call('pexpire', KEYS[1],ARGV[2])\n" +
                    "    else return 0 end";
    // 客户端
    Jedis jedis = null;

    public WatchDogThead(Jedis jedis) {
        this.jedis = jedis;
    }

    public void addDelayDog(ItemVo<LockItem> itemItemVo) {
        delayDog.add(itemItemVo);
    }

    @Override
    public void run() {
        System.out.println("看门狗线程已启动......");
        while (!Thread.currentThread().isInterrupted()) {
            try {
                LockItem lockItem = delayDog.take().getData();//只有元素快到期了才能take到  0.9s
                try {
                    Long result = (Long) jedis.eval(DELAY_LOCK_LUA,
                            Arrays.asList(RS_DISTLOCK_NS + lockItem.getKey()),
                            Arrays.asList(lockItem.getValue(), LOCK_TIME_STR));
                    if (result.longValue() == 0L) {
                        System.out.println("Redis上的锁已释放,无需续期!");
                    } else {
                        delayDog.add(new ItemVo<>((int) LOCK_TIME,
                                new LockItem(lockItem.getKey(), lockItem.getValue())));
                        System.out.println("Redis上的锁已续期:" + LOCK_TIME);
                    }
                } catch (Exception e) {
                    throw new RuntimeException("锁续期失败!", e);
                }
            } catch (InterruptedException e) {
                System.out.println("看门狗线程被中断");
                break;
            }
        }
        System.out.println("看门狗线程准备关闭......");
    }

}

1.5 测试类

java 复制代码
package com.ningzhaosheng.distributelock.redis;

import com.ningzhaosheng.distributelock.zookeeper.OrderServiceHandle;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;

/**
 * @author ningzhaosheng
 * @date 2024/4/19 8:07:38
 * @description redis看门狗分布式锁测试类
 */
public class TestRedisDistributedLockWithDog {
    private static final String REDIS_ADDRESS1 = "192.168.31.167:26379";
    private static final String REDIS_ADDRESS2 = "192.168.31.215:26379";
    private static final String REDIS_ADDRESS3 = "192.168.31.154:26379";
    public static void main(String[] args) {
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(REDIS_ADDRESS1);
        sentinels.add(REDIS_ADDRESS2);
        sentinels.add(REDIS_ADDRESS3);
        // Redis主服务器的名字
        String masterName = "mymaster";

        // Redis服务器的密码
        String password = "xiaoning";

        try {

            int NUM = 10;
            CountDownLatch cdl = new CountDownLatch(NUM);
            for (int i = 1; i <= NUM; i++) {
                // 创建JedisSentinelPool
                JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels, password);
                // 从池中获取Jedis实例
                Jedis jedis  = pool.getResource();
                // 按照线程数迭代实例化线程
                Lock lock = new RedisDistributedLockWithDog(jedis);
                new Thread(new OrderServiceHandle(cdl, lock)).start();
                // 创建一个线程,倒计数器减1
                cdl.countDown();
                pool.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

1.6 测试结果

1.7 总结

以上基于看门狗机制实现的Redis锁在对高并发要求不高的场景下可以使用,但是其实它还是有问题的,在主从架构模式下,可能会导致两个线程同时获取到锁得问题出现:

  • 线程A从主redis中请求一个分布式锁,获取锁成功;
  • 从redis准备从主redis同步锁相关信息时,主redis突然发生宕机,锁丢失了;
  • 触发从redis升级为新的主redis;线程B从继任主redis的从redis上申请一个分布式锁,此时也能获取锁成功;
  • 导致,同一个分布式锁,被两个客户端同时获取,没有保证独占使用特性;

当对高可用有要求的场景,这种方式就不合适了,那么在高并发、高可靠场景下,我们该如何基于Redis 实现分布式锁呢?得接着往下看!

二、RedLock分布式锁

2.1 Redlock分布式锁简介

Redlock是由Redis的作者Salvatore Sanfilippo提出的一种分布式锁算法,它利用Redis的特性来实现分布式锁。Redlock算法的核心思想是通过在多个Redis实例上创建相同的分布式锁,以保证锁的可靠性。Redlock算法通过在Redis中设置一个唯一的键值对来表示锁的存在,其他线程或进程需要通过竞争来获取这个锁。

官网wiki:8. 分布式锁和同步器 · redisson/redisson Wiki · GitHub

2.2 RedLock测试例子

java 复制代码
package com.ningzhaosheng.distributelock.redis.redisson;

import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * @author ningzhaosheng
 * @date 2024/6/22 15:57:17
 * @description
 */
public class TestRedLockByRedisson {
    public static void main(String[] args) {
         String RS_DISTLOCK_NS = "tdln2:";
         String lockName = "lock";
        Config config = new Config();

        config.useSentinelServers()
                .addSentinelAddress("redis://192.168.31.215:26379","redis://192.168.31.167:26379","redis://192.168.31.154:26379")
                .setMasterName("mymaster")
                .setPassword("xiaoning");
        RedissonClient redisson = Redisson.create(config);

        try {
    
            for (int i = 1; i <= 10; i++) {
                // 生成8位随机数
                Random random = new Random();
                // 创建RedLock
                RLock rLock = redisson.getLock(RS_DISTLOCK_NS+lockName+ random.nextInt(99999999));
                RedissonRedLock rdLock = new RedissonRedLock(rLock);
                boolean islock = rdLock.tryLock(500,3000,TimeUnit.MILLISECONDS);
                if(islock){
                    System.out.println("执行业务啦!======================");
                }else{
                    System.out.println("获取锁失败!======================");
                }
                rdLock.unlock();

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        redisson.shutdown();
    }
}

测试结果:

2.3 RedLock 加锁核心源码分析

这里我就以tryLock()为例,分析下RedLock 的源码:

java 复制代码
 public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long newLeaseTime = -1L;
        if (leaseTime != -1L) {
            if (waitTime == -1L) {
                newLeaseTime = unit.toMillis(leaseTime);
            } else {
                newLeaseTime = unit.toMillis(waitTime) * 2L;
            }
        }

        long time = System.currentTimeMillis();
        long remainTime = -1L;
        if (waitTime != -1L) {
            remainTime = unit.toMillis(waitTime);
        }

        long lockWaitTime = this.calcLockWaitTime(remainTime);
        // 允许加锁失败的节点数量(N-(N/2+1))
        int failedLocksLimit = this.failedLocksLimit();
        List<RLock> acquiredLocks = new ArrayList(this.locks.size());
        ListIterator iterator = this.locks.listIterator();
        // 遍历所有节点,通过EVAL命令执行lua脚本加锁
        while(iterator.hasNext()) {
            // 迭代获取Redis实例
            RLock lock = (RLock)iterator.next();

            boolean lockAcquired;
            // 尝试加锁,调用lock.tryLock()方法
            try {
                if (waitTime == -1L && leaseTime == -1L) {
                    lockAcquired = lock.tryLock();
                } else {
                    long awaitTime = Math.min(lockWaitTime, remainTime);
                    // 尝试加锁
                    lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
                }
            } catch (RedisResponseTimeoutException var21) {
                // 如果抛出异常,为了防止加锁成功,但是响应失败,需要解锁所有节点,同时返回加锁失败状态
                this.unlockInner(Arrays.asList(lock));
                lockAcquired = false;
            } catch (Exception var22) {
                lockAcquired = false;
            }

            if (lockAcquired) {
                // 如果加锁成功,把锁加到以获得锁集合中
                acquiredLocks.add(lock);
            } else {
                //计算已经申请锁失败的节点是否已经到达允许加锁失败节点个数限制 (即:N-(N/2+1));如果已经到达,就认定最终申请锁失败,则没有必要继续从后面的节点申请了因为 Redlock 算法要求至少N/2+1 个节点都加锁成功,才算最终的锁申请成功
                if (this.locks.size() - acquiredLocks.size() == this.failedLocksLimit()) {
                    break;
                }

                if (failedLocksLimit == 0) {
                    this.unlockInner(acquiredLocks);
                    if (waitTime == -1L) {
                        return false;
                    }

                    failedLocksLimit = this.failedLocksLimit();
                    acquiredLocks.clear();

                    while(iterator.hasPrevious()) {
                        iterator.previous();
                    }
                } else {
                    --failedLocksLimit;
                }
            }
            // 计算 目前从各个节点获取锁已经消耗的总时间,如果已经等于最大等待时间,则认定最终申请锁失败,返回false
            if (remainTime != -1L) {
                remainTime -= System.currentTimeMillis() - time;
                time = System.currentTimeMillis();
                if (remainTime <= 0L) {
                    this.unlockInner(acquiredLocks);
                    return false;
                }
            }
        }
        // 重置锁过期时间
        if (leaseTime != -1L) {
            acquiredLocks.stream().map((l) -> {
                return (RedissonLock)l;
            }).map((l) -> {
                return l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
            }).forEach((f) -> {
                f.syncUninterruptibly();
            });
        }
        // 如果逻辑正常执行完则认为最终申请锁成功,返回true
        return true;
    }

2.4 RedLock的问题与解决方案

2.4.1 问题

2.4.1.1 时钟偏移

Redlock算法中需要使用到各个Redis实例的系统时钟来实现锁的过期时间控制。然而,不同Redis实例之间的时钟可能存在偏移,导致锁的过期时间计算错误,从而导致锁的误释放或者死锁的发生。

2.4.1.2 单点故障

Redlock算法的可靠性依赖于多个Redis实例之间的协作。如果其中一个Redis实例发生故障,可能会导致整个系统的分布式锁服务不可用。

2.4.1.3 性能瓶颈

RedLock需要访问多个实例,网络延迟可能导致某些线程获取锁的时间较长,会增加网络带宽的压力。此外,每个实例都需要对锁进行检查和定时删除操作,也会影响Redis的性能。

2.4.2 解决方案

针对上述问题,我们可以采取以下解决方案来提高Redlock分布式锁在高并发环境下的性能和可靠性:

2.4.2.1 引入重试机制

为了应对网络延迟带来的竞争问题,我们可以在获取锁失败后进行重试。通过设定适当的重试次数和重试间隔,可以减少因网络延迟导致的锁竞争失败的情况。但是该方案还是解决不了性能瓶颈问题,性能瓶颈问题是架构方案决定的。

2.4.2.2 时钟校准

为了解决时钟偏移问题,我们可以通过定期校准各个Redis实例的系统时钟来保证它们之间的一致性。可以使用NTP协议或者其他时间同步机制来实现时钟校准。

2.4.2.3 引入冗余节点

为了避免单点故障带来的问题,我们可以引入冗余节点来提高系统的可用性。通过在系统中增加多个Redis实例作为备份节点,可以在主节点故障时快速切换到备份节点,保证分布式锁服务的可用性。

2.5 总结

基于Redis实现分布式锁的方案,他的实现方式有两种,一直是基于setnx指令+lua脚本+看门狗进程,实现在Redis单实例下的分布式锁方案,这种方式在主从模式下,当主节点宕机,主从切换的时候,会有获取锁失败的情况或者会有多个客户端同时获取到锁的情况,为了提高分布式锁的可用性,则出现了改进后的RedLock锁,RedLock锁是基于多实例加锁(即:N/2+1),同时给N/2+1个实例加锁成功,才算获取锁成功。但是他的问题也很明显,具体如前文所述。所以在进行技术方案选型的时候,你要结合你的场景,明确选用分布式锁时需要用途:

  1. 如果你只考虑提升加锁效率问题,那么选用一台高性能的服务器,部署单实例,采用基于setnx指令+lua脚本+看门狗进程的方案就够了。
  2. 如果你考虑高可用,想避免单点问题,并且你的部署架构是主从的,或者集群模式的,那么你可以选择使用RedLock,但是你就不得不考虑因此带来的复杂性和各种问题。
  3. 如果你要保证绝对的数据一致性,那么不好意思,Redis实现分布式锁方案可能不适合你,因为从架构的角度来说,Redis的架构属于AP架构,它保证的是可用性;这时,你可以考虑使用Zookeeper实现分布式锁方案,因为它的架构属于CP架构,保证的是一致性;但是Zookeeper也有问题,就是它的性能没有Redis高。

那么在并发量大,可靠性高的场景下,我们最终应该怎么技术选型呢,我的建议是避免使用分布式锁,可以通过串行化来解决,比如同步串行化,异步串行化等方案。这里暂时不对串行化设计展开论述,后续有时间我会在另外的主题文章进行分享。

好了,本次内容就分享到这,欢迎关注本博主。如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!

相关推荐
okok__TXF3 分钟前
Nginx + Lua脚本打配合
nginx·lua
miss writer8 分钟前
Redis分布式锁释放锁是否必须用lua脚本?
redis·分布式·lua
m0_7482548815 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
Mr.131 小时前
数据库的三范式是什么?
数据库
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Python之栈1 小时前
【无标题】
数据库·python·mysql
风_流沙1 小时前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
亽仒凣凣1 小时前
Windows安装Redis图文教程
数据库·windows·redis
字节程序员1 小时前
Jmeter分布式压力测试
分布式·jmeter·压力测试
亦世凡华、2 小时前
MySQL--》如何在MySQL中打造高效优化索引
数据库·经验分享·mysql·索引·性能分析