MySQL的insert into导致锁表_加锁


MySQL锁概述

相对其他数据库而言,MySQL的锁机制比较简单,其最 显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有 并发查询的应用,如一些在线事务处理(OLTP)系统。这里先介绍MYISAM引擎的表锁。

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。

共享读锁(Table Read Lock)

对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;也即当一个session给表加读锁,其他session也可以继续读取该表,但所有更新、删除和插入将会阻塞,直到将表解锁。下面是具体步骤:

session1给myisam_lock表加读锁:


MySQL的insert into导致锁表_读锁_02


session2可以照常读取myisam_lock表:


MySQL的insert into导致锁表_读锁_03


session2执行insert语句,被阻塞:


MySQL的insert into导致锁表_加锁_04


session1解锁myisam_lock表:


MySQL的insert into导致锁表_读锁_05


session2中被阻塞的insert操作成功执行:


MySQL的insert into导致锁表_读锁_06


MyISAM引擎在执行select时会自动给相关表加读锁,在执行update、delete和insert时会自动给相关表加写锁。而InnoDB则加行锁。

表级读锁有几点需要特别注意的地方:

  • lock表的时候一定要把所有需要访问的表都锁住,因为锁表之后无法访问其他未加锁的表。(InnoDB一样)
  • 当前session lock表之后,当前session只能读锁住的表,而无法对其进行update、delete和insert操作。(InnoDB一样)


MySQL的insert into导致锁表_MySQL_07


  • 同一个表如果在sql语句里面如果出现了N次,那么就要锁定N次,否则会出错。(InnoDB一样)


MySQL的insert into导致锁表_MySQL_08


MySQL的insert into导致锁表_加锁_09


独占写锁(Table Write Lock)

MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的。也即当一个session给表加写锁,其他session所有读取、更新、删除和插入将会阻塞,直到将表解锁。下面是具体步骤:

session1给myisam_lock表加写锁:


MySQL的insert into导致锁表_读锁_10


session2对myisam_lock表的查询被阻塞:


MySQL的insert into导致锁表_加锁_11


session1解锁myisam_lock表,session2查询出结果:


MySQL的insert into导致锁表_加锁_12


lock表的时候一定要把所有需要访问的表都锁住,因为锁表之后无法访问其他未加锁的表。

并发插入(Concurrent Inserts)

上面我们说到只要给一个表加了读锁,其他session对该表的写操作将被阻塞。那么有没有办法让其他session也能往里面添加数据呢?

这里我们可以使用local关键字,语法如下:

lock table 表名 read local。

这样在当前表被加读锁的时候,可以让其他session往表里添加记录,但需要配合concurrent_insert全局变量使用。

concurrent_insert属性有三种取值,分别是NEVER(0)、AUTO(1)和ALWAYS(2),从5.5.3版本开始concurrent_insert参数用枚举值,以前的版本则直接使用对应的数字。他们的含义如下:

NEVER:加读锁后,不允许其他session并发写入。

AUTO:加读锁后,在表里没有空洞(就是没有删除过行)的条件下,允许其他session并发写入。

ALWAYS:加读锁后,允许其他session并发写入。

通过show global variables like '%concurrent_insert%'命令可以查看当前数据库的设置:


MySQL的insert into导致锁表_读锁_13


通过set global concurrent_insert = ALWAYS命令可以改变数据库设置:


MySQL的insert into导致锁表_加锁_14


下面我们在AUTO的条件下进行试验,首先session1用local方式给myisam_lock表加读锁:


MySQL的insert into导致锁表_MySQL_15


session2可以正常读取,还可以插入数据:


MySQL的insert into导致锁表_加锁_16


但session2插入的数据对session1是不可见的,必须等session1释放锁之后才可见:


MySQL的insert into导致锁表_MySQL_17


MyISAM引擎锁的调度机制

MyISAM引擎默认是write Lock优先于read Lock的,也就是说如果一堆写请求和一堆读请求同时要一张表的锁,那读请求只能在所有的写请求执行完成后才能获得执行机会。这样就会出现一个很大的问题:如果我们在批量更新一张用户表,那么用户登录操作可能会出现长时间阻塞的情况,因为用户登录的读取操作在更新完之前无法访问用户表。

所以MyISAM最好不要用在那些更新和读取都非常频繁的表里,会造成读取的长时间阻塞。但我们可以用下面的方法来缓解这类问题:

①使用LOW_PRIORITY、HIGH_PRIORITY和DELAYED关键字。

语法为:insert [LOW_PRIORITY | HIGH_PRIORITY | DELAYED] into 表名 ...

执行delete、insert、update、load data和replace的时候可以使用LOW_PRIORITY来降低该更新语句的优先级,让读取操作能够执行。

执行select和insert的时候可以使用HIGH_PRIORITY来提高该语句的优先级,让读取操作能够执行。

执行insert和replace的时候可以使用DELAYED让MySQL返回OK状态给客户端,并且修改也是对该session可见的。但并不是已经将数据插入表,而是存储在内存里面等待排队。当能够获得表的写锁再插入。这样的好处是,提高插入的速度,客户端不需要等待太长时间。坏处是,不能返回自动递增的ID,以及系统崩溃时,MySQL还没有来得及插入数据的话,这些数据将会丢失。

②set LOW_PRIORITY_UPDATES = 1。

让所有支持LOW_PRIORITY选项的语句都默认地按照低优先级来处理。

③修改MAX_WRITE_LOCK_COUNT变量。

该变量默认为int最大值,表示当一个表的写锁数量达到设定的值后,就降低写锁的优先级,让读锁有机会执行。

总结

本文重点介绍了MySQL中MyISAM表级锁的实现特点,主要有以下几点:

  • 共享读锁之间是兼容的,但共享读锁与独占写锁之间,以及独占写锁之间是互斥的,也就是说读和写是串行的。
  • 在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
  • MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
  • 由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。