分布式锁实现原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/D_Guco/article/details/82559099

一 首先我们来回顾下普通锁的种类。

1 自旋锁

       自旋锁如果已经被别的线程获取,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

2 互斥锁 (Mutex Lock)
      互斥锁是阻塞锁,也是我们最常用的一种锁,当某线程无法获取互斥锁时,该线程会被直接挂起,不再消耗CPU时间,当其他线程释放互斥锁后,操作系统会唤醒那个被挂起的线程。阻塞锁可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间)时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争进入运行状态。它的优势在于,阻塞的线程不会占用 CPU 时间, 不会导致 CPU 占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下阻塞锁的性能要明显高于自旋锁。
      上面两种锁适用于不同场景:如果是多核处理器,预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥锁。如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等到运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥量的开销较大。

3 可重入锁 (Reentrant Lock)
      可重入锁是一种特殊的互斥锁,它可以被同一个线程多次获取,而不会产生死锁。首先它是互斥锁:任意时刻,只有一个线程锁。即假设A线程已经获取了锁,在A线程释放这个锁之前,B线程是无法获取到这个锁的,B要获取这个锁就会进入阻塞状态。其次,它可以被同一个线程多次持有。即,假设A线程已经获取了这个锁,如果A线程在释放锁之前又一次请求获取这个锁,那么是能够获取成功的。

     此外还有一些不常见的,偏向锁,重量级锁等等,https://www.cnblogs.com/charlesblc/p/5994162.html,这些锁应该是java中实现的锁,作为一个业余的java开发者真是有点孤陋寡闻,这里写个链接吧。

二 什么是分布式锁

     分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

     分布式锁的本质可以看作是特殊的普通锁,它的竞争者不是普通的进程和线程,它的竞争者分布在不同的主机上,需要通过网络来相互通信,不同的主机分布和网络的不确定性给分布式锁的实现和普通锁有着很大的不同。

   目前常见的分布式锁的实现有基于数据库,基于缓存(redis等缓存数据库),基于zoonkeeper三种:https://www.cnblogs.com/austinspark-jessylu/p/8043726.html,这里我简单通过redis实现一种。

      基于redis实现分布式锁很简单,通过一个命令setnx(set if no exist).只有在key不存在的时候才set成功返回1,否则返回失败0,但是要考虑到一些意外情况还是需要一个很严谨的逻辑。

//
// Created by dguco on 18-9-9.
//

#ifndef SERVER_REDI_LOCK_H
#define SERVER_REDI_LOCK_H
#include <iostream>
#include <redis_client.h>
#include <future>

using namespace std;

// 获取当前秒数
int GetSecond()
{
	struct timeval tmval = {0};
	int nRetCode = gettimeofday(&tmval, NULL);
	if (nRetCode != 0) {
		return 0;
	}
	return (int) (tmval.tv_sec);
}

#define LOCK_TIME_OUT 1 //1s

CRedisClient g_RedisCli;

class CRedisLock
{
public:
	/**
	 *
	 * @param lockKey
	 * @param isBlock  司机阻塞
	 * @return 是否锁成功
	 */
	bool Lock(std::string &lockKey, bool isBlock = false);
	template<class F, class... Args>
	void DoWithLock(std::string &lockKey, F &&f, Args &&... args);
	template<class F, class... Args>
	void TryDoWithLock(std::string &lockKey, F &&f, Args &&... args);
private:
	int m_iLockTimeOut;
};

template<class F, class... Args>
void CRedisLock::TryDoWithLock(std::string &lockKey, F &&f, Args &&... args)
{
	bool isLock = Lock(lockKey, false);
	if (isLock) {
		using return_type = typename std::result_of<F(Args...)>::type;
		auto task = std::make_shared<std::packaged_task<return_type()> >(
			std::bind(std::forward<F>(f), std::forward<Args>(args)...)
		);
		(*task)( );
		int now = GetSecond( );
		if (now < m_iLockTimeOut) {
			g_RedisCli.Del(lockKey);
		}
	}
}

template<class F, class... Args>
inline void CRedisLock::DoWithLock(std::string &lockKey, F &&f, Args &&... args)
{
	bool isLock = Lock(lockKey, true);
	if (isLock) {
		using return_type = typename std::result_of<F(Args...)>::type;
		auto task = std::make_shared<std::packaged_task<return_type()> >(
			std::bind(std::forward<F>(f), std::forward<Args>(args)...)
		);
		(*task)( );
		int now = GetSecond( );
		if (now < m_iLockTimeOut) {
			g_RedisCli.Del(lockKey);
		}
	}
}

bool CRedisLock::Lock(std::string &lockKey, bool isBlock)
{
	int lock = 0;
	m_iLockTimeOut = 0;
	bool isLock = false;
	while (lock != 1) {
		int now = GetSecond( );
		m_iLockTimeOut = now + LOCK_TIME_OUT + 1;
		lock = g_RedisCli.Setnx(lockKey, to_string(m_iLockTimeOut));
		//是否获取成功
		if (lock == 1) {
			isLock = true;
		}
		//判断是否超时,并设置新的超时时间
		if (!isLock) {
			string res = "";
			g_RedisCli.Get(lockKey, &res);
			//如果没有被其他竞争者
			if (res != "") {
				int out1 = atoi(res.c_str( ));
				string res1 = "";
				g_RedisCli.Getset(lockKey, &res1);
				//如果更新超时之前没有被其他竞争者抢先且超时
				if (now > out1 && res == res1) {
					isLock = true;
				}
			}

		}
		if (isLock or !isBlock) {
			break;
		}
		else {
			usleep(1000);
		}
	}
	return isLock;
}

#endif //SERVER_REDI_LOCK_H

string key = "test";
string lockKey = "Lock.test";
string key1 = "test1";
CRedisLock redisLock;

int main()
{
//	testDlopen();
//	testSoHotLoad();
//	listenFileChange();
    if (!g_RedisCli.Initialize("127.0.0.1", 6379, 2, 1)) {
        std::cout << "connect to redis failed" << std::endl;
        return -1;
    }
    redisLock.DoWithLock(lockKey, [key]
    {
        string res;
        g_RedisCli.Get(key,&res);
        int a  = atoi(res.c_str());
        a++;
        g_RedisCli.Set(key, to_string(a));
    });
    std::cout << "Over" << std::endl;
};

 注意:这只是使用redis实现分布式锁的基本原理,这种实现仅适用于单机redis,其本身有一些缺点:

一  锁的超时问题 :

客户端1获取锁成功。
客户端1访问共享资源。
客户端1为了释放锁,先执行’GET’操作获取随机字符串的值。
客户端1判断随机字符串的值,与预期的值相等。
客户端1由于某个原因阻塞住了很长时间。
过期时间到了,锁自动释放了。
客户端2获取到了对应同一个资源的锁。
客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

二 主从复制

客户端1从Master获取了锁。
Master宕机了,存储锁的key还没有来得及同步到Slave上。
Slave升级为Master。
客户端2从新的Master获取到了对应同一个资源的锁。

       为了解决上述问题,官方给出了一个算法叫做redlock,client获取当前时间(毫秒数)按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。

      但是这样做给我的感觉就是效率太低,当redis节点越来越多的时候,锁的消耗会越来越大,并且这种实现仍然有缺点,引用Martin Kleppmann的观点,他对锁的用途的区分。他把锁的用途分为两种:

1 为了效率(efficiency),协调各个客户端避免做重复的工作。即使锁偶尔失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。比如重复发送了一封同样的email。
2 为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生,因为一旦发生,就可能意味着数据不一致(inconsistency),数据丢失,文件损坏,或者其它严重的问题。
         最后,Martin得出了如下的结论:如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高。Redlock则是个过重的实现(heavyweight)。如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于timing)。而且,它没有一个机制能够提供fencing token。那应该使用什么技术呢?Martin认为,应该考虑类似Zookeeper的方案,或者支持事务的数据库。

详细的看下面这两篇文章吧:

  基于Redis的分布式锁到底安全吗(上)?

  基于Redis的分布式锁到底安全吗(下)?

阅读更多
换一批

没有更多推荐了,返回首页