原创

并发锁 (三):myisam表锁

温馨提示:
本文最后更新于 2019年09月16日,已超过 1,850 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

在之前我们讲到了并发下锁的重要性,以及在php中怎么实现文件锁

现在我们来讲讲关于mysql之间的锁:表锁和行锁

MyISAM 表锁

MyISAM 存储引擎只支持表锁,这也是MySQL 开始几个版本中唯一支持的锁类型。

表锁模式

所谓表锁,就是按表为单位直接锁住整个表

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

在前面的文章已经讲过了共享锁和独占锁,不多解释

如何加表锁

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,

例如:

select * from test limit 1

自动加共享锁,查询结束之后释放

再未查询成功之前,该表不允许执行更新操作:

测试案例

mysql终端1执行

select * ,sleep(100) from test limit 1 ; // sleep 100秒之后再获取查询结果

mysql终端2执行

update test set id=1;

mysql终端3执行

select * from test limit 1 ;

此时会出现 终端1  sleep等待状态,同时终端2在等待终端1释放共享锁,终端3在等待终端2释放独占锁(虽然没获取到独占锁):

仙士可博客

仙士可博客

如果去掉终端2语句,终端3语句将正常执行(不多说明)

MyISAM之锁机制

在上面,我们发现了一个这样的现象:(现象1)

终端1 sleep读取,共享锁

终端3读取,共享锁,正常执行

然而:(现象2)

终端1 sleep读取,共享锁

终端2 写入,独占锁,阻塞等待锁

终端3 读取,共享锁,阻塞等待锁

理论上来说,终端1和终端3都是共享锁,为什么终端3还需要等待呢?

因为myisam引擎的读写操作是串行的,

在现象1中

终端1 sleep读取,共享锁运行之后,其他连接还可以继续读取表,(共享锁特性)

串行到终端3,终端3可以继续执行

现象2中

终端1 sleep读取,共享锁运行之后,其他连接可以继续读取表,不能更新表数据(共享锁特性)

串行到终端2,终端2由于是写入操作,独占锁,将会被阻塞,但是终端2已经是独占锁等待状态,其他连接不能读取,不能更新(独占锁特性)

此时串行到终端3,由于myisam已经有了独占锁等待情况,所以终端3被阻塞

MyISAM之锁调度

前面讲过,MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个 MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这种情况有时可能会变得非常糟糕!幸好我们可以通过一些设置来调节MyISAM 的调度行为。

  • 通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。

  • 通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。

  • 通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。

另外,MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。

上面已经讨论了写优先调度机制带来的问题和解决办法。这里还要强调一点:一些需要长时间运行的查询操作,也会使写进程“饿死”!因此,应用中应尽量避免出现长时间运行的查询操作,不要总想用一条SELECT语句来解决问题,因为这种看似巧妙的SQL语句,往往比较复杂,执行时间较长,在可能的情况下可以通过使用中间表等措施对SQL语句做一定的“分解”,使每一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。

手动加锁

有一个订单表orders,其中记录有各订单的总金额total,同时还有一个订单明细表order_detail,其中记录有各订单每一产品的金额小计 subtotal,假设我们需要检查这两个表的金额合计是否相符,可能就需要执行如下两条SQL:

select sum(total) from orders;
select sum(subtotal) from order_detail;

这个语句看起来是没错的,但是可能出现这个情况:

语句1查询,加共享锁,查询完,释放

在其他连接中,出现一条insert的语句,在order_detail中加了一条数据 加独占锁,因为语句1已经查询完,锁已经释放,所以正常加锁

语句2查询需要等待insert插入完毕,释放锁,才能继续查询

这时候就出现了2个问题:

1:语句1和语句2原本是同一个逻辑,统计2种金额的,却导致了语句2多统计了一条数据,导致金额合计不符合

2:语句2需要等待insert插入完毕才能够继续执行,如果insert插入花费了10秒,那语句2也得等10秒之后才能查询

那该怎么解决呢?

myisam可以使用语句手动加锁:

LOCK TABLES tbl_name [AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}

例如:

lock tables orders read local, order_detail read local;
select sum(total) from orders;
select sum(subtotal) from order_detail;
unlock tables;
  • 上面的例子在LOCK TABLES时加了“local”选项,其作用就是在满足MyISAM表并发插入条件的情况下,允许其他用户在表尾并发插入记录。

  • 在用LOCK TABLES给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL不支持锁升级。也就是说,在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。其实,在自动加锁的情况下也基本如此,MyISAM总是一次获得SQL语句所需要的全部锁。这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。

手动锁表之后不能操作未锁的表:

mysql> lock tables test read local;
Query OK, 0 rows affected

mysql> select * from test2 limit 1;
1100 - Table 'test2' was not locked with LOCK TABLES
mysql> unlock tables;
Query OK, 0 rows affected

mysql> select * from test2 limit 1;
+----+------+------------------------+------------------------+
| id | name | url1                   | url2                   |
+----+------+------------------------+------------------------+
|  1 | 0    | http://www.KmxhtRE.com | moc.ERthxmK.www//:ptth |
+----+------+------------------------+------------------------+
1 row in set

mysql>

并发插入

myisam在加共享锁之后,理论上是不允许同时插入数据的,但是mysql有个系统变量concurrent_insert 用于控制其插入行为(只允许插入,不允许更新等)

  • 当concurrent_insert设置为0时,不允许并发插入。

  • 当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。

  • 当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。

 mysql> select @@concurrent_insert;//查询当前系统全局配置 
+---------------------+
| @@concurrent_insert |
+---------------------+
| AUTO                |
+---------------------+
1 row in set

AUTO 等于1 NEVER为0,ALWAYS为2

可以利用MyISAM存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。例如,将concurrent_insert系统变量设为2,总是允许并发插入;同时,通过定期在系统空闲时段执行 OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。

分析数据库锁状态

mysql> show status like 'table%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 157   |
| Table_locks_waited         | 22    |
| Table_open_cache_hits      | 10    |
| Table_open_cache_misses    | 1     |
| Table_open_cache_overflows | 1     |
+----------------------------+-------+
5 rows in set

Table_locks_immediate:能够立即获得表级锁的锁请求次数

Table_locks_waited:不能立即获取表级锁而需要等待的锁请求次数

分析:

如果table_locks_waited值较高,且存在性能问题,则说明存在着较严重的表级锁争用情况。这时,需要对应用做进一步的检查,来确定问题所在,应首先优化查询,然后拆分表或复制表。

并发锁系列文章

并发锁(一):为什么要加锁

并发锁(二) 文件中的共享锁和独占锁

并发锁 (三):myisam表锁

并发锁 (四):innodb 事务

正文到此结束
本文目录