redis分布式锁原理及Redisson怎么实现

本文详细介绍了Redisson作为Redis Java客户端的特性,包括分布式锁的实现。在单节点模式下,展示了如何初始化配置。分布式锁分为单master节点和多master节点的RedLock,前者在节点故障时存在高可用问题,后者通过多数节点加锁提高可靠性。代码示例展示了单节点模式下加锁、解锁的关键逻辑,利用发布订阅机制确保锁的正确释放和通知。
摘要由CSDN通过智能技术生成

先贴上redisson官方中文文档:目录 · redisson/redisson Wiki · GitHubRedisson - Redis Java client with features of In-Memory Data Grid. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Publish / Subscribe, Bloom filter, Spring Cache, Tomcat, Scheduler, JCache API, Hibernate, MyBatis, RPC, local cache ... - 目录 · redisson/redisson Wikihttps://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

redisson根据部署方式不同分为几种不同的模式:初始化时要选择适合自己的模式。

集群模式、云托管模式、单节点模式、哨兵模式、主从模式

redis分布式锁实现分两类:

  1. 单master节点的锁 即所有数据存在一个master节点上,可能是单节点模式、主从模式。
  2. 多master节点的锁 即数据是分开存的,槽可能分在不同的节点上。这种采用RedLock实现。

 单master节点锁原理:

        先判断加锁的key是否存在,不存在则创建key并加上客户端唯一ID和当前线程ID,让其值+1,再给key设置过期时间。存在则判断是否当前客户端的线程加锁的锁,是则继续+1,更新过期时间。解锁的时候也是判断是否当前客户端线程加锁的锁,是锁-1;为0时则删除锁,并在一个通道发布消息。然后当没设置自动释放锁时间时,会有一线程在锁释放前去不断刷新过期时间。默认是30秒,因为程序也不知道你到底要用多久,所以每隔10秒去刷新过期时间为30秒。这个线程叫看门狗。获取不到锁的线程会去订阅一个通道,然后循环尝试获取锁。直到有人释放锁,发布了该通道的消息。然后订阅了该通道的线程则会被唤起去尝试会去锁。

        这种锁的高可用存在问题,单节点部署一旦master挂掉则无法加锁。主从部署的话,刚加锁后主节点挂调,从节点升级为主节点。走新主节点去加锁可能发现没有该锁导致重复加锁。因为主从复制存在延迟。

多master节点锁原理(redlock)其实和上面类似,只不过会去大多数的节点加锁后才算加锁成功,这样就算部分节点挂调也可以保持高可用。

我这里单节点模式,贴下初始化代码。

public static RedissonClient redissonClient() {
    Config config = new Config();
    config.setCodec(new JsonJacksonCodec());

    SingleServerConfig singleServer = config.useSingleServer();
    singleServer.setAddress(prefixAddress("redis://xxx:6379"));
    singleServer.setPassword("xxx");
    return Redisson.create(config);
}

接下来看Redisson具体怎么实现:获取锁时就会选择获取的是具体什么锁。

 这里有个很关键点就是利用发布订阅来,实现后续的通知。

 private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        //获取锁
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
        // 为空表示已经获取到锁
            return;
        }
        //这里订阅对应这个锁的渠道 redisson_lock__channel:{zzy_test}
        //在释放锁时会去发布这个通知,这里就会收到消息
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        if (interruptibly) {
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }

        try {
            //获取不到则循环等待获取锁
            while (true) {
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    try {
                        //这里尝试获取,过期时间一到则再次循环尝试获取
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
    }
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1) {
            //这里设置了过期时间则没有下面的看门狗线程
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        //加锁核心代码
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }
            // lock acquired
            if (ttlRemaining == null) {
                //这里获取之后会添加看门狗线程去不断维护过期时间,默认是30秒释放锁,这里10秒续命一次
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }

 这里就是加锁核心逻辑,打包给lua脚本去执行。

 <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "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; " +
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

//再往里面跟进去有个关键的点
//计算槽,redis一共有16000+个槽,看key对应的哪个槽,再按槽去得到加锁的节点信息
 @Override
    public MasterSlaveEntry getEntry(String name) {
        int slot = calcSlot(name);
        return getEntry(slot);
    }

接下来看解锁核心逻辑:解锁就会发布指定消息到对应渠道redisson_lock__channel:{zzy_test},这样等待锁的线程讲被唤起,这样就串起来了。

 protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                        "return nil;" +
                        "end; " +
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                        "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.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值