锁
按粒度分:行级锁、表级锁、页级锁
按锁的共享策略分:共享锁、排他锁、意向锁、(SqlServer可支持更新锁)
按加锁策略分:乐观锁、悲观锁
(这些也相当于只是类别,里面还有具体的锁,比如记录锁,是行级锁,也是排他锁)
按粒度分
-
行级锁:只对当前操作的行进行加锁。加锁粒度最小,发生锁冲突的概率最低,并发度最高。开销最大,加锁慢,会出现死锁。
-
表级锁:MySQL中锁定粒度最大的一种锁,对当前操作的整张表加锁。开销小,发生锁冲突的概率最高,并发度最低。加锁快,不会出现死锁。
-
页级锁:介于行级锁和表级锁之间,一次锁定相邻的一组记录。开销和加锁时间介于行级锁和表级锁之间,锁定粒度介于行级锁和表级锁之间,并发度一般,会出现死锁。
相对于事务锁(行锁、表锁),页面锁是一个短期持有的锁,而事务锁是长期持有的锁
若仅从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如WEB应用;行级锁更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
(Gap Lock(间隙锁):间隙锁,锁定一个范围,但不包括记录本身(只不过它的锁粒度比记录锁的锁整行更大一些,他是锁住了某个范围内的多个行,包括根本不存在的数据)。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。该锁只会在隔离级别是RR或者以上的级别内存在。间隙锁的目的是为了让其他事务无法在间隙中新增数据。(排它锁))
如果想要在一个表上做大量的 INSERT 和 SELECT 操作,但是并行的插入却不可能时,可以将记录插入到临时表中,然后定期将临时表中的数据更新到实际的表里。
按共享策略分
共享锁(读锁)
select * from ad_plan lock in share mode;
排他锁(写锁)
select * from ad_plan for update;
意向锁
更容易地支持多粒度封锁
在检查封锁冲突的时候,不仅要检查显式的,还要检查隐式的,搜索当前结点,其上级结点(判断该点隐式锁)和下级结点(不然下级隐式的加不下去),如果搜索的其中有一个数据对象已经加了不相容锁,即必须等待
如果对一个结点加意向锁,说明该结点的下层结点正在被加锁。对任意结点加锁时,必须先对其上层结点加意向锁
有意向锁就无需逐一检查下级结点的显式封锁
IS、IX、SIX
如果对一个数据对象加IS锁,表示要对其后裔结点加S锁;若对一个数据对象加SIX锁,表示对它加S锁,再加IX锁
IS、IX锁是表级锁(对整个表加IS和IX锁),它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。就是说当对一个行加锁之后,如果有打算给行所在的表加一个表锁,必须先看看该表的行有没有被加锁,否则就会出现冲突。IS锁和IX锁就避免了判断表中行有没有加锁时对每一行的遍历。直接查看表有没有意向锁就可以知道表中有没有行锁。
注意:如果一个表中有多个行锁,他们都会给表加上意向锁,意向锁和意向锁之间是不会冲突的。
意向锁之间都不会发生冲突,排他锁跟谁都冲突
SqlServer可支持更新锁
更新锁的意思是:“我现在只想读,你们别人也可以读,但我将来可能会做更新操作,我已经获取了从共享锁(用来读)到排他锁(用来更新)的资格”。一个事物只能有一个更新锁获此资格。
T1执行select,加更新锁。
T2运行,准备加更新锁,但发现已经有一个更新锁在那儿了,只好等。
当后来有user3、user4…需要查询table表中的数据时,并不会因为T1的select在执行就被阻塞,照样能查询,提高了效率。
三级封锁协议
1、写之前一定要加X锁,结束才能释放
2、在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。
3、在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
两段锁协议:前段只能加,后段只能放
按加锁策略分
乐观锁
乐观锁就是不加锁,认为对于同一个数据的并发操作,是不会发生修改的(或者增删改少,查多)。
不管资源有没有被别的线程占用,直接取申请操作,如果没有产生冲突,那就操作成功,如果产生冲突,有其他线程已经在使用了,那么就不断地轮询。
乐观的认为,不加锁的并发操作是没有事情的。就是通过记录一个数据历史记录的多个版本,如果修改完之后发现有冲突再将版本返回到没修改的样子,乐观锁就是不加锁。
-
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
-
乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁的思路一般是表中增加版本字段,更新时where语句中增加版本的判断,算是一种CAS(Compare And Swap)操作
(就是检查一下在自己更新之前,数据库里的数据是否已经被其他的更新了,如果已经被更新的话,则自己的数据是过去的了,就是无效的,是版本冲突)
乐观锁一般来说有以下2种方式:
- 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
- 使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
悲观锁
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的(或者增删改多,查少),哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
共享锁、排他锁、意向锁都属于悲观锁
一定会发生并发冲突,屏蔽一切可能违反数据完整性的操作,每次在拿数据的时候都会上锁
悲观锁之所以是悲观,在于他认为本次操作会发生并发冲突,所以一开始就对商品加上锁(SELECT … FOR UPDATE),然后就可以安心的做判断和更新,因为这时候不会有别人更新这条商品库存。
死锁
所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,它们都将无法推进下去
常见情况:
1、事务之间对资源访问顺序的交替
a锁了表1,要表2;b锁了表2,要表1
2、并发修改同一记录
由于两个事务都要转换为排它 (X) 锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。
产生条件(以下四种条件必须同时满足才能产生死锁):(OS)
1、互斥:共享资源同一时刻只能被一个线程占用
2、请求与保持:线程1拿到资源A的锁,不释放A,而去申请资源B的锁
3、不可抢占:线程1拿到对象锁A锁后,其他线程不能强行抢占A锁
4、循环等待:线程1拿到资源A的锁,申请资源B的锁;线程2拿到资源B的锁,申请资源A的锁
预防:一次封锁法(问题:1.扩大封锁范围,降低并发度;2.数据库是变化的,原本不封锁的也可能变封锁)、顺序封锁法(问题:对象很多、动态)、乐观锁
诊断并解除:超时法、事务等待图法
解除:撤销处理死锁代价最小的事务,并释放此事务的所有的锁,使其他事务得以继续运行下去。
常见问题
死锁的概念,避免死锁的具体方法
死锁的类型?
说一下乐观锁和悲观锁,分别是怎么实现的?