分布式锁RedisLockRegistry实现及原理

一、Redis实现分布式锁原理

Redis所谓分布式锁,需要满足以下特性:
在这里插入图片描述

  • 独占性:对同一把锁,在同一时刻只能被同一个客户端占有,因此体现了互斥性。
  • 健壮性:即不能产生死锁(dead lock). 占有锁客户端因宕机获取锁失失败或过期立即返回执行解锁动作,锁可以被正常使用,不会造成客户端县城阻塞。
  • 对称性:加锁和解锁的使用方必须为同一身份. 不允许非法释放他人持有的分布式锁
  • 高可用:当提供分布式锁服务的基础组件中存在少量节点发生故障时,应该不能影响到分布式锁服务的稳定性
二、实现步骤

根据以上条件,可以大致设想出以下的Redis锁实现方案:

  1. 使用SETNX命令尝试设置锁的值,如果返回1表示成功获取锁,否则表示获取锁失败。
  2. 使用GET命令获取锁的值,判断当前客户端是否持有锁,如果持有锁则将锁的值加1,否则返回获取锁失败。
  3. 使用DEL命令释放锁。
  4. 使用过期时间来防止死锁,锁的过期时间应该大于业务处理的时间,一般为几秒到几分钟。

集成springIntegrationRedis

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.integration</groupId>
     <artifactId>spring-integration-redis</artifactId>
</dependency>

配置Redis分布式锁

@Configuration
public class RedisLockConfig {
    /**
     * 锁过期毫秒数
     */
    private static final long EXPIRE_AFTER_MILLS = 600000L;

    @Bean(destroyMethod = "destroy")
    public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
        return new RedisLockRegistry(redisConnectionFactory, "redis-lock", EXPIRE_AFTER_MILLS);
    }
}
// 第一种实现方式
@Resource
private RedisLockRegistry redisLockRegistry;

public void RedisLockTest(String key) {
    Lock lock = redisLockRegistry.obtain(key);
    // 尝试解锁,当前锁是否在使用
    if (!lock.tryLock()) {
        // 执行其他业务逻辑
        return;
    }
        
    try {
        // 加锁
        lock.lock();  
        //业务逻辑
    } finally {
        //释放锁
        lock.unlock();   
    }
}

//第二种实现方式
@RedisLock("redis-lock")
public void bussinessMehod(){

}
三、原理分析

obtain方法

public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean{

	private final Map<String, RedisLock> locks = new ConcurrentHashMap<>();
	private final String registryKey;

	@Override
	public Lock obtain(Object lockKey) {
		Assert.isInstanceOf(String.class, lockKey);
		String path = (String) lockKey;
		// 如果 key 对应的 value 不存在,则使用获取 remappingFunction 重新计算后的值,并保存为该 key 的 value,否则返回 value。
		return this.locks.computeIfAbsent(path, RedisLock::new);
	}

	private final class RedisLock implements Lock {
		private final String lockKey;
		private RedisLock(String path) {
			this.lockKey = constructLockKey(path);
		}

		private String constructLockKey(String path) {
			return RedisLockRegistry.this.registryKey + ":" + path;
		}
	}
}

根据key查找ConcurrentHashMap类型的locks集合,其中是否存在key,如存在就返回内部类RedisLock,不存在以当前registryKey类变量为key,调用RedisLock构造方法,并放入map集合。
补充:每个应用都会创建一个RedisLockRegistry实例,且ConcurrentHashMap是一个支持高并发更新与查询的map集合,因此不同线程之间会共享RedisLock类。

tryLock方法

public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {

	private @Nullable ScriptExecutor<K> scriptExecutor;
	
	@Override
	public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
		return scriptExecutor.execute(script, keys, args);
	}
}
local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
	redis.call('PEXPIRE', KEYS[1], ARGV[2])
	return true
elseif not lockClientId then
	redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
	return true
end	
return false

在研究tryLock方法之前,需解下obtainLock这私有方法。具体逻辑是调用RedisLockRegistry的内置Redis Lua脚本获取分布式锁key对应的value,结合上面代码和脚本可以看出execute方法key和args替换脚本KEYSARGV,并生成最终脚本内容,

  • 为保证分布式锁对称性须校验lockClientId和客户端一致,如一致则为重入锁,需重设置过期时间expireAfter(默认60s) 并返回成功;
  • 分布式锁lockClientI和key不存在,则设置传入RedisLock中私有变量lockKey为新key、当前lockClientId,返回成功;
  • 以上两项都不满足,当前锁被占用,上锁失败,返回失败;
public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {

	private final RedisScript<Boolean> obtainLockScript;
	
	private final StringRedisTemplate redisTemplate;
	
	private static final String OBTAIN_LOCK_SCRIPT =
			"local lockClientId = redis.call('GET', KEYS[1])\n" +
					"if lockClientId == ARGV[1] then\n" +
					"  redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
					"  return true\n" +
					"elseif not lockClientId then\n" +
					"  redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
					"  return true\n" +
					"end\n" +
					"return false";

	// 构造函数
	public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey, long expireAfter) {
			Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
			Assert.notNull(registryKey, "'registryKey' cannot be null");
			this.redisTemplate = new StringRedisTemplate(connectionFactory);
			this.obtainLockScript = new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);
			this.registryKey = registryKey;
			this.expireAfter = expireAfter;
		}
	
	//内部类 实现了Lock接口
	private final class RedisLock implements Lock {

		private final ReentrantLock localLock = new ReentrantLock();
	
		@Override
		public boolean tryLock() {
			try {
				return tryLock(0, TimeUnit.MILLISECONDS);
			}
			catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				return false;
			}
		}
	
		@Override
		public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
			long now = System.currentTimeMillis();
			if (!this.localLock.tryLock(time, unit)) {
				return false;
			}
			try {
				long expire = now + TimeUnit.MILLISECONDS.convert(time, unit);
				boolean acquired;
				while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { //NOSONAR
					Thread.sleep(100); //NOSONAR
				}
				if (!acquired) {
					this.localLock.unlock();
				}
				return acquired;
			}
			catch (Exception e) {
				this.localLock.unlock();
				rethrowAsLockException(e);
			}
			return false;
		}
		
		private boolean obtainLock() {
			Boolean success = RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript, Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId, String.valueOf(RedisLockRegistry.this.expireAfter));
			boolean result = Boolean.TRUE.equals(success);
			if (result) {
				this.lockedAt = System.currentTimeMillis();
			}
			return result;
		}
	}
}

通过ReentrantLock获取本获取本地锁,不存在直接返回,当前锁若存在但obtainLock()返回失败,且未超过过期时间则锁转为自旋锁睡眠100ms后进行下次重试,直到拿到redis锁循环结束,返回成功,若超时了还没拿到,释放锁,返回失败
补充: ReentrantLock是一个可重入的互斥锁

unlock方法

private final class RedisLock implements Lock {	

	@Override
	public void unlock() {
		if (!this.localLock.isHeldByCurrentThread()) {
			throw new IllegalStateException("You do not own lock at " + this.lockKey);
		}
		if (this.localLock.getHoldCount() > 1) {
			this.localLock.unlock();
			return;
		}
		try {
			if (!isAcquiredInThisProcess()) {
				throw new IllegalStateException("Lock was released in the store due to expiration. " +
						"The integrity of data protected by this lock may have been compromised.");
			}
			// 当前线程是否已销毁
			if (Thread.currentThread().isInterrupted()) {
				RedisLockRegistry.this.executor.execute(this::removeLockKey);
			}
			else {
				removeLockKey();
			}
	
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("Released lock; " + this);
			}
		}
		catch (Exception e) {
			ReflectionUtils.rethrowRuntimeException(e);
		}
		finally {
			this.localLock.unlock();
		}
	}
}	
    // 当前线程是否持有锁,
	public boolean isHeldByCurrentThread() {
	    return sync.isHeldExclusively();
	}
  
	// 当前线程持有该锁次数
	public int getHoldCount() {
	    return sync.getHoldCount();
	}
	
	// 当前客户端持有锁跟当前是否一致
	public boolean isAcquiredInThisProcess() {
		return RedisLockRegistry.this.clientId.equals(RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).get());
	}
	
	// 释放当前锁
    @ReservedStackAccess
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

释放锁的过程就表简单,isHeldByCurrentThread()方法获取当前线程是否持有锁,若不是则抛出当前锁不被持有异常,getHoldCount()方法获取当前线程持有该锁次数,次数>1,释放当前锁且当前线程持有锁的计数-1

四、总结

RedisLockRegistry通过本地锁ReentrantLockRedis锁的双重锁实现,使用ReentrantLock可重入锁,RedisLockRegistry对锁无续期操作;只适用于单实例的情况下,key过期,还能通过本地锁保证,但多实例下无法通过本地锁保证,会有问题。在分布式系统中,锁的作用非常重要,合理有效的利用锁可以保证系统的安全性和高效性。Redis分布式锁是一种比较常用的方式,通过Redis的高性能和分布式特性,可以方便地实现分布式锁的功能。开发人员可以根据业务需求和系统性能需求来决定是否使用Redis分布式锁,

[1]Java三种方式实现redis分布式锁[EB/OL].
[2]redis 分布式锁进阶篇[EB/OL].
[3]Java中使用Redis实现分布式锁[EB/OL].

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RedisLockRegistry 是 Spring Integration 提供的一种基于 Redis 的分布式实现。它使用 Redis 实现的管理和协调,提供了一种简单而有效的方式来确保在分布式环境中的资源互斥访问。 使用 RedisLockRegistry 首先需要配置 Redis 连接信息,然后通过创建 RedisLockRegistry 对象来获取分布式实例。下面是一个简单的示例: ```java @Configuration @EnableIntegration public class RedisLockConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { // 配置 Redis 连接工厂 RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("localhost",6379); return new LettuceConnectionFactory(config); } @Bean public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) { // 创建 RedisLockRegistry 实例,并指定 Redis 连接工厂 return new RedisLockRegistry(redisConnectionFactory, "my-locks"); } } ``` 在需要使用分布式的地方,可以通过 RedisLockRegistry 来获取并执行相应的操作。下面是一个简单的示例: ```java @Autowired private RedisLockRegistry redisLockRegistry; public void performTaskWithLock() { Lock lock = redisLockRegistry.obtain("my-lock"); // 获取名为 "my-lock" 的分布式 try { if (lock.tryLock()) { // 尝试获取 // 执行需要互斥访问的操作 } else { // 被其他线程占用,执行相应的逻辑 } } finally { lock.unlock(); // 释放 } } ``` RedisLockRegistry 会在 Redis 中创建相应的键来表示的状态,并使用 Redis 的原子操作来确保的正确获取和释放。这样可以保证在分布式场景下的资源互斥访问。 需要注意的是,在使用 RedisLockRegistry 进行分布式管理时,需要确保 Redis 的可用性和性能,以及合理设置的超时时间和重试机制,以防止死和长时间的资源占用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值