本文主要介绍了锁的基本概念,以及InnoDB引擎使用行锁时的注意事项
锁只要是用来解决数据访问的一致性、有效性问题。
锁可以按照下面两种维度区分
按照对数据操作粒度区分:
锁类型 | 描述 |
---|---|
表锁 | 操作时,锁定整张表,偏向MyISAM 存储引擎,开销小,加锁块; 不会出现死锁;锁粒度大,发生锁冲突的概率最高,并发度最小 |
行级锁 | 操作时,锁定当前操作行;偏向InnoDB引擎,开销大加锁慢;会出现死锁 锁粒度最小,发生锁冲突的概率最低,并发度最高 |
页面锁 | 开销和加锁时间介于表锁和行锁之间;会出现死锁;锁粒度介于表锁和行锁之间 |
从对数据操作的类型分:
1) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。
2) 写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。
MyISAM 存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。
MyISAM 在执行查询语句(SELECT
)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT
等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。
对MyISAM表的读操作,读锁跟写锁有如下特点:
数据:
mysql> CREATE TABLE `tb_book` (
-> `id` INT(11) auto_increment,
-> `name` VARCHAR(50) DEFAULT NULL,
-> `publish_time` DATE DEFAULT NULL,
-> `status` CHAR(1) DEFAULT NULL,
-> PRIMARY KEY (`id`)
-> ) ENGINE=myisam DEFAULT CHARSET=utf8 ;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'java编程思想','2088-08-01','1');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'solr编程思想','2088-08-08','0');
Query OK, 1 row affected (0.00 sec)
mysql> CREATE TABLE `tb_user` (
-> `id` INT(11) auto_increment,
-> `name` VARCHAR(50) DEFAULT NULL,
-> PRIMARY KEY (`id`)
-> ) ENGINE=myisam DEFAULT CHARSET=utf8 ;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO tb_user (id, name) VALUES(NULL,'令狐冲');
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO tb_user (id, name) VALUES(NULL,'田伯光');
Query OK, 1 row affected (0.00 sec)
显示加锁命令:
-- 读锁
lock table table_name read;
-- 写锁
lock table table_name write
写锁演示:
session1 | session2 |
获得表tb_user写锁 mysql> lock table tb_user write; Query OK, 0 rows affected (0.00 sec) |
|
当前session对锁定的表的查询、更新、插入都可以执行 mysql> select * from tb_user; +----+-----------+ | id | name | +----+-----------+ | 1 | 令狐冲 | | 2 | 田伯光 | | 3 | 任盈盈 | +----+-----------+ mysql> insert into tb_user values(null,'风清杨'); Query OK, 1 row affected (0.00 sec mysql> update tb_user set name='东方不败' where id=3; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
其他session对锁定表的查询被阻塞,需要等待锁被释放 mysql> select * from tb_user; 等待 |
mysql> unlock tables; Query OK, 0 rows affected (0.00 sec) |
等待 |
session2获得锁,查询返回 mysql> select * from tb_user; +----+--------------+ | id | name | +----+--------------+ | 1 | 令狐冲 | | 2 | 田伯光 | | 3 | 东方不败 | | 4 | 风清杨 | +----+--------------+ 4 rows in set (1 min 50.20 sec) |
读锁展示:
session1 | session2 |
获得表tb_user读锁 mysql> lock table tb_user read; Query OK, 0 rows affected (0.00 sec) |
|
当前session可以查询锁定的表 mysql> SELECT * FROM tb_user; +----+--------------+ | id | name | +----+--------------+ | 1 | 令狐冲 | | 2 | 田伯光 | | 3 | 东方不败 | | 4 | 风清杨 | +----+--------------+ 4 rows in set (0.00 sec) 当前session不能查询没有锁定的表 mysql> SELECT * FROM tb_book; ERROR 1100 (HY000): Table 'tb_book' was not locked with LOCK TABLES |
其他session可以查询锁定表的记录 mysql> select * from tb_user; +----+--------------+ | id | name | +----+--------------+ | 1 | 令狐冲 | | 2 | 田伯光 | | 3 | 东方不败 | | 4 | 风清杨 | +----+--------------+ 4 rows in set (0.00 sec) 其他session可以查询或更新为锁定的表 mysql> SELECT id,status from tb_book; +----+--------+ | id | status | +----+--------+ | 1 | 1 | | 2 | 0 | +----+--------+ 2 rows in set (0.00 sec) mysql> update tb_book set status=1 where id=2; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
mysql> unlock tables; Query OK, 0 rows affected (0.00 sec) |
等待 |
当前session插入或更新锁定的表都会发生错误 mysql> INSERT INTO tb_user values(null,'仪琳'); ERROR 1099 (HY000): Table 'tb_user' was locked with a READ lock and can't be updated mysql> UPDATE tb_user SET name='仪琳' WHERE id=3; ERROR 1099 (HY000): Table 'tb_user' was locked with a READ lock and can't be updated |
其他session更新锁定表会处于等待 mysql> UPDATE tb_user SET name='仪琳' WHERE id=3; 等待 |
释放锁 mysql> unlock table; Query OK, 0 rows affected (0.00 sec) |
等待 |
session获得锁,更新操作完成 mysql> UPDATE tb_user SET name='仪琳' WHERE id=3; Query OK, 1 row affected (1 min 13.69 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。
事务是由一组SQL语句组成的逻辑处理单元,具有以下ACID属性:
并发事务处理带来的问题
问题 | 描述 |
---|---|
丢失更新 (Lost Update) |
当两个或多个事务选择同一行,最初的事务修改的值, 会后面的事务修改的值覆盖。 |
脏读 (Dirty Reads) |
当一个事务正在访问数据,并且对数据进行了修改, 而这种修改还没有提交到数据库中,这时,另外一个事务也访问到修改了但没提交数据 |
不可重复读 (Non Repeatable Reads) |
在同一个事务内,两次读到的数据不一致 |
幻读 (Phantom Reads) |
一个事务按照相同的查询条件重新读取以前查询过的数据, 却发现其他事务插入了 满足其查询条件的新数据。 |
事务隔离级别
为了解决上述提到的事务并发问题,数据库提供一定的事务隔离机制
隔离级别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read uncommitted | × | √ | √ | √ |
Read committed | × | × | √ | √ |
Repeatable read(默认) | × | × | × | √ |
Serializable | × | × | × | × |
备注 : √ 代表可能出现 , × 代表不会出现 。
查看隔离级别
mysql> SHOW variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.01 sec)
查询事务状态(是否自动提交)
-- 查询事务状态(是否自动提交)
mysql> SHOW VARIABLES LIKE 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
-- 设置事务不自动提交
mysql> SET AUTOCOMMIT=0;
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 开启事务
START TRANSACTION;
-- 手动提交
COMMIT;
行锁等待时间
mysql> SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
1 row in set (0.02 sec)
InnoDB实现了以下两种类型的锁模式:
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
对于普通SELECT语句,InnoDB不会加任何锁;
可以通过以下语法显示给记录集加共享锁或排他锁
-- 共享锁(S)
SELECT * FROM tb_name WHERE ... LOCK IN SHARE MODE;
-- 排他锁(X)
SELECT * FROM tb_name WHERE ... FOR UPDATE
下面演示锁:
数据准备
create table innodb_lock(
id int(11),
name varchar(16),
sex varchar(1)
)engine = innodb default charset=utf8;
insert into innodb_lock values(1,'100','1');
insert into innodb_lock values(3,'3','1');
insert into innodb_lock values(4,'400','0');
insert into innodb_lock values(5,'500','1');
insert into innodb_lock values(6,'600','0');
insert into innodb_lock values(7,'700','0');
insert into innodb_lock values(8,'800','1');
insert into innodb_lock values(9,'900','1');
insert into innodb_lock values(1,'200','0');
create index idx_lock_id on innodb_lock(id);
create index idx_name on innodb_lock(name);
1)共享锁:
session1 | session2 |
---|---|
关闭自动提交 |
关闭自动提交 |
当前session对id为1的记录加share mode共享锁 |
其他session仍然可以查询该条记录,并添加共享锁 |
当前session对锁定的记录进行更新操作,等待锁 |
其他session对锁定数据进行更新 |
获得锁后,该session成功更新 |
2)排他锁
session1 | session2 |
---|---|
对id为1的记录加排他锁 |
|
其他session可以查询该记录,但是不能对该记录加共享锁,会处于等待 |
|
当前记录可以对锁定的数据进行更新 |
|
其他session获得锁,并查询到session1提交的记录 |
InnoDB行锁是通过给索引上的索引项加锁实现的,如果没有索引,将通过隐藏的聚簇索引来对记录加锁。这种行锁的实现特点意味着,如果不通过索引条件检索数据,将对表中的所有记录加锁,实际效果跟表锁一样。
InnoDB行锁分3种情形:
1)在不同过索引条件查询时,InnoDB会锁定表中的所有记录
mysql> drop index idx_lock_id on innodb_lock;
Query OK, 0 rows affected (0.02 sec)
mysql> SHOW CREATE TABLE innodb_lock\G
*************************** 1. row ***************************
Table: innodb_lock
Create Table: CREATE TABLE `innodb_lock` (
`id` int(11) DEFAULT NULL,
`name` varchar(16) DEFAULT NULL,
`sex` varchar(1) DEFAULT NULL,
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
session1 | session2 |
---|---|
对id=1的记录进行更新,但未提交 |
session2对id=3的记录更新,处于锁等待状态 |
session1提交,释放锁 |
session2获得锁后,更新成功 |
作为对比,可对id进行添加索引后,在进行上面的更新操作。这里不再演示
需要特别注意的是,虽然建立了索引,查询索引失效了(如传值类型不匹配),就会导致对索引的记录进行加锁。
小结:在没有使用索引的情况下更新,InnoDB会对所有的记录都加锁
2)访问不同行记录,相同的索引键,会出现锁冲突
mysql> CREATE index idx_id ON innodb_lock(id);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> drop index idx_name ON innodb_lock;
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SHOW CREATE TABLE innodb_lock\G
*************************** 1. row ***************************
Table: innodb_lock
Create Table: CREATE TABLE `innodb_lock` (
`id` int(11) DEFAULT NULL,
`name` varchar(16) DEFAULT NULL,
`sex` varchar(1) DEFAULT NULL,
KEY `idx_id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> SELECT * FROM innodb_lock WHERE id=1;
+------+------+------+
| id | name | sex |
+------+------+------+
| 1 | 100 | 0 |
| 1 | 200 | 0 |
+------+------+------+
数据id为1的记录符合“访问不同行记录,相同的索引键”
session | session |
---|---|
虽然session2访问的是个session_1不同的记录 ,但是因为使用了相同的索引,所以需要等待锁 |
3)有多个索引时,InnoDB会对多个索引都加锁
mysql> ALTER TABLE innodb_lock ADD INDEX idx_name(name);
Query OK, 0 rows affected (0.03 sec)
mysql> SHOW CREATE TABLE innodb_lock\G
*************************** 1. row ***************************
Table: innodb_lock
Create Table: CREATE TABLE `innodb_lock` (
`id` int(11) DEFAULT NULL,
`name` varchar(16) DEFAULT NULL,
`sex` varchar(1) DEFAULT NULL,
KEY `idx_id` (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> SELECT * FROM innodb_lock WHERE id = 1;
+------+------+------+
| id | name | sex |
+------+------+------+
| 1 | 100 | 0 |
| 1 | 200 | 0 |
+------+------+------+
session1 | session2 |
---|---|
session2使用name的索引访问记录,因为记录没有被加锁,所以可以获得锁 |
|
访问的记录被session1锁定,等待获得锁 |
4)Next-Key锁
当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁; 对于键值在条件范围内但并不存在的记录,叫做 “间隙(GAP)” , InnoDB也会对这个 “间隙” 加锁,这种锁机制就是所谓的 间隙锁(Next-Key锁) 。
举例说明:emp表中有101条记录,其empId的值分别为1,2,…,100,101
下面的sql
SELECT * FROM emp WHERE empId>100 FOR UPDATE
是一个范围条件的检索,InnoDB不仅会对符合条件的empId为101的记录加锁,也会对empId大于101(这些记录不存在)的“间隙”加锁。
mysql> SELECT * FROM innodb_lock;
+------+------+------+
| id | name | sex |
+------+------+------+
| 1 | 100 | 0 |
| 3 | 3 | 0 |
| 4 | 400 | 0 |
| 5 | 500 | 1 |
| 6 | 600 | 0 |
| 7 | 700 | 0 |
| 8 | 800 | 1 |
| 9 | 900 | 1 |
| 1 | 200 | 0 |
+------+------+------+
9 rows in set (0.00 sec)
session1 | session2 |
---|---|
根据id范围更新数据 |
|
插入id=2的记录,处于阻塞状态 |
|
提交事务 |
阻塞解除,插入成功 |
当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势。
当我们使用InnoDB的行级锁不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。
优化建议: