MySQL锁机制

数据加锁也是用来保证事务并发的重要机制。

MySQL锁分类:

  • 从数据粒度分表锁和行锁
  • 从性能分乐观锁和悲观锁
  • 从对数据库的操作类型分读锁和写锁,读锁共享,写锁排他

表锁:锁住整张表,加锁快,开销小,不会死锁,但是锁粒度大,锁冲突概率高,并发度很低,数据迁移时比较适用。

表锁测试,开启两个客户端连接,A客户端加表锁,B客户端不加锁

# A客户端加表锁
MySQL [test]> lock table account read;
Query OK, 0 rows affected (0.00 sec)
# 当前session查询正常
MySQL [test]> select * from account;
+----+---------+
| id | balance |
+----+---------+
|  1 |     400 |
|  2 |     600 |
|  3 |     200 |
|  4 |    2000 |
+----+---------+
4 rows in set (0.00 sec)
# 当前session插入数据会执行失败
MySQL [test]> insert into account(id,balance) values (5,1000);
ERROR 1099 (HY000): Table 'account' was locked with a READ lock and can't be updated
# 释放锁的同时,外面的session的阻塞操作会执行成功
MySQL [test]> unlock tables;
Query OK, 0 rows affected (0.01 sec)
# B客户端,在A客户端加表读锁之后,查询正常
MySQL [test]> select * from account;
+----+---------+
| id | balance |
+----+---------+
|  1 |     400 |
|  2 |     600 |
|  3 |     200 |
|  4 |    2000 |
+----+---------+
4 rows in set (0.00 sec)
# 插入会被阻塞,直到锁被释放
MySQL [test]> insert into account(id,balance) values (5,1000);
Query OK, 1 row affected (1 min 13.08 sec)

行锁:每次锁一行数据,锁粒度小,加锁慢开销大,发生锁冲突的概率低,有可能出现死锁,并发度高。innodb是支持行锁的,myisam不支持,且innodb支持事务。

行锁测试:

A客户端开启事务,对一行数据加锁但是不提交,select * from account where id = 1 for update;

B客户端更新id = 1的数据会被阻塞,更新其他的数据不会阻塞

值得注意的是,在innodb中锁是加在索引上,如果对非索引字段更新,行锁可能会变表锁
A客户端执行:update account set balance = 1000 where name = 'a'; B客户端对该表任一行写操作都会阻塞住。
 
行锁可以使用lock in share mode(共享锁)和for update(排它锁)
A客户端select * from account where id = 1 for update; B客户端只能读不能写这行数据,直到A的事务提交。
 
行锁分析:
通过检查innodb状态变量来分析系统上的行锁的争夺情况:show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待所花平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
Innodb_row_lock_waits: 系统启动后到现在总共等待的次数
当高次数,平均时长也高的时候,就需要分析系统锁竞争了,着手优化。
 
 
间隙锁(Gap Lock):锁的就是两个值之间的空隙,Mysql默认级别是repeatable-read,在此隔离级别下会生效,间隙锁在某些情况下可以解决幻读问题。
现在account表中数据如下:
MySQL [test]> select * from account;
+----+---------+------+
| id | balance | name |
+----+---------+------+
|  1 |     400 | a    |
|  2 |     600 | b    |
|  3 |     200 | c    |
|  8 |    1000 | h    |
| 13 |    1000 | m    |
+----+---------+------+
那么间隙就有 id 为 (8),(8,13),(13,正无穷) 这三个区间,
在A客户端下执行update account set balance = 1000 where id > 1 and id < 8; 则其他客户端没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(1,8]区间都无法修改数据。
 

死锁:两个session互相持有且等待对方释放锁

A客户端执行:select * from account where id = 1 for update;
B客户端执行:select * from account where id = 2 for update;
A客户端执行:select * from account where id = 2 for update;
B客户端执行:select * from account where id = 1 for update;
大多数情况mysql能够自动检测死锁并且做事务回滚,有些情况无法释放,就需要手动处理了:
 -- 查看INFORMATION_SCHEMA系统库锁相关数据表
-- 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;
-- 查看锁 select * from INFORMATION_SCHEMA.INNODB_LOCKS;
-- 查看锁等待 select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS; -- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到 kill trx_mysql_thread_id -- 查看锁等待详细信息 show engine innodb status\G;

 关于锁优化:

  1. 合理设计索引,缩小锁范围
  2. 减少范围查询,避免间隙锁
  3. 尽可能通过索引来检索数据,避免行锁升级为表锁
  4. 控制事务大小,需要加锁的sql放后面执行
  5. 尽可能低事务级别隔离

 

posted @ 2021-01-15 15:00  以战止殇  阅读(94)  评论(0编辑  收藏  举报