MySQL—事务和锁

MySQL锁

和其他数据库相比,MySQL的锁机制比较假单,不同的引擎支持不同的锁机制。MyISAM和MEMORY使用表级锁,BDB使用页面锁和表级锁;InnoDB默认支持行级锁,也支持表级锁。

  • 表级锁:开销小,加锁块,不会出现死锁;锁颗粒度大,容易出现锁冲突,并发级数小。
  • 行级锁:开销大,加锁慢,会出现死锁;锁颗粒度小,不容易出现锁冲突,并发级数大。
  • 页面锁:介于表级锁和行级锁之间。

MyISAM表锁

MyISAM表锁有两中,一个是都锁,一个是写锁,兼容性如下:

模式 读锁 写锁
读锁 兼容 不兼容
写锁 不兼容 不兼容

可见MyISAM对表的读取不会造成其他用户对表的读取,但会阻塞其他用的写。因此,除了读-读操作,其他对数据的操作都是串行的。

MyISAM在执行SELECT前,会自动的给涉及到的所有表加读锁,使用跟新操作前都会自动的加写锁,这些过程不需要用户干预。

并发插入:在一定程度上,MyISAM也支持查询和插入操作的并发进行。MyISAM中有一个系统变量concurrent_insert,专门控制并发插入,可以取值为0、1、2。

  • 设置为0时:表示不允许并发插入。
  • 设置为1时:表示如果表中没有空洞(中间没有被删除的行),MyISAM允许在表尾插入记录。
  • 设置为2时:表示无论是否有空洞,都允许在表尾插入数据。
    可以使用MyISAM的并发插入特性来解决应用中对同一个表查询和插入的锁竞争。

MyISAM锁调度

对于同时存在读写操作竞争锁的时候,默认情况下MyISAM中写进程会先获得锁,也就是说MyISAM认为写请求比读请求更加重要。这也就解释了MyISAM不适合有大量跟新操作的原因,因为大量的更新操作会使得读线程很难获得锁,从而造成永远的阻塞。我们通过一些设置来改变这种情况。

  1. 指定启动参数low-priority-updates,使得读请求有优先的权利。
  2. 执行SET LOW_PRIORITY_UPDATES=1.
  3. 指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性。

InnoDB事务

InnoDB是支持事务的,这是和MyISAM最大的不同。

事务以及ACID属性:

  • 原子性Atomicity:事务是原子操作单元,对数据的修改要么全都正确执行,要么全都不执行。

  • 一致性Consistent:事务开始前和事务结束后,数据库的完整性约束没有被破坏。

  • 隔离性Isolation:事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。

  • 持久性Durable:事务结束后,事务的结果必须是永久保存的。


事务并发处理带来的问题

  • 更新丢失:当数据库没有加任何锁操作的情况下,有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。

  • 脏读:一个事务读到另一个尚未提交的事务中的数据。

  • 不可重复读书:同一个事务中在不同的时间读取同一个数据,结果不同。

  • 幻读:一个事务按照相同查询条件查询之前检索过的数据,却发现多了满足查询条件的新数据。

事务隔离级别

1.Read uncommitted未提交读:从名字可以知道,在该级别下,一个事务对一行数据修改的过程中,允许另一个事务对该行数据进行修改。
2. Read committed已提交读:在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行未提交数据进行修改,但允许另一个事务对该行数据读。 虽然解决了脏读,但是会出现不可重复读和幻读。
3. Repeatable read可重复读:在该级别下,读事务禁止写事务,但允许读事务,因此解决了不可重复读,且写事务禁止其他一切事务。
4. Serializable 序列化:最高的隔离级别,该级别下,读事务禁止写事务,但允许读事务,且写事务禁止其他一切事务。

数据库实现事务隔离机制的方法主要有两种

  • 读取数据之前为数据加上锁,防止其他事务对数据的修改。

  • 不加任何锁,通过一定机制生成一个数据请求时间点的一致数据快照,用其提供一定级别的一致性读取,就好像数据库可以提供多版本的数据一样。于是这种技术就叫数据多版本并发控制简称MVCC,也称为多版本数据库。


InnoDB锁

InnoDB中默认的是行级锁,和MyISAM一样有两种锁:

  • 共享锁(S):允许多个事务读取一行,阻止其他事务获得相同数据级的排他锁。
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据的排他锁和共享锁。
    兼容情况如下:
锁模式 X S
X 冲突 冲突
S 冲突 兼容

对于UPDATE、INSERT、DELETE语句,InnoDB会自动的加上X锁;一般的SELECT语句不会加锁,除非显示的给记录集加上锁。如果取得数据的共享锁以后,还要进行更新,就会造成死锁(后面会讲到);因此如果要对数据要进行更新就直接获得排它锁即可。

InnoDB行锁实现的方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,从而间接对一行数据集上锁,如果没有索引,那么就会通过隐藏的聚集索引来对记录上锁。行锁有三种:

  • Record lock:对索引加锁。
  • Gap lock:对索引的间隙加锁。
  • Next-key lock:上面两者的组合。

这种加锁机制表名,如果不通过索引条件查询数据,那么InnoDB将会对表中所有数据记录加锁,这就和表锁的实际效果一样了。因此需要注意如下情况:

  1. 不通过索引查询数据,InnoDB会锁定整个表记录。
  2. 如果不同事务使用相同的索引键则会出现锁冲突。
  3. 表有多个索引时候,不同事务使用不同的索引锁定不同的行,无论使用哪种索引,都会锁定数据。
    事务1查询 select * from table where id = 1 for update;
    结果id :1 name :1
    id:1 name:4
    事务2查询 select * from table where name= 4 for update ;
    会造成阻塞,知道事务1释放锁。
  4. 即便是在条件使用了索引字段,MySQL也不一定会使用索引(上一篇讲索引时指出了原因),因此分析锁冲突是需要确认是否真正使用了索引。

NEXT-KEY 锁

对于键值在条件范围以内但并不存在记录的叫做“间隙”,InnoDB会对这个间隙加锁,这就是所谓的Next-Key锁。

select * from table where id>100 for update;

对于上述语句,InnoDB不仅会对id位为101记录上锁,而且还会对大于101的记录上锁,虽然这些记录并不存在。InnoDB使用NEXT-KEY 锁的好处就是防止了幻读的出现,以满足相关隔离级别的要求。虽然InnoDB默认的隔离级别是RR(可重复读),但是防止了幻读的出现,实际上达到了Serializable级别。

很显然,这也会带来一个问题:在使用范围查找时,InnoDB这种加锁机制会造成严重的锁等待,所以在实际开发中需要优化业务逻辑,避免出现范围条件。

关于死锁

当两个事务都需要获得对方的排他锁时,会出现循环等待,也就是“死锁”。对于InnoDB,死锁都能自动检测到,并使得一个事务回滚并释放锁。
避免死锁的办法:
1.设置合适的锁等待时间,一旦超过这个时间阈,就会自动回滚并释放锁。
2.尽量约定以相同的顺序来访问表,这就会大大降低产生死锁的机会。
3.在事务中,申请足够级别的排他锁,如:需要更新数据就直接申请排它锁。
4.死锁很难避免,因此需要在程序设计中捕获并处理死锁异常是一个好的习惯。

总结:

  1. MyISAM锁的特点?以及锁的调度机制?
  2. InnoDB的行锁是如何实现的?
  3. InnoDB为什么在RR级别下解决了幻读的出现?
  4. InnoDB如何减少死锁的发生?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章