外观
锁机制
MySQL锁机制的基本知识
在同一时刻,可能会有多个客户端对中同一行操作,例如,有的客户端在读取该行数据,有的则尝试删除它。为了保证数据的一致性,数据库就要对这种并发操作进行控制,因此产生了锁的概念。
锁的类型
- 读锁
读锁也称为共享锁。它是共享的,或者说相互不阻塞的。多个客户端在同一时间可以同时读取同一资源,互不干扰。
- 写锁
写锁也成为排他锁。它是排他的,也就是说一个写锁会阻塞其他的写锁和读锁。这是为了确保在给定的时间内,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源,保证安全。
在实际数据库中,随时都在发生锁定。例如,当某个用户修改某一部分数据时,MySQL就会通过锁定防止其他用户读取同一数据。在大多数时候,MySQL锁的内部管理是透明的。
读锁和写锁的区别如下表。
| 请求模式 | 读锁 | 写锁 |
|---|---|---|
| 读锁 | 兼容 | 不兼容 |
| 写锁 | 不兼容 | 不兼容 |
锁粒度
一种提高共享资源并发性的方式就是让指定对象更有选择性,也就是尽量只锁定部分数据,而不是所有的资源,这就是锁粒度的概念。它是指锁的作用范围,是为了对数据库中高并发的响应和系统性能两方面进行平衡提出的。
锁粒度越小,并发访问性能越高,越适合做并发更新操作(即采用InnoDB存储引擎的表适合做并发查询操作);锁粒度越大,并发访问的效率越低,越适合做并发查询操作(即采用MyISAM存储引擎的表适合做并发查询操作)。
由于加锁也要消耗资源,所以需要注意,若系统花费大量的时间来管理锁,而不是存储数据,会得不偿失。
锁策略
锁策略是指在锁的开销和数据的安全性之间寻求平衡,但是这种平衡会影响性能,所以大多数数据库安全系统都没有提供更多的选择,一般都是在表上添加行级锁,并以各种复杂的方式来实现,以便在比较多的情况下提供更好的性能。
在MySQL中,每种存储引擎都可以实现自己的锁策略和颗粒度。因此,它提供了多种锁策略。在存储引擎的设计中,锁管理是非常重要的决定,它将颗粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时会失去对另外一个应用场景的良好支持。MySQL支持多个存储引擎,所以不用单一的通用解决方法。下面介绍两种不同的锁策略。
- 表级锁:表级锁是MySQL中最基本的锁策略,而且是开销最小的锁策略。它会固定整个表,一个用户在对表进行操作(如插入、删除、更新)前,需要先获得写锁,这会阻塞其他用户对表的所有读写操作。只有没有写锁时,其他读取的用户才能获取该锁,并且读锁之间是不会相互阻塞的。
另外,由于写锁比读锁的优先级高,所以一个写锁请求可能会被插入读锁队列的前面,但是读锁不能插入写锁的前面。
- 行级锁:行级锁可以最大限度地支持并发处理,同时也带来了最大的锁开销。在InnoDB或者一些其他存储引擎中实现了行级锁。行级锁只在存储引擎层中实现,而在服务器层中没有实现。服务器层中完全不了解存储引擎中的锁实现。
锁的生命周期
锁的生命周期就是在一个MySQL会话内,对数据进行加锁和解锁之间的时间间隔。锁的生命周期越长,并发性能就越低,反之并发性能就越高。另外,锁是数据库管理系统中的重要资源,需要占据一定的服务器内存,锁的周期越长,占用服务器内存的时间就越长;相反,占用的内存时间也就越短,因此应尽可能地缩短锁的生命周期。
MyISAM的表级锁
MySQL的MyISAM存储引擎不支持事务提交和回滚操作,所以当用户对数据库执行插入、删除、更新等操作时,这些变化的操作会立即保存在磁盘中。这在多用户环境会导致诸多问题。为了避免同一时间有多个用户对数据库中指定表进行操作,可以应用表锁定来避免用户在操作数据表时受到干扰。当且仅当用户释放表的操作锁定后,其他用户才可以访问修改后的数据表。
设置表级锁定事务的基本步骤如下:
为指定数据表添加锁定,语法如下。
LOCK TABLE table_name lock_type,...;其中,
lock_type表示锁定类型,该类型包括以读方式(READ)锁定表和以写方式(WRITE)锁定表。用户执行数据表,可以添加、删除或者更改部分数据。
用户完成对锁定数据表的操作后,需要对该表进行解锁操作,释放该表的锁定状态,语法如下。
UNLOCK TABLES;
下面将分别介绍如何以读方式和以写方式锁定数据表。
以只读方式锁定事务表
以读方式锁定数据表,是指设置锁定用户的其他操作方式,如删除、插入、更新等不被允许,直至用户进行解锁操作。下面我们使用只读方式锁定表tb_user,步骤如下。
创建数据表tb_user,存储引擎选择MyISAM,语法如下。
create table tb_user( id int(10) not null auto_increment primary key, username varchar(30) not null, pwd varchar(30) not null ) ENGINE = 'MyISAM';
运行结果 尝试向表中添加三组数据,语法如下。
insert into tb_user (username, pwd) values ('first','123456'),('second','222222'),('third','123123');
运行结果 查看表中数据,语法如下。
select * from tb_user;
运行结果 以读方式锁定tb_user表,语法如下。
lock tables tb_user read;运行结果 尝试查看数据和插入数据,代码如下。
select * from tb_user; insert into tb_user (username, pwd) values ('fourth','1234321');
运行结果
可以看到加入读锁后表仍然可以读取,但是不能写入。
解锁tb_user表,代码如下。
unlock tables;运行结果 再次尝试查看和插入数据表,代码如下。
select * from tb_user; insert into tb_user (username, pwd) values ('fourth','1234321');
运行结果
可以看到在解锁后可以插入数据表。
最后查看数据表的结果,代码如下。
select * from tb_user;
运行结果
在LOCK TABLES的参数中,用户指定数据表以读方式(READ)锁定数据表的变体为READ LOCAL锁定,其与READ锁定的不同点是:该参数所指定的用户会话可以执行INSERT语句,它是为了方便MySQL dump工具而创建的变体形式。
InnoDB的行级锁
为InnoDB表设置锁比为MyISAM设置锁更为复杂,这是因为InnoDB既支持表级锁又支持行级锁。为InnoDB表设置行级锁也是使用LOCK TABLES命令,其使用方法同MyISAM表基本相同,这里不再赘述。
为InnoDB表设置行级锁有以下三种方式。
在查询语句中设置读锁,语法如下。
SELECT 语句 LOCK IN SHARE MODE;如为采用InnoDB存储引擎的数据表tb_account在查询语句中设置读锁,可以使用如下语句:
select * from tb_account lock in share mode;在查询语句中设置写锁,语法如下。
SELECT 语句 FOR UPDATE;如为采用InnoDB存储引擎的数据表tb_account在查询语句中设置写锁,语法如下:
select * from tb_account for update;在更新(包括INSERT、UPDATE和DALETE)语句中,InnoDB存储引擎会自动为更新语句添加隐式写锁。
通过以上三种方式为表设置行级锁的生命周期非常短暂,通常使用事务来延长行级锁的生命周期。
下面以写锁的方式演示如何使用行级锁。
在命令行窗口1输入下面的语句开启事务并开启写锁。
start transaction; select * from tb_account for update;
运行结果 在命令行窗口2输入下面的语句开启事务并开启写锁。
start transaction; select * from tb_account for update;
运行结果 提交命令行窗口1的事务,释放写锁。
commit;
运行结果
可以看到提交命令行窗口1的事务后,命令行窗口2的select语句执行了,中间间隔16.39秒。
死锁的概念及避免
死锁就是两个或多个处于不同序列的用户打算同时更新某相同的表时,因互相等待对方释放锁而导致几方一直处于等待状态。实际应用中,若两个处于不同序列的用户想要同时更新某个相同的表时最有可能产生死锁。更具体地讲,当两个事务同时等待对方释放所持有的资源,而导致两个事务都无法操作对方持有的资源,这样无限期的等待被称为死锁。
InnoDB表处理程序会自动检测死锁,若该处理程序发现用户在操作过程中产生死锁,会立即通过撤销方式来撤销其中一个事务,以便使死锁消失。