spring-integration-redis中的分布式锁基本使用和源码解析

使用

依赖

spring-integration-redis中提供了Redis分布式锁的实现,使用spring-integration-redis需要引入以下依赖:

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

<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>

代码示例

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.integration.redis.util.RedisLockRegistry;

@Configuration
public class RedisLockConfig {
    @Bean
    public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
        return new RedisLockRegistry(redisConnectionFactory, "myRegistryKey");
    }
}
@Autowired
private RedisLockRegistry redisLockRegistry;

public void test() {
	// 获取锁对象
	Lock lock = redisLockRegistry.obtain("myLockKey");
	// 加锁
	boolean lockRsp = lock.tryLock(2, TimeUnit.SECONDS);
	try {
		// 业务逻辑
	} catch (Exception ex) {
		ex.printStackTrace();
	} finally {
		// 释放锁
		lock.unlock();
	}
}

源码解析

获取锁

首先看org.springframework.integration.redis.util.RedisLockRegistry类,Redis分布式锁就是由这个类提供,先看obtain方法:

@Override
public Lock obtain(Object lockKey) {
	Assert.isInstanceOf(String.class, lockKey);
	String path = (String) lockKey;
	return this.locks.computeIfAbsent(path, RedisLock::new);
}

这里用到了成员变量locks,locks用于存放RedisLock对象,每个lockKey对应一个RedisLock。computeIfAbsent表示如果有则返回,没有就new一个实例放到map并返回该实例。

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

加锁和锁互斥机制

RedisLock是RedisLockRegistry的内部类,实现了java.util.concurrent.locks.Lock接口,对外提供了加锁和释放锁等操作。首先看它的lock方法:

@Override
public void lock() {
	this.localLock.lock();
	while (true) {
		try {
			while (!obtainLock()) {
				Thread.sleep(100); //NOSONAR
			}
			break;
		}
		catch (InterruptedException e) {
			/*
			 * This method must be uninterruptible so catch and ignore
			 * interrupts and only break out of the while loop when
			 * we get the lock.
			 */
		}
		catch (Exception e) {
			this.localLock.unlock();
			rethrowAsLockException(e);
		}
	}
}

这里的核心代码就是obtainLock(),也就是获取锁。获取成功返回true,否则返回false。当返回false时会重试,每次重试之前都会sleep100毫秒,主要是为了防止占用太多的系统资源。但是这也造成了一个弊端,那就是当有多个客户端在竞争锁时,其中一个客户端释放了锁,别的客户端理论上至少要等100毫秒才能拿到锁,所以这种基于重试机制来竞争锁,性能不是很高,但是如果不sleep,会造成CPU占用率过高,所以这两者之间必须得有个权衡。也许通过事件通知机制来告诉其他客户端锁已经释放了会是一个不错的选择。注意到,lock方法通过this.localLock.lock()实现了线程同步,this.localLock是一个可重入锁对象:

private final ReentrantLock localLock = new ReentrantLock();

这里解释一下为什么要在本地实现线程同步,虽然是分布式锁,但是锁的竞争不一定是发生在多个客户端之间,同一个客户端不同的线程竞争锁也是也有可能的,如果没有线程同步,大量线程在下面这段代码自旋,也会使CPU资源耗尽。

while (!obtainLock()) {
	Thread.sleep(100); //NOSONAR
}

还有,在里面获取锁的循环外面为什么还有一个死循环?正如源码中注释所言,应该忽略Thread.sleep抛出的InterruptedException,直到获取到锁才能跳出循环体。所以,在里层循环因为InterruptedException中断时,还会继续获取分布式锁,直到获取成功为止。
我们再看这个obtainLock()方法:

private boolean obtainLock() {
	boolean success = RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,
			Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,
			String.valueOf(RedisLockRegistry.this.expireAfter));
	if (success) {
		this.lockedAt = System.currentTimeMillis();
	}
	return success;
}

它通过redisTemplate执行了一段lua脚本:

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";

调整下格式:

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

在Redis中,lua脚本是原子的,中间不会被其他命令插入。这里解释下参数,KEYS[1]就是上面调用RedisLockRegistry的obtain方法传入的那个lockKey,以它作为Redis中的key;ARGV[1]是客户端ID,是key对应的value;ARGV[2]是上述key-value的超时时间。所以上述脚本的意思是:

  1. 通过GET命令获取持有此锁的客户端ID,有可能是nil;
  2. 如果持有此锁的客户端ID等于调用此脚本的客户端ID,则重置当前缓存的超时时间并返回true;
  3. 如果返回的是nil,则设置缓存,同时设置缓存的过期时间,设置成功返回true;
  4. 其他情况,例如持有此锁的客户端ID和调用此脚本的客户端ID不相同,直接返回false。

释放锁和锁可重入机制

再看另外一个很重要的方法unlock:

@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 (Thread.currentThread().isInterrupted()) {
			RedisLockRegistry.this.executor.execute(() ->
					RedisLockRegistry.this.redisTemplate.delete(this.lockKey));
		}
		else {
			RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Released lock; " + this);
		}
	}
	catch (Exception e) {
		ReflectionUtils.rethrowRuntimeException(e);
	}
	finally {
		this.localLock.unlock();
	}
}

刚才在看lua脚本时我就有些疑问,RedisLock难道不是可重入的?直到看到unlock方法里的这段代码,才解答的我的疑惑:

if (this.localLock.getHoldCount() > 1) {
	this.localLock.unlock();
	return;
}

在当前线程中,localLock每调用一次lock方法,holdCount就+1,每调用一次unlock方法,holdCount就-1,所以如果holdCount>1,说明当前线程还继续持有锁,故直接return,不用去操作redis. 这样就实现了RedisLock的可重入。
最终释放锁时,直接通过redisTemplate删除对应的redis缓存。

总结

watch dog机制缺失

在源码中我并没有找到传说中的watch dog机制相关代码(不清楚是没找到还是真没实现),什么是watch dog机制?在源码中默认的redis key超时时间是60秒,也就是说60秒后锁就自动释放了,一般情况下我们的业务不会执行这么久,但要是真有这种极端情况,那就有问题了。watch dog可以定期检测,如果客户端还持有锁,那就给key续时。

加锁的性能太低

上面源码已经分析过了,竞争锁失败时通过自旋的方式来重试,由于担心CPU占用率过高,每次重试之前sleep 100毫秒,当有多个客户端在竞争锁时,其中一个客户端释放了锁,别的客户端理论上至少要等100毫秒才能拿到锁。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Mybatis-plus乐观锁和Redis分布式锁都是用于解决并发访问数据时的线程安全问题,但它们的实现方式和应用场景有所不同。 Mybatis-plus乐观锁是基于数据库的乐观锁实现机制,通过在数据表添加一个版本号字段来实现。当多个线程同时访问同一条数据时,每个线程会读取到这个版本号,并在更新时将版本号作为更新条件,如果版本号匹配,则执行更新操作;如果版本号不匹配,则说明其他线程已经修改了数据,当前线程更新失败。乐观锁适用于高并发读取、低并发更新的场景,可以减少数据库的锁冲突,提高并发性能。 Redis分布式锁是基于Redis实现的一种分布式锁机制。通过在Redis设置一个特定的key作为锁,在获取锁时判断该key是否存在,如果存在则表示锁已被其他线程占用,当前线程需要等待;如果不存在,则表示当前线程获取到了锁,可以执行业务操作。分布式锁适用于分布式环境下的并发控制,可以保证多个节点之间的数据一致性和并发安全。 对比而言,Mybatis-plus乐观锁是在数据库层面实现的,适用于单个数据库实例的并发控制,可以减少数据库的锁冲突,但并不能解决分布式环境下的并发问题。而Redis分布式锁则是基于Redis实现的,适用于分布式环境下的并发控制,可以保证多个节点之间的数据一致性和并发安全。 在实际应用,选择使用哪种机制还需要根据具体场景和需求来决定。如果是单个数据库实例的并发控制,可以选择Mybatis-plus乐观锁;如果是分布式环境下的并发控制,可以选择Redis分布式锁

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值