当一个事务锁住了一行数据的时候,其他的事务不能操作这一行数据,那它到底是锁住了这一行数据,还是锁住了这一个字段,还是锁住了别的什么东西呢?
注意:虽然InnoDB默认的是行锁,但是行锁是实现在索引上的,而不是锁在物理行记录上。所以,即使表设置了索引,但是如果访问没有命中索引,也无法使用行锁,将要退化为表锁。
测试
首先我们准备三张表,一张没有索引的 test1,一张有主键索引的 test2,一张有唯一索引的 test3。且表中均只有int 类型的 id 和 varchar 类型的 name,一共四条数据,id为1-4。
没有索引的 test1测试
Transaction1 | Transaction2 |
---|---|
begin; SELECT * FROM test1 WHERE id =1 FOR UPDATE; | |
select * from test1 where id=3 for update; – 阻塞 INSERT INTO test1 (id , name ) VALUES (5, ‘5’);– 阻塞 |
如上表,现在我们在两个会话里面手工开启两个事务。 在第一个事务里面,我们通过 where id =1 锁住第一行数据。 在第二个事务里面,我们尝试给 id=3 的这一行数据加锁。
但是真正执行的时候会发现,事物2这个加锁的操作被阻塞了。这就有点奇怪了,第一个事务锁住了 id=1 的这行数据,为什么我不能操作 id=3 的数据呢? 而且插入 id=5。它也被阻塞了。实际上这里整张表都被锁住了,所以InnoDB 的锁锁住的应该不是记录。
主键索引的 test2测试
Transaction1 | Transaction2 |
---|---|
begin; SELECT * FROM test2 WHERE id =1 FOR UPDATE; | |
select * from test2 where id=1 for update; – 阻塞 select * from test2 where id=4 for update;– 成功 |
第一种情况,使用相同的 id 值去加锁,冲突;使用不同的 id 加锁,可以加锁成功。那么,既然不是锁定一行数据,有没有可能是锁住了 id 的这个字段呢?
唯一索引的 test3测试(name 上创建了一个唯一索引)
Transaction1 | Transaction2 |
---|---|
begin; SELECT * FROM test3 WHERE name= ‘4’ FOR UPDATE; | |
select * from test3 where name= ‘4’ for update; – 阻塞 select * from test3 where id=4 for update;– 阻塞 |
在第一个事务里面,我们通过 name 字段去锁定值是 4 的这行数据。
在第二个事务里面,尝试获取一样的排它锁,肯定是失败的,这个不用怀疑。 在这里我们怀疑 InnoDB 锁住的是字段,所以这次我换一个字段,用 id=4 去给这行数据加锁,但还是被阻塞了。说明锁住的是字段的这个推测也是错的,否则就不会出现第一个事务锁住了 name,第二个字段锁住 id 失败的情况。
既然锁住的不是 记录,也不是 行,InnoDB 里面锁住的到底是什么呢?通过案例我们知道这三张表的结构区别就在于索引,而InnoDB 的行锁,就是通过锁住索引来实现的。
分析
那么我们还有两个问题没有解决:
为什么表里面没有索引的时候,锁住一行数据会导致锁表? 或者说,如果锁住的是索引,一张表没有索引怎么办? 所以,一张表有没有可能没有索引?
1)如果我们定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键作为聚集索引。
2)如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。
3)如果也没有这样的唯一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 作为隐藏的聚集索引,它会随着行记录的写入而主键递增。
所以,为什么锁表,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了。
为什么通过唯一索引给数据行加锁,主键索引也会被锁住?
在 InnoDB 里面,在辅助索引里面,索引存储的是二级索引和主键的值。比如name=4,存储的是name的索引和主键 id 的值 4。 而主键索引里面除了索引之外,还存储了完整的数据。所以我们通过辅助索引锁定一行数据的时候,它跟我们检索数据的步骤是一样的,会通过主键值找到主键索引,然后也锁定。