Redisson实现分布式锁原理浅析

本文记录Redisson实现分布式锁原理


分布式锁特性

不管使用什么中间件,下述几点是实现分布式锁必须要考虑到的.

  • 互斥
  • 避免死锁
  • 性能保证:

    高并发分布式系统中,线程互斥等待会成为性能瓶颈,需要好的中间件和实现来保证性能.
  • 锁特性:

    分布式锁最好实现如Java Lock的一些功能如:锁判断,超时设置,可重入性等

Redis实现之Redisson原理

本次演示涉及的Redisson版本为

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.1</version>
</dependency>
  • 互斥:

    通过Redis数据结构来保证分布式锁的唯一性
  • 避免死锁:

    通过Redis设置有效期保证锁的释放
  • 性能保证:

    通过Redis部署方式(主从或集群)实现锁分段,且Redis本身作为缓存中间件满足大部分场景性能需求
  • 锁特性:

    针对分布式锁的使用,提供超时设置,锁中断,可重入性,锁续期(看门狗),锁判断(订阅模式)等特性

原理简图

下述为整理的原理简图,大致描述了加锁解锁的功能流程.
在这里插入图片描述

源码分析

针对上述流程简图,对关键代码进行分析

  • 加锁
/**
 * leaseTime:租赁时间,当前锁最久可使用时间,默认30S,可通过看门狗进行自动续期
 * unit:时间单位
 * interruptibly:是否可被中断,默认不可中断
 */
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    // 当前线程Id,用于组成锁的唯一键
    long threadId = Thread.currentThread().getId();
    /*
     * 加锁核心逻辑.
     * 当其他线程使用同一加锁关键字加锁时,返回的ttl为关键字锁的剩余租赁时间
     */
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // ttl为null时,证明当前线程加锁成功
    if (ttl == null) {
        return;
    }
    /*
     * 当锁被占用的情况下,线程通过Redis订阅解锁事件,及时感知解锁操作,并再次竞争锁.
     * 同时通过分布式信号量,完成竞争限流
     */
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    if (interruptibly) {
        commandExecutor.syncSubscriptionInterrupted(future);
    } else {
        commandExecutor.syncSubscription(future);
    }

    try {
        // 循环竞争锁
        while (true) {
            ttl = tryAcquire(leaseTime, unit, threadId);
            // 锁获取成功
            if (ttl == null) {
                break;
            }

            // 等待解锁消息发布
            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);
    }
}

/**
 * 异步的加锁方法,由tryAcquire调用,实现加锁的核心逻辑
 */
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
    /*
     * leaseTime=-1为内置默认的锁租赁时间标志,默认为看门狗的超时时间30S
     */
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }
    // 异步执行Lua脚本
    RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        // 针对异常进行处理
        if (e != null) {
            return;
        }

        // 加锁成功,开启看门狗,进行锁续期操作
        if (ttlRemaining) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

/**
 * 定义异步类,执行加锁Lua脚本,保证执行的原子性
 */
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    /*
     * Lua脚本解释:
     * 如果当前锁关键字不存在,新增Redisson锁,并设置过期时间,返回null,结束;
     * 如果Redisson锁存在,且加锁线程为当前线程,Redisson锁重入次数加1,重置过期时间,返回null,结束;
     * 否则,当前锁关键字存在,返回其剩余租赁时间
     */
    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));
}

注:Redisson锁数据结构:

Hash:{"myLock":{"key":"761e9b81-5e36-494c-a94d-24ae725b3747:28","value":1}}

myLock:加锁的关键字,按照业务自定义
key:锁的唯一键,由Redisson连接池中连接Id及当前线程Id组合而成,使用:分割
value:当前锁重入的次数

  • 解锁
/**
 * 通过线程Id进行解锁操作
 */
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    // 解锁核心操作,异步执行Lua脚本
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
        // 取消当前线程注册的看门狗
        cancelExpirationRenewal(threadId);

        // 异常处理
        if (e != null) {
            result.tryFailure(e);
            return;
        }
        
        // 当前线程未持有锁,进行解锁时的异常处理
        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;
        }
        
        // 解锁成功
        result.trySuccess(null);
    });

    return result;
}

/**
 * 定义异步类,执行解锁Lua脚本,保证执行的原子性
 */
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    /*
     * 如果锁关键字对应的Redisson锁下线程不为当前线程,返回null
     * 如果锁关键字对应的Redisson锁下线程为当前线程,重入次数减1:当重入次数大于0,重设锁失效时间,返回0;当重入次数小于等于0,删除Redisson锁,并发布解锁事件,返回1
     * 均不符合,返回null
     */
    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));
}

测试代码

package com.pig4cloud.pig.admin.lock;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.test.context.SpringBootTest;

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

@Slf4j
@SpringBootTest
public class TestRedission {

	CountDownLatch countDownLatch;

	/**
	 * 测试多线程竞争
	 */
	@Test
	public void expirationRenewalTest() throws InterruptedException {
		countDownLatch = new CountDownLatch(3);
		Thread t1 = new Thread(new RedissionLockRunnable());
		t1.start();
		Thread t2 = new Thread(new RedissionLockRunnable());
		t2.start();
		countDownLatch.await();
	}

	/**
	 * 测试锁重入
	 */
	@Test
	public void tryLockInnerAsyncTest() throws InterruptedException {
		countDownLatch = new CountDownLatch(1);
		Thread t1 = new Thread(new RedissionLockReentrantRunnable());
		t1.start();
		countDownLatch.await();
	}

	RedissonClient redissonClient = createRedissonClient();
	private class RedissionLockRunnable implements Runnable{
		@Override
		public void run() {
			try {
				System.out.println("Thread:" + Thread.currentThread().getName() + ",time: " + System.currentTimeMillis());
				RLock lock = redissonClient.getLock("myLock");
				if (lock.tryLock(100000, TimeUnit.MILLISECONDS)) {
					try {
						Thread.sleep(10000);
					} finally {
						countDownLatch.countDown();
						lock.unlock();
					}
				} else {
					countDownLatch.countDown();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	private class RedissionLockReentrantRunnable implements Runnable{
		@Override
		public void run() {
			try {
				System.out.println("Thread:" + Thread.currentThread().getName() + ",time: " + System.currentTimeMillis());
				RLock lock = redissonClient.getLock("myLock");
				if (lock.tryLock(10000, TimeUnit.MILLISECONDS)) {
					try {
						Thread.sleep(1000);
						lock.tryLock();
						Thread.sleep(1000);
					} finally {
						countDownLatch.countDown();
						lock.unlock();
					}
				} else {
					countDownLatch.countDown();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	private RedissonClient createRedissonClient() {
		Config config = new Config();
		String node = "redis://127.0.0.1:6379";
		node = node.startsWith("redis://") ? node : "redis://" + node;
		SingleServerConfig serverConfig = config.useSingleServer()
			.setAddress(node)
			.setTimeout(50000)
			.setConnectionPoolSize(10)
			.setConnectionMinimumIdleSize(5);
		if (StringUtils.isNotBlank("")) {
			serverConfig.setPassword("");
		}
		RedissonClient redissonClient = Redisson.create(config);
		return redissonClient;
	}
}


查看Redisson锁的Redis指令:

-- 查看当前订阅者
pubsub channels;

-- 查看当前锁结构
HSCAN myLock;

-- 查看锁剩余有效期
ttl myLock;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值