转载
一、 锁分类(按照锁的粒度分类)
-
表级锁
Mysql中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单 ,资源消耗也比较少,加锁快,不会出现死锁 。锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
表锁下又分为两种模式:
- 表读锁(Table Read Lock)
- 表写锁(Table Write Lock)
从下图可以清晰看到,在表读锁和表写锁的环境下:读读不阻塞,读写阻塞,写写阻塞!
- 读读不阻塞:当前用户在读数据,其他的用户也在读数据,不会加锁
- 读写阻塞:当前用户在读数据,其他的用户不能修改当前用户读的数据,会加锁!
- 写写阻塞:当前用户在修改数据,其他的用户不能修改当前用户正在修改的数据,会加锁!
- 读锁和写锁是互斥的,读写操作是串行。
- 如果某个进程想要获取读锁,同时另外一个进程想要获取写锁。在mysql里边,写锁是优先于读锁的!
- 写锁和读锁优先级的问题是可以通过参数调节的:
max_write_lock_count
和low-priority-updates
表级锁的优势(与行级锁对比)
- 事务更新大表中的大部分数据直接使用表级锁效率更高;
- 事务比较复杂,使用行级索很可能引起死锁导致回滚。
-
行级锁
Mysql中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 InnoDB支持的行级锁,包括如下几种。
InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB将使用表锁。也就是说,InnoDB的行锁是基于索引的!
InnoDB存储引擎的锁的算法有三种:
- Record Lock: 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
- Gap Lock: 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。
- Next-key Lock: 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。
我们是很少手动加表锁的。表锁对我们程序员来说几乎是透明的,即使InnoDB不走索引,加的表锁也是自动的!我们应该更加关注行锁的内容,因为InnoDB一大特性就是支持行锁!
二 、锁分类(按照是否可写分类)
-
共享锁(s)
共享锁(Share Locks,简记为S)又被称为读锁,其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。
共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
-
排他锁(X)
排它锁((Exclusive lock,简记为X锁))又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。
两者之间的区别:
- 共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不 能加排他锁。获取共享锁的事务只能读数据,不能修改数据。
- 排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
注意:
- 这里的意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正加行锁时才会判断是否冲突。意向锁是InnoDB自动加的,不需要用户干预。
- IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突。
- 意向锁也是数据库隐式帮我们做了,不需要程序员操心!
当一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之如果请求不兼容,则该事物就等待锁释放。
三、乐观锁和悲观锁
事务级别
无论是Read committed
还是Repeatable read
隔离级别,都是为了解决读写冲突的问题。
单纯在Repeatable read
隔离级别下我们来考虑一个问题:
此时,用户李四的操作就丢失掉了:丢失更新:一个事务的更新覆盖了其它事务的更新结果。
解决的方法:
- 使用Serializable隔离级别,事务是串行执行的!
- 乐观锁
- 悲观锁
1.乐观锁是一种思想,具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。
处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,
反之拒绝。之所以叫乐观,因为这个模式没有从数据库加锁,等到更新的时候再判断是否可以更新。
2.悲观锁是数据库层面加锁,都会阻塞去等待锁。
-
悲观锁
所以,按照上面的例子。我们使用悲观锁的话其实很简单(手动加行锁就行了):
select * from xxxx for update
在select 语句后边加了 for update
相当于加了排它锁(写锁),加了写锁以后,其他的事务就不能对它修改了!需要等待当前事务修改完之后才可以修改。
也就是说,如果张三使用select ... for update
,李四就无法对该条记录修改了~
-
乐观锁
乐观锁不是数据库层面上的锁,是需要自己手动去加的锁。一般我们添加一个版本字段来实现:
1)、张三select * from table
--->会查询出记录出来,同时会有一个version字段
2)、李四select * from table
--->会查询出记录出来,同时会有一个version字段
3)、李四对这条记录做修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version}
,判断之前查询到的version与现在的数据的version进行比较,同时会更新version字段
4)、张三也对这条记录修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version}
,但失败了!因为当前数据库中的版本跟查询出来的版本不一致!
间隙锁GAP
当我们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
间隙锁只会在Repeatable read
隔离级别下使用~
例子:假如emp表中只有101条记录,其empid的值分别是1,2,...,100,101
Select * from emp where empid > 100 for update;
上面是一个范围查询,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的有两个:
- 为了防止幻读(上面也说了,
Repeatable read
隔离级别下再通过GAP锁即可避免了幻读) - 满足恢复和复制的需要。MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读
死锁与活锁、饥饿锁
-
饥饿锁
是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......,T2可能永远等待。
-
活锁
活锁:数据资源释放时间不确定,导致某些事务长时间等待,得不到封锁的机会。
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
例如:马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车A和B,A比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去。
解决活锁:先来先服务
-
死锁
死锁:多个事务各自占有部分资源等待另一部分资源,资源需求出现回路,导致事务停顿得不到执行
本质原因:
- 系统资源有限。
- 进程推进顺序不合理。
死锁产生的4个必要条件
资源 | 互斥 | 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。 |
资源 | 不可抢占 | 线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。 |
线程 | 占有且等待 | 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。 |
线程 | 循环等待 | 在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,...pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,于是两个进程就相互等待) |
避免死锁的方法
推荐