排它锁
(简称X锁),又称为写锁或独占锁,是一种基本的锁类型。如果事务T1对数据对象O1加上了排它锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其它任何事务都帮你再对这个数据对象进行任何类型的操作,——直到T1释放了排它锁。
从上边讲解的排它锁的基本概念中,我们可以看到,排它锁的核心是如何保证当前有且仅有一个事务获得锁,并且锁被释放后,所有等待获取锁的事务都能被通知到。
共享锁
(简称S锁),又称为读锁,同样是一种基本的锁类型,如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其它事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。
共享锁和排它锁最根本的区别在于,加上排它锁后,数据对象只对一个事务可见,而加上共享锁后,数据对所有的事务可见。
在zookeeper中,对于共享锁的使用和在SQL中略有不同。
在需要获取共享锁时,所有的客户端都会到/shared-lock这个节点下边创建一个临时数据节点,如果当前请求是读请求,那么在命名临时节点的时候要表明是读的,如果是写请求,在创建节点命名时要表明是写的。
根据共享锁的定义,不同的事务都可以同时对同一个数据对象进行读取操作,而更新操作必须在当前没有任何进行读写操作的情况下进行的。基于这个原则,下边简述如何通过zookeeper的节点来确定分布式读写顺序,大致可以分为四步。
1. 创建完节点后,获取共享锁下边的所有子节点,并且对该节点注册子节点变更的Wacher监听。
2. 确定自己的节点序号在所有子节点中的顺序。
3. 对于读请求:
如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读请求,那么表明自己已经成功的获取到了共享锁,同时开始执行读取逻辑。
如果比自己序号小的子节点中有写请求,那么就需要进入等待。
对于写请求:
如果自己不是序号最小的子节点,那么就需要进入等待。
4.接收到Wacher通知后,重复步骤1。
但是这种情况会带来羊群效应(惊群效应)。
改进: 创建完节点后,获取共享锁下边的所有子节点,不在对该节点注册子节点变更的Wacher监听。
对于读请求:向比自己序号小的最后一个写请求节点注册wacher监听。
对于写请求:向比自己序号小的最后一个节点注册Wacher监听。
悲观锁
又被称作悲观并发控制(PCC),是数据库中一种非常典型且非常严格的并发控制策略,悲观锁有强烈的独占和排它性,能够有效的避免不同事务对同一数据并发更新而造成的数据一致性问题。在悲观锁的实现原理中,如果一个事务(假定是事务A)正在对数据进行处理,那么在整个数据处理过程中,都会将数据处于锁定状态。在这期间,其他事务无法对这个数据进行更新操作,直到事务A完成对该数据的处理,释放了对应的锁之后,其他事务才能够重新竞争来对数据进行更新操作。
乐观锁
又被称作乐观并发控制(OCC),也是一中常见的乐观并发控制策略。相对于悲观锁而言,乐观锁的机制显得更加友好与宽松。从上边的介绍可以看到,悲观锁假定不同的事务之间的处理一定会出现相互干扰,从而需要在一个事务从头到尾的过程中对数据进行加锁处理。而乐观锁正好相反,它假定多个事务在处理的过程中不会彼此影响,因此在事务处理的绝大部分时间里不需要进行加锁处理。当然,既然有并发,就一定存在数据更新冲突的可能。在乐观锁机制中,在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否有其他事务对该数据进行了更改。如果其他事务有更新的话,那么正在提交的事务就会回滚。乐观锁适合使用在数据迸发竞争不大、事务冲突较少的应用场景中。
其实,我们可以把一个乐观锁控制的事务分成三个阶段:数据读取、写入校验和数据写入。其中写入校验是整个乐观锁控制的关键所在。在写入校验阶段(根据版本号去判断),事务会首先检查数据在读取阶段后是否有其他事务对数据进行了更新,以保证数据更新的一致性。
乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行读取后,再去判断是否有冲突。
共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。