数据加锁也是用来保证事务并发的重要机制。
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 statusG;
关于锁优化:
- 合理设计索引,缩小锁范围
- 减少范围查询,避免间隙锁
- 尽可能通过索引来检索数据,避免行锁升级为表锁
- 控制事务大小,需要加锁的sql放后面执行
- 尽可能低事务级别隔离