常见的锁:互斥锁、自旋锁、原子变量、读写锁、条件变量、信号量。
IPC(进程间通信的方式):pipe、FIFO、信号量、消息队列、共性内存、socket、信号。
(拓展)虚假唤醒解决:把pthread_cond_wait放在while循环体里面,循环里判断condition是否满足。由此还能解决信号劫持的问题,比如线程池里只有一个任务,但是signal唤醒了多个消费者线程,于是需要加入判断,只能有一个线程得到满足。
分布式锁:在分布式场景中使用的锁。操作系统并没有提供分布式锁,只能由用户自行实现。这个锁是抽象的锁,并不是真正的锁。
分布式场景:执行程序、存储等分布在多个节点(节点通常是独立的计算机或设备等)的场景。
分布式场景的特点:执行体分布在不同的节点上,一个节点至少是一个进程。
分布式场景与锁简化模型:
如图,如果s1\s2\s3\s4都想去操作DB(数据库),但是DB同一时间最多只允许一个节点操作,那么需要设置一个锁(lock)放在s1\s2\s3\s4均能取到的地方(比如数据库,有可能和要操作的数据库放在了一起,不过在不同的表里),谁能拿到锁谁就能去操作数据。
分布式锁的构成:资源+行为。
资源:是获取到锁的节点的id(比如s1的id)。
行为:加锁和解锁,分布式场景中加锁和解锁的方式都通过网络通信的方式实现。
加锁:原来没有标记,获取到锁之后打上标记。
解锁:持锁方取出标记。
特性:
锁超时:对持锁方能持有锁的最长时间有限制,如果持锁方崩坏,不至于让其他节点也获取不到资源。
可用性和容错性其实差不多,只不过容错性更侧重数据一致性,可用性更侧重共享节点崩坏的处理。
分布式锁的类型:
重入锁:允许线程多次持有锁。
公平锁:对应互斥锁。拿不到互斥锁的线程进入阻塞态,id进入阻塞队列。
非公平锁:对应自旋锁。拿不到自旋锁的线程进入阻塞态,但是忙等待。
需要基于中间件来实现,资源存储在中间件当中,加锁解锁行为基于中间件的特性来实现。
以下分别以mysql和redis为例来实现分布式锁。
首先需要建立在mysql数据库里建立用于存储锁的table,加锁的时候还要记录加锁的时间,因此设置了一个字段记录锁建立的时间。锁类型唯一,说明只能被获取一次,注意:这里的锁类型不是互斥锁、自旋锁等,而是活动1的锁、活动2的锁等。
用户节点加锁。用户节点会将以下sql语句发送到mysql数据库进行加锁操作,如果有多个用户节点,那么先发送到的节点先获取到锁,后发送到的节点会获得操作失败的信息。因为lock_type唯一,因此只有一个节点能获取到锁,获取到锁的时候数据还有记录获取到了锁的owner_id。。
用户节点解锁。解锁的时候owner_id要和之前加锁的时候的记录匹配才能解锁。
其他想获取锁但是没能获取到锁的用户节点,只能每隔一段尝试向mysql去获取一次(因为mysql不会主动通知解锁操作),看看有没有解锁。因此mysql只能实现非公平锁。
Mysql还存在锁超时问题解决,即多起一个进程去检测锁从更新时间已经过了多久了,超时了就解锁。
因此我们发现mysql实现分布式锁有这些缺点:
Redis的特性:
因为redis是一句数据库,启动了之后,会进入redis服务器的运行态,在这上面可以数据redis的操作命令,redis有自己的原语操作命令。
Redis收到以下命令,说明在完成相应的锁操作:
Setnx lock1 user1:用户1尝试抢锁并进行加锁(setnx:set no lock)。
Get lock1:某个用户在尝试看看是谁持有lock1。
Del lock1:如果用户发现锁的持有者是自己,那么可以进行解锁。
注意:get lock1和del lock1需要实现原子性,可以用redis的lua脚本实现。
Set lock user1 nx ex 20:Setnx lock1 user1的拓展版,多了20s之后会自动删除记录的功能,实现锁超时功能。
Ttl lock1:看看lock1还有多久被删除。
用户节点向redis发送命令,redis去执行lua脚本。脚本的具体实现不用被用户知道,但此处进行一下redis的lua脚本的大致解析。
加锁:
先判断锁是否存在。如果不存在则设置锁与超时时间,如果存在则进入监听状态,监听未来释放锁的广播。
解锁:
解锁成功的时候要进行广播,告知给正在监听的用户。
注意防范锁不存在、锁与用户不匹配等问题。如果出现了这些情况那么直接return。