锁机制
# 简介
事务并发访问同一数据资源的情况主要就分为读-读、写-写和读-写三种。
读-读 即并发事务同时访问同一行数据记录。由于两个事务都进行只读操作,不会对记录造成任何影响,因此并发读完全允许。
写-写 即并发事务同时修改同一行数据记录。这种情况下可能导致脏写问题,这是任何情况下都不允许发生的,因此只能通过加锁实现,也就是当一个事务需要对某行记录进行修改时,首先会先给这条记录加锁,如果加锁成功则继续执行,否则就排队等待,事务执行完成或回滚会自动释放锁。
读-写 即一个事务进行读取操作,另一个进行写入操作。这种情况下可能会产生脏读、不可重复读、幻读。最好的方案是读操作利用多版本并发控制(MVCC),写操作进行加锁。
可以看到锁主要解决写,写 的场景
# 锁分类
# 从性能上
乐观锁(用版本对比来实现)
不是数据库自带的,需要我们自己去实现。
悲观锁
悲观锁是由数据库自己实现了,就是写锁,for update
# 从对数据库操作(锁的产生)
读锁(共享锁,S锁(Shared)) : 针对同一份数据,多个读操作可以同时进行而不会互相影响
InnoDB通过使用lock in share mode加读锁,但是注意
只锁覆盖索引,可以更新id主键相关的内容写锁(排它锁,X锁(eXclusive)) : 写锁(排它锁,X锁(eXclusive))
InnoDB所有的DML操作默认加写锁。
select可以通过for update加写锁,并且会锁住所有索引,不仅仅是索引覆盖的索引。
所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。但是注意在MyISAM引擎中因为读也会自动加上读锁,所以使用排它锁的时候是不能去读的,这点要和InnoDB区别开
读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞
# 从对数据操作的粒度
# 全局锁
全局锁即对整个数据库实例加锁,使得整个库处于只读状态,会阻塞DML和DDL语句。
# 表锁
- 每次操作锁住整张表,一般用在整表数据迁移的场景。
- 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
# 操作
- 手动增加表锁 :
lock table 表名称 read(write),表名称2 read(write); - 查看表上加过的锁:
show open tables; - 删除表锁 :
unlock tables;
# 行锁
- 每次操作锁住一行数据。
- 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
- InnoDB支持行级锁,MYISAM不支持
# 注意:
行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。
# 操作
参考读写锁的操作。
# 元数据锁
元数据锁(meta data lock 简称MDL) 用于确保事务执行过程中表结构的稳定。
# 间隙锁(Gap Lock)
- 其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据(锁的操作都不能.)
- 锁的就是两个值之间的空隙,中访问到的对象才会加锁。
- 间隙锁是在可重复读隔离级别下才会生效。
- 左开右开区间
# 临键锁(Next-key Locks)
- 行锁与间隙锁的组合
- 它的封锁范围,既包含索引记录,又包含索引区间。
- 可以解决幻读的问题。
- 在可重复读隔离级别下才会生效。
- 临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
- 左开右闭区间
# 总结:
以下都是针对锁的前提下:
1、当使用唯一索引来等值查询的语句时, 如果这行数据存在,不产生间隙锁,而是记录锁。
2、当使用唯一索引来等值查询的语句时, 如果这行数据不存在,会产生间隙锁。
3、当使用唯一索引来范围查询的语句时,对于满足查询条件但不存在的数据产生间隙(gap)锁,如果查询存在的记录就会产生记录锁,加在一起就是临键锁(next-key)锁。
4、当使用普通索引不管是锁住单条,还是多条记录,都会产生间隙锁; 没有查询到没有影响。
5、在没有索引上不管是锁住单条,还是多条记录,还是没有查询到,都会产生表锁;
# 死锁问题
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
# 排查
- 查看事务:: select * from INFORMATION_SCHEMA.INNODB_TRX;
- 查看锁 :: select * from INFORMATION_SCHEMA.INNODB_LOCKS;
- 锁等待:: select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
- kill trx_mysql_thread_id
- 查看锁等待详细信息::: show engine innodb status\G;
# 解决机制
超时
直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
死锁主动检查
发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on,表示开启死锁检测。
注意,死锁检查会消耗额外资源,若并发的线过多,则有可能由死锁检测而消耗过多资源而导数据库崩溃。
# 锁优化建议
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 尽可能减少检索条件范围,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
- 尽可能低级别事务隔离
# 存储引擎中锁的区别
# MyISAM
MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自 动给涉及的表加写锁。
# InnoDB
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行 锁。
# 参考资料
MySQL锁的浅析到深入 (opens new window)
共享锁,只锁覆盖索引的详细解释。
mysql记录锁(record lock),间隙锁(gap lock),Next-key锁(Next-key lock) (opens new window)
mysql行锁升级为表锁_MySQL 全局锁、表锁以及行锁 (opens new window)
InnoDB行锁升级为表锁的问题 (opens new window)
mysql的锁机制(读锁,写锁,表锁,行锁,悲观锁,乐观锁,间隙锁) (opens new window)
MySQL记录锁、间隙锁、临键锁小案例演示 (opens new window)
mysql事务和锁,一次性讲清楚! (opens new window)
什么时候数据库行锁会升级为表锁? (opens new window)
事务的隔离级别为:可重复读 (opens new window)时,如果有索引 (opens new window)(包括主键索引),以索引列为条件更新数据,会存在间隙锁,行锁,页锁,而锁住一些行。如果没有索引,更新数据时会锁住整张表。
事务隔离级别为:串行化时,读写数据都会锁住整张表。(一次只能一个连接玩表)
insert 和 delete 同时操作一段间隙,就会导致一个等锁。
解决办法: 修改代码逻辑, 存在才删除,尽量不去删除不存在的记录。
mysql排它锁(FOR UPDATE) 场景介绍 (opens new window)
- 当前使用for UPDATE (opens new window)查询,其他地方查询 -- 其他地方也使用for UPDATE会堵塞, 其他地方未使用for UPDATE不会堵塞
- 当前使用for UPDATE查询,其他地方UPDATE更新 -- 会堵塞等待
- 当前事务使用UPDATE更新, 其他地方使用for UPDATE查询 -- 会堵塞等待
- 当前非事务使用UPDATE更新, 其他地方使用for UPDATE查询 -- 不会堵塞
- 当前普通查询,其他地方使用for UPDATE查询 -- 不影响,不会堵塞
总结: for UPDATE 和 update等操作 都是加锁的,select 是不加锁的,所以不会影响。
MySQL-乐观锁 (opens new window) 和 MySQL 技巧:如何实现乐观锁? (opens new window)
1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
通过乐观锁+自旋的方式,解决数据更新的线程安全问题,而且锁粒度比悲观锁低,并发性能好。