并发锁 (三):myisam表锁
在之前我们讲到了并发下锁的重要性,以及在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值较高,且存在性能问题,则说明存在着较严重的表级锁争用情况。这时,需要对应用做进一步的检查,来确定问题所在,应首先优化查询,然后拆分表或复制表。
并发锁系列文章
- 本文标签: 并发锁 数据库
- 本文链接: https://www.php20.cn/article/216
- 版权声明: 本文由仙士可原创发布,转载请遵循《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权