首先你要知道:仅仅一篇文章是无法全面讲清楚明白mysql里面的加锁机制的!
虽然网上有很多文章都号称“理解mysql里的加锁机制看这一篇就够了”,实际上都只是理论方面到位,工程实践方面则都所不足。
本文针对工程实践方面经常会碰到的一些关于锁的问题进行剖析:
1.锁的类型:
经常碰到的主要是行锁(record lock),间隙锁(gap lock)和(next-key 锁)。
- 行锁表示锁住了某一行记录
- 间隙锁表示锁定了一个区间,在这个区间不能新增记录。在一个区间不能新增记录就避免了所谓幻读的问题,叫区间锁好像更好听点,所以下面就叫区间锁好了。注意:区间锁不会对区间两边的记录加锁,只对区间内部加锁。
- next-key锁可以看成是以上两者的结合,表示既对记录加了行锁又在记录的前面加上了区间锁,实际上底层也是这样实现的。
举个例子:
select * from t where id=100 for update
或者
update t set name=? where id=100
上面两种类型的sql语句的加锁方式是一致的:
- 如果id为唯一索引,100这条记录存在,则会对这条记录加上行锁。
- 如果id为唯一或非唯一索引,100这条记录不存在,则会加上区间锁,具体的区间为小于100的第一条记录到大于100的第一条记录之间的区间。
- 如果id是非唯一索引,100这条记录存在,则会加上next-key lock,既锁定100这条记录,又锁定小于100的第一条记录到100之间的区间。
另外:如果查询条件是大于或小于某个值,也即查询的结果预期是一个区间时也会加区间锁。
我们这可以这样来分析:
- 如果查询条件命中了一个区间则会加上区间锁
- 如果只是精确命中了记录则给记录加上行锁(前提条件:查询条件为唯一索引)
- 区间锁是为了防止加锁期间其他事务在区间新增记录而设计的,所以,如果查询条件是非唯一索引时且命中记录时会加next-key锁,它相当于包含了一个区间锁:锁定命中的记录和记录之前的区间。有了区间锁,其他事务就不能在区间内新增记录从而产生幻读了。
2.共享和排他锁:
共享锁就不讲了,平时常用的主要是排他锁,即一个会话对某行记录加了排他锁,则其他会话就不能再获得这把锁。
但是在mysql里面:区间锁相互之间是不互斥的,也就是不同会话可以同时给同一个区间加锁!
这样一来,就会产生以下几种情况:
- 如果对方获得了行锁,则你只能等它释放。
- 如果对方获得了区间锁,则你也可以获取相同的区间锁。
- 如果对方获得了next-key锁,则表示对方获得了行锁和区间锁,这时你不能获取对方持有的行锁,但可以获取对方持有的区间锁。
所以,要不要等待对方的锁,就看你当前申请的锁是否包含对方持有的行锁,如果有则需要等待,否则不需要。
3.死锁问题
一般来说,如果所有事务都保持一致的加锁顺序,则不容易产生死锁。但是不容易产生,不代表不会产生,在一些特殊的情况下还是有可能碰到的!
这是因为在mysql中有这样一个奇葩的机制:
一个会话在申请锁的时候如果需要等待另一方释放锁,则这个申请会进入锁申请队列,这时如果另一方接下来需要申请的锁与申请队列中的锁有冲突,则会产生死锁!
比如有这样两个会话:
会话1:
select * from t where id=100 for update //id是唯一索引且记录存在,这里产生行锁
会话2:
select * from t where id=100 for update //id是唯一索引且记录存在,因为会话1持有行锁,会话2只能等待,该申请会进入锁申请队列。
这时如果会话1再执行:
update t set name=xxx where id>90
此处查询条件的结果是一个区间,于是产生了区间锁,这个锁与申请队列中的会话2申请的锁有冲突,就产生的死锁!
是不是很奇葩?会话2还没有持有任何锁,仅仅是申请了锁而已,就产生了死锁。mysql大概是这样判断的:
- 会话1申请申请区间锁。
- 因为队列里有其他申请,于是先跟其他申请做下比较,发现会话2已经先申请了id=100这条记录的行锁且处于区间中,所以它认为会话1申请在后需要等待会话2。
- 这时为避免死锁再去检查会话2申请的锁是否被会话1持有
- 不检查不知道,一检查吓一跳:会话1持有会话2需要的锁,而在第二步时会话1当前又需要等待会话1!
- 结论:有死锁,选择牺牲会话2!