今天总结一下 MySQL 的索引和锁机制相关的知识点。之所以总结索引,是因为索引非常重要,对于任何一个网站数据库来说,查询占 80% 以上,优化数据库性能,最主要是优化查询效率,使用索引是提高查询效率的最有效途径之一。之所以总结 MySQL 的锁机制,一方面是因为网上资料太少,平时大家也很少关注,另一方面是了解 MySQL 的锁机制,有利于数据库的优化设计,在一些重要场景中合理使用锁机制,能够有效保障数据的安全性。
MySQL 索引的主要用途就是提高数据的查询性能。索引本质上就是一种数据结构,在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 并且利用这些数据结构上实现高级查找算法,这种数据结构就是索引。
MySQL 索引按照功能分类,主要包含以下索引:
按照结构分类,主要包含两种索引:
-- MySQL 创建索引的语法格式如下: CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] -- 默认是 B+TREE 索引 ON 表名(列名...); ---------------------------- -- 在创建一张表时,可以直接创建主键索引 -- 因此主键列不需要单独创建索引 CREATE TABLE test( id INT PRIMARY KEY AUTO_INCREMENT, data1 VARCHAR(100), data2 INT ); -- 在登录用户表中,为【用户注册时间】创建普通索引 CREATE INDEX idx_register_time ON login_user(register_time); -- 在登录用户表中,为【用户名】创建唯一索引 CREATE UNIQUE INDEX udx_user_name ON login_user(user_name); ---------------------------- -- 查看一个表中的索引,语法格式为: -- SHOW INDEX FROM 表名; SHOW INDEX FROM login_user;
-- 使用 ALTER 为表添加索引,语法格式如下: -- 普通索引 ALTER TABLE 表名 ADD INDEX 索引名称(列名); -- 唯一索引 ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); -- 主键索引 ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); -- 联合索引 ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); -- 外键索引(添加外键约束,就是外键索引) ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); -- 全文索引(mysql只支持文本类型) ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); ---------------------------- -- 为 login_user 表中家庭地址 address 列添加全文索引 ALTER TABLE login_user ADD FULLTEXT fdx_address(address); ---------------------------- -- 删除索引的语法格式为: -- DROP INDEX 索引名称 ON 表名; DROP INDEX fdx_address ON login_user;
MySQL 在一张表中建立联合索引时,会遵循最左匹配的原则:即在查询数据时从联合索引的最左边的字段开始匹配。
假设我们在 login_user 表中,针对 user_name 、mobile、email 这三个字段创建联合索引:
ALTER TABLE login_user ADD INDEX idx_login(user_name, mobile, email);
联合索引 idx_login 实际建立了 (user_name)、 (user_name, mobile)、 (user_name, mobile, email) 三个索引。在查询 SQL 语句中,只要包含最左边的字段 user_name 即可,不需要考虑字段顺序,因为 MySQL 的优化器会自动帮助我们调整 where 条件中的字段顺序,匹配我们建立的索引。
-- 联合索引不需要考虑字段顺序,MySQL优化器会自动调整 where 条件后面的字段顺序, -- 只要包含最左边的字段即可,所以下面的 SQL语句都可以命中索引: -- 使用了 (user_name, mobile, email) 这个索引 SELECT * FROM login_user WHERE email='jobs@test.com' AND mobile='158xxxx2108' AND user_name='乔豆豆'; -- 使用了 (user_name, mobile) 这个索引 SELECT * FROM login_user WHERE user_name='候胖胖' AND mobile='134xxxx4820'; -- 使用了 (user_name) 这个索引 SELECT * FROM login_user WHERE user_name='蔺赞赞' -- 使用了 (user_name) 这个索引, email 这个字段的查询条件没有索引 SELECT * FROM login_user WHERE email='wolfer@funny.com' AND user_name='任肥肥'; ---------------------------- -- 下面的 SQL 查询语句,不会使用索引,因为不包含联合索引最左边的字段 user_name SELECT * FROM login_user WHERE mobile='134xxxx6559' AND email='wolfer@funny.com';
MySQL 数据表中创建索引时请尽量考虑一些原则,便于提升索引的使用效率,更高效的使用索引:
常用的数据库(Oracle,SQL Server,MySQL)的事务,都有四种隔离级别,如下所示:
隔离级别 | 中文名称 | 出现脏读 | 出现不可重复读 | 出现幻读 | 数据库默认隔离级别 |
---|---|---|---|---|---|
read uncommitted | 读未提交 | 是 | 是 | 是 | |
read committed | 读已提交 | 否 | 是 | 是 | Oracle / SQL Server |
repeatable read | 可重复读 | 否 | 否 | 是 | MySQL |
serializable | 串行化 | 否 | 否 | 否 |
以上排序,按照隔离级别从小到大,安全性越来越高,但是效率越来越低。
一般情况下,不会使用 read uncommitted 和 serializable ,因为 read uncommitted 安全级别最差,在并行处理时,各种问题都可能出现; serializable 虽然不会出现问题,但是所有对数据库的操作无法并行处理,只能单线程排队处理,性能效率比较低。
一般情况下,我们不要修改数据库事务的默认隔离级别。要想修改 MySQL 数据库隔离级别,使用以下 SQL 语句:
-- 将 MySQL 的数据库隔离级别,修改为串行化 -- 注意:每次修改完数据库隔离级别时,客户端需要重新连接 MySQL 才能生效 set global transaction isolation level serializable;
下面简单介绍一下脏读、不可重复读、幻读这三个问题:
问题 | 现象 |
---|---|
脏读 | 在一个事务处理过程中读取了另一个未提交的事务中的数据,导致两次查询结果不一致 |
不可重复读 | 在一个事务处理过程中读取了另一个事务中修改并已提交的数据,导致两次查询结果不一致 |
幻读 | 查询某些记录时不存在,但 insert 时发现此记录已存在,造成添加失败。 查询某些记录时不存在,但 delete 时却发现删除成功(受影响行数大于 0),导致误删了数据 |
从上面介绍的情况可以发现,MySQL 默认使用 repeatable read 隔离级别,只会有可能出现幻读的问题,怎么解决呢?
答案就是:采用 MySQL 的锁机制来解决。可以考虑在一些比较重要的场景中使用,比如跟钱有关的业务,以及动态数据迁移等等。
我们首先对 MySQL 的锁进行一下分类,这样能够从全局的角度去理解锁。
按操作分类:
按粒度分类:
存储引擎 | 表锁 | 行锁 |
---|---|---|
MyISAM | 支持 | 不支持 |
InnoDB | 支持 | 支持 |
MEMORY | 支持 | 不支持 |
按使用方式分类:
InnoDB 同时支持表锁和行锁,MyISAM 和 MEMORY 只支持表锁。
下面我们针对 InnoDB 和 MyISAM 两种存储引擎进行锁操作介绍,有关 MEMORY 的锁操作,可以参考 MyISAM 的锁操作。
在演示相关锁操作的代码之前,我们先介绍一下要操作的示例表结构,我们有一张示例表 employee ,该表只有 3 个字段:
字段名称 | 说明 |
---|---|
id | 员工id,只针对该字段创建了主键索引,其它字段没有索引 |
name | 员工姓名 |
money | 员工薪水 |
InnoDB 同时支持表锁和行锁,默认使用行锁,当编写的 SQL 语句的 Where 条件无法使用索引时,则自动提升为表锁。
-- InnoDB 读锁(共享锁)语法格式如下: SELECT语句 LOCK IN SHARE MODE; /* 读锁(共享锁)的特点:数据可以被多个事务查询,但是不能修改 */ ---------------------------- /* 打开【第一个 MySQL 客户端工具】,先运行下面前 3 条 SQL,别运行 COMMIT 语句。 */ -- 开启事务 START TRANSACTION; -- 查询id为 1 的数据记录。加入共享锁 SELECT * FROM employee WHERE id=1 LOCK IN SHARE MODE; -- 查询名称为其它某个人的数据记录。加入共享锁 SELECT * FROM employee WHERE name='侯胖胖' LOCK IN SHARE MODE; -- 提交事务 COMMIT; ---------------------------- /* 打开【第二个 MySQL 客户端工具】,手动逐条选中以下 SQL 语句并运行。 */ -- 开启事务 START TRANSACTION; -- 查询 id 为 1 的数据记录 -- 结果:可以查询 SELECT * FROM employee WHERE id=1; -- 查询 id 为 1 的数据记录,并加入读锁(共享锁) -- 结果:可以查询,这说明【共享锁】和【共享锁】是兼容的 SELECT * FROM employee WHERE id=1 LOCK IN SHARE MODE; -- 修改 id 为 2 的姓名为蔺赞赞 -- 结果:修改成功,因为 id 是 employee 表的索引,而 InnoDB 引擎默认是行锁 -- 而且【第一个客户端工具】并没有对 id 为 2 的记录加锁 UPDATE employee SET name='蔺赞赞' WHERE id=2; -- 修改id为 1 的姓名为任肥肥 -- 结果:不能修改,会出现阻塞。 -- 因为【第一个客户端工具】给 id 为 1 的记录加了读锁(共享锁),其它线程只能读,不能写 -- 只有当【第一个客户端工具】运行 COMMIT 后,才能修改成功 UPDATE employee SET name='任肥肥' WHERE id=1; -- 通过 name 字段,修改 monkey 字段的值 -- 结果:不能修改。InnoDB引擎如果不采用带索引的列。则会提升为表锁 -- 所以由于 name 不是 employee 表的索引,无法使用行锁,自动升级为表锁 -- 但是此处的表锁,跟【第一个客户端工具】的 id 为 1 的行锁有冲突,所以这里会阻塞 -- 只有当【第一个客户端工具】运行 COMMIT 后,才能修改成功 UPDATE employee SET money=30000 WHERE name='乔豆豆'; -- 提交事务 COMMIT;
-- InnoDB 写锁(排他锁)语法格式如下: SELECT语句 FOR UPDATE; /* 写锁(排他锁)的特点:加锁的数据,不能被其他事务【加锁查询】或【修改】 */ ---------------------------- /* 打开【第一个 MySQL 客户端工具】,先运行下面前 2 条 SQL,别运行 COMMIT 语句。 */ -- 开启事务 START TRANSACTION; -- 查询id为1的数据记录,并加入排他锁 SELECT * FROM employee WHERE id=1 FOR UPDATE; -- 提交事务 COMMIT; ---------------------------- /* 打开【第二个 MySQL 客户端工具】,手动逐条选中以下 SQL 语句并运行。 */ -- 开启事务 START TRANSACTION; -- 查询 id 为 1 的数据记录 -- 结果:普通查询没问题 SELECT * FROM employee WHERE id=1; -- 查询 id 为 1 的数据记录,并加入共享锁 -- 结果:不能查询,会出现阻塞。因为写锁(排他锁)不能和其他锁共存 -- 只有当【第一个客户端工具】运行 COMMIT 后,才能查询 SELECT * FROM employee WHERE id=1 LOCK IN SHARE MODE; -- 查询 id 为 1 的数据记录,并加入写锁(排他锁) -- 结果:不能查询,会出现阻塞。因为写锁(排他锁)不能和其他锁共存 -- 只有当【第一个客户端工具】运行 COMMIT 后,才能查询 SELECT * FROM employee WHERE id=1 FOR UPDATE; -- 修改 id 为 1 的姓名为天蓬 -- 结果:不能修改,会出现阻塞。因为写锁(排他锁)不能和其他锁共存 -- 只有当【第一个客户端工具】运行 COMMIT 后,才能修改 UPDATE employee SET name='天蓬' WHERE id=1; -- 提交事务 COMMIT;
最后得出的结论就是:
MyISAM 和 MEMORY 仅支持表锁,而且不支持事务,只有 InnoDB 才支持事务,下面仅对 MyISAM 进行锁操作演示。
-- MyISAM 读锁,进行加锁的语法格式如下: LOCK TABLE 表名 READ; /* 读锁的特点:所有连接只能读取数据,不能修改 */ -- 解锁(将当前会话所有的表进行解锁) UNLOCK TABLES; ---------------------------- /* 打开【第一个 MySQL 客户端工具】,先运行下面前 3 条 SQL,别运行解锁语句。 */ -- 为 employee 表加入读锁 LOCK TABLE employee READ; -- 查询 employee 表 -- 结果:查询成功 SELECT * FROM employee; -- 修改 employee 表的某条记录 -- 结果:修改失败,不阻塞,直接报错 UPDATE employee SET money=20000 WHERE id=1; -- 解锁 UNLOCK TABLES; ---------------------------- /* 打开【第二个 MySQL 客户端工具】,手动逐条选中以下 SQL 语句并运行。 */ -- 查询 employee 表 -- 结果:查询成功 SELECT * FROM employee; -- 修改 employee 表的任意一条记录 -- 结果:修改失败,会出现阻塞。 -- 只有当【第一个客户端工具】运行 UNLOCK TABLES 之后,才能修改成功 UPDATE employee SET money=25000 WHERE id=2;
-- MyISAM 写锁,进行加锁的语法格式如下: LOCK TABLE 表名 WRITE; /* 写锁的特点:一个连接加锁后,只能该连接进行查询和修改操作,其他连接【不能查询】和【不能修改】 */ -- 解锁(将当前会话所有的表进行解锁) UNLOCK TABLES; ---------------------------- /* 打开【第一个 MySQL 客户端工具】,先运行下面前 3 条 SQL,别运行解锁语句。 */ -- 为 employee 表添加写锁 LOCK TABLE employee WRITE; -- 查询 employee 表 -- 结果:查询成功。当前连接可以查询 SELECT * FROM employee; -- 修改 employee 表某条记录 -- 结果:修改成功。当前连接可以修改 UPDATE employee SET money=3999 WHERE id=2; -- 解锁 UNLOCK TABLES; ---------------------------- /* 打开【第二个 MySQL 客户端工具】,手动逐条选中以下 SQL 语句并运行。 */ -- 查询 employee 表 -- 不能查询。 -- 只有当【第一个客户端工具】运行 UNLOCK TABLES 之后,才能查询成功 SELECT * FROM employee; -- 修改 employee 表某条记录 -- 结果:不能修改。 -- 只有当【第一个客户端工具】运行 UNLOCK TABLES 之后,才能修改成功 UPDATE employee SET money=2999 WHERE id=3;
悲观锁的特点:
乐观锁的特点:
悲观锁和乐观锁的使用场景:
乐观锁的实现方式:
通过增加【版本号】来实现
通过增加【标识符】来实现
-- 获取数据的 version 值,假如获取出来的值是 100 SELECT version FROM employee WHERE id=1; -- 同时使用 id 和 version 作为 where 条件,修改该数据的值 UPDATE employee SET name='弼马温',version=version+1 WHERE id=1 AND version=100; ---------------------------- -- 获取数据的 dataflag 值,假如获取出来的值是 bedcb0aa-85ba-11ec-b014-902e16a6f8db SELECT dataflag FROM employee WHERE id=2; -- 同时使用 id 和 dataflag 作为 where 条件,修改该数据的值 UPDATE employee SET name='齐天大圣',version=uuid() WHERE id=2 AND version='bedcb0aa-85ba-11ec-b014-902e16a6f8db';
到此为止,MySQL 的索引和锁相关的知识点,基本上总结完毕,希望对大家有所帮助。