1、分布式锁的实现方式

大概有三种:1.基于关系型数据库,2.基于缓存,3基于zookeeper
大部分网站使用的是基于缓存的,有更好的性能,而缓存一般是以集群方式部署,保证了高可用性

总体来说,支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以给你完美实现。

2.基于缓存redis,使用开源 redisson 实现分布式锁

3、关于redisson 锁的几点说明,

1、通过阅读redission锁的API可以得知,其获取锁释放锁的使用和JDK里面的lock很相似,底层的实现采用了类似lock的处理方式
2、redisson 依赖redis,因此使用redisson 锁需要服务端安装redis,而且redisson 支持单机和集群两种模式下的锁的实现
3、redisson 在多线程或者说是分布式环境下实现机制,其实是通过设置key的方式进行实现,也就是说多个线程为了抢占同一个锁,其实就是争抢设置key,这个和zookeeper的锁是不是有点儿相似?

通过下面这张图来简单看看redisson 锁的实现原理,

spring整合swing_spring整合swing


1)加锁机制

咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器

这里注意,仅仅只是选择一台机器!这点很关键!

紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:

spring整合swing_redis_02

简单解释一下这段lua脚本要做的事情,

1、锁不存在的情况下加锁,
KEYS[1]代表的是你加锁的那个key,比如说:
RLock lock = redisson.getLock(“myLock”);
这里你自己设置了加锁的那个锁key就是“myLock”

ARGV[1]代表的就是锁key的默认生存时间,默认30秒

ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:
8743c9c0-0795-4907-87fd-6c719a6b4586:1

给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁
如何加锁呢?很简单,用下面的命令:

hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:

mylock{

    "8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
}
  • 上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。

接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
好了,到此为止,ok,加锁完成了。

(2)锁互斥机制

那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?

很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。

接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。

此时客户端2会进入一个while循环,不停的尝试加锁。

(3)watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

(4)可重入加锁机制
那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?
第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。

第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

此时就会执行可重入加锁的逻辑,他会用:

incrby myLock

8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

通过这个命令,对客户端1的加锁次数,累加1。

此时myLock数据结构变为下面这样:

大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

(5)释放锁机制

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。

如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:

“del myLock”命令,从redis里删除这个key。

然后呢,另外的客户端2就可以尝试完成加锁了。

这就是所谓的分布式锁的开源Redisson框架的实现机制。

一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁

有了上面的概念,下面来具体说一下springboot整合redisson实现分布式锁的代码整合,

1、pom文件需要添加如下依赖,

<dependencies>

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

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.5.0</version>
		</dependency>

	</dependencies>


2、redisson锁需要依赖redis,因此需要在配置文件中添加redis的响应配置,这里为了演示方便没有加其他的配置,

server.port=8082

redisson.address=redis://127.0.0.1:6379

3、定义一个接口,里面是关于redisson操作锁的API接口,

public interface DistributedLocker {
	
	RLock lock(String lockKey);

	RLock lock(String lockKey, long timeout);

	RLock lock(String lockKey, TimeUnit unit, long timeout);

	boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);

	void unlock(String lockKey);

	void unlock(RLock lock);

}

4、接口实现类,

@Component
public class RedissonDistributedLocker implements DistributedLocker {

	@Autowired
	private RedissonClient redissonClient; // RedissonClient已经由配置类生成,这里自动装配即可

	// lock(), 拿不到lock就不罢休,不然线程就一直block
	@Override
	public RLock lock(String lockKey) {
		RLock lock = redissonClient.getLock(lockKey);
		lock.lock();
		return lock;
	}

	// leaseTime为加锁时间,单位为秒
	@Override
	public RLock lock(String lockKey, long leaseTime) {
		RLock lock = redissonClient.getLock(lockKey);
		lock.lock(leaseTime, TimeUnit.SECONDS);
		return null;
	}

	// timeout为加锁时间,时间单位由unit确定
	@Override
	public RLock lock(String lockKey, TimeUnit unit, long timeout) {
		RLock lock = redissonClient.getLock(lockKey);
		lock.lock(timeout, unit);
		return lock;
	}

	@Override
	public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
		RLock lock = redissonClient.getLock(lockKey);
		try {
			return lock.tryLock(waitTime, leaseTime, unit);
		} catch (InterruptedException e) {
			return false;
		}
	}

	@Override
	public void unlock(String lockKey) {
		RLock lock = redissonClient.getLock(lockKey);
		lock.unlock();
	}

	@Override
	public void unlock(RLock lock) {
		lock.unlock();
	}

}


5、redisson基本配置类,也是大家熟悉的套路,因为redisson支持多种模式下的配置,比如单机、集群、哨兵模式等,都可以根据实际业务需要进行配置,这里为演示方便使用单机配置,

@Configuration
public class RedissonManager {
	
	@Value("${redisson.address}")
	private String addressUrl;
	
	@Bean
	public RedissonClient getRedisson() throws Exception{
		RedissonClient redisson = null;
		Config config = new Config();
		config.useSingleServer()
			  .setAddress(addressUrl);
		redisson = Redisson.create(config);
		
		System.out.println(redisson.getConfig().toJSON().toString());
		return redisson;
	}
	
	
}


注意的是,在这个配置类里面,关于redis连接配置还有很多其他参数,比如像连接的用户名、密码、超时时间、连接的库信息等,可以根据需要往里面添加,

6、接下来,写一个测试类,使用100个线程模拟一下获取锁的动作,第一次假如我们不释放锁,可以比较清楚看到哪个线程获取到了锁,接口请求一下,看看控制台打印结果,

@RestController
@RequestMapping("/redisson")
public class LockTestController {

	@Autowired
	private DistributedLocker distributedLocker;

	@RequestMapping("/test")
	public void redissonTest() {
		String key = "redisson_key";
		for (int i = 0; i < 100; i++) {
			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						System.err.println("=============线程开启============" + Thread.currentThread().getName());
						/*
						 * distributedLocker.lock(key,10L); //直接加锁,获取不到锁则一直等待获取锁
						 * Thread.sleep(100); //获得锁之后可以进行相应的处理
						 * System.err.println("======获得锁后进行相应的操作======"+Thread.
						 * currentThread().getName());
						 * distributedLocker.unlock(key); //解锁
						 * System.err.println("============================="+
						 * Thread.currentThread().getName());
						 */
						boolean isGetLock = distributedLocker.tryLock(key, TimeUnit.SECONDS, 5L, 10L); // 尝试获取锁,等待5秒,自己获得锁后一直不解锁则10秒后自动解锁
						if (isGetLock) {
							System.out.println("线程:" + Thread.currentThread().getName() + ",获取到了锁");
							Thread.sleep(100); // 获得锁之后可以进行相应的处理
							System.err.println("======获得锁后进行相应的操作======" + Thread.currentThread().getName());
							//distributedLocker.unlock(key);
							System.err.println("=============================" + Thread.currentThread().getName());
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
			t.start();
		}
	}
	

}

项目启动一下,浏览器输入,http://localhost:8082/redisson/test,查看控制台,可以看到在这100个线程中,只有第24个线程获取到了锁,

spring整合swing_redis_03

然后我们将释放锁的动作放开,重启项目再访问一下,这时可以看到只要某个抢到锁的线程执行完毕并且释放了锁资源,其他的线程很快就会获取到锁,速度还是很快的,

spring整合swing_客户端_04


到这里,关于redisson锁的基本使用整合就结束了,感谢观看!