@adamhand
2019-03-01T10:12:14.000000Z
字数 2762
阅读 1313
行锁就是针对数据表中行记录的锁。行锁比表锁的粒度小,发生锁争用的可能小,并发度高。但行锁比表锁开销大,因为锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。
两阶段锁协(2-phase-lock,2PL
)议如下所述:
事务分两个阶段,第一个阶段是获得封锁。事务可以获得任何数据项上的任何类型的锁,但是不能释放;第二阶段是释放封锁,事务可以释放任何数据项上的任何类型的锁,但不能申请。(即在某段时间只能申请锁,在某段时间只能释放锁)
两阶段锁示意图如下所示:
那么,如何判断什么时候该加锁,什么时候该释放锁呢?在工程实践中,一般遵守下面的协议S2PL(Strict-2PL)
:
select for update
、lock in share model
)时)示意图如下:
引入2PL是为了保证事务的隔离性,即多个事务在并发的情况下等同于串行的执行。 在数学上证明了如下的封锁定理:
如果事务是良构的且是两阶段的,那么任何一个合法的调度都是隔离的。
具体的数学推到过程可以参照《事务处理:概念与技术》这本书的7.5.8.2
节。
知道了两阶段锁,需要注意到 是:如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
例子如下:
顾客 A 和顾客C要在影院 B 购买电影票,这个买票过程可以简化为以下三个步骤:
也就是说,要完成这个交易,需要 update
两条记录,并 insert
一条记录。为了保证交易的原子性,要把这三个操作放在一个事务中。那么,应该怎样安排这三个语句在事务中的顺序呢?
根据两阶段锁协议,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果把语句 2 安排在最后,比如按照 3、1、2
这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度。
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
下面是一个例子:
这时候,事务 A 在等待事务 B 释放 id=2 的行锁,而事务 B 在等待事务 A 释放 id=1 的行锁。 事务 A 和事务 B 在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有两种策略:
innodb_lock_wait_timeout
来设置。innodb_deadlock_detect
设置为 on,表示开启这个逻辑。在 InnoDB 中,innodb_lock_wait_timeout
的默认值是 50s,这个等待时间长得无法接受;但是如果将这个参数设置得过小,那么InnoDB可能就分不清锁等待和死锁。所以,第二种策略是比较常用的:主动监测死锁。
但是第二种策略也会面临一个问题:死锁检测可能会极大耗费CPU资源。死锁检测的一般过程是:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
如果上述过程作用在同一行,每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。即使没有发生死锁,由于CPU消耗极大,MySQL的效率也会极低。
怎么解决由这种热点行更新导致的性能问题呢?
一种头痛医头的方法,就是如果能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。但是这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是业务无损的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损的。
另一个思路是控制并发度。但是如果在客户端做并发控制,如果客户端很多,即使每个客户端并发量很小,MySQL服务器的并发量也会很大。因此,这个并发控制要做在数据库服务端。如果有中间件,可以考虑在中间件实现;如果团队有能修改 MySQL 源码的人,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。
如果上述条件都没有,可以考虑从设计上改善。通过将一行改成逻辑上的多行来减少锁冲突。还是以影院账户为例,可以考虑放在多条记录上,比如 10 个记录,影院的账户总额等于这 10 个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的 1/10,可以减少锁等待个数,也就减少了死锁检测的 CPU 消耗。
InnoDB行锁是通过给索引上的索引项加锁来实现的。所以,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
对于UPDATE
、DELETE
和INSERT
语句,InnoDB
会自动给涉及数据集加排他锁(X);
对于普通SELECT
语句,InnoDB
不会加任何锁;但是可以通过以下两种范式显示加锁:
S
):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
X
) :SELECT * FROM table_name WHERE ... FOR UPDATE
锁和隔离级别的关系是什么?加锁的过程其实比较复杂,所以MySQL为程序员提供了四种隔离界别,隔离级别的底层是靠加锁来实现的,使用隔离级别时,MySQL会自动加锁和释放锁,这样就屏蔽了底层的加锁细节,简化了应用。所以说,加锁是实现隔离级别的一种方式。之所以说是一种,是因为还有其他方式比如MVCC来实现隔离级别。“读提交”和“可重复读”就是由MVCC来实现的。
那么,各种隔离级别下的加锁情况是怎样的?(先说结论吧,有待验证一下)