文章目录
MySQL存储引擎
在MySQL数据库中,存储引擎的选择对于数据库的性能、事务支持、数据完整性等方面有着至关重要的影响。下面将详细讲解MySQL中的存储引擎,特别是InnoDB和MyISAM,以及InnoDB行锁与索引的关系。
MySQL存储引擎
MySQL支持多种存储引擎,每种存储引擎都有其特定的使用场景和优缺点。其中,InnoDB和MyISAM是最常用的两种存储引擎。
MyISAM
- 特点:
- 不支持事务和外键约束。
- 占用资源较小,访问速度快。
- 表级锁定,这意味着在写操作时,整个表会被锁定,其他读或写操作必须等待。
- 支持全文索引,适用于需要全文搜索的应用场景。
- 存储格式:
- 静态表:字段都是固定长度的,存储迅速,容易缓存,但占用空间多。
- 动态表:包含可变字段,占用空间少,但更新、删除会产生碎片,需要定期优化。
- 压缩表:通过myisamchk工具创建,占用空间极小,但访问速度稍慢。
InnoDB
- 特点:
- 支持事务处理和外键约束,适用于需要数据一致性和完整性的应用场景。
- 缓存能力较好,支持行级锁定,读写并发能力较强。
- 从MySQL 5.5版本开始支持全文索引。
查看和修改存储引擎
查看存储引擎
- 查看系统支持的存储引擎:
SHOW ENGINES;
- 查看表使用的存储引擎:
或者:SHOW TABLE STATUS FROM 库名 WHERE NAME='表名'\G;
USE 库名; SHOW CREATE TABLE 表名;
修改存储引擎
- 通过
ALTER TABLE
修改:USE 库名; ALTER TABLE 表名 ENGINE=MyISAM;
- 通过修改配置文件指定默认存储引擎:
在/etc/my.cnf
中添加或修改以下配置,并重启MySQL服务:
注意:此方法只对修改配置文件并重启MySQL服务后新创建的表有效。[mysqld] default-storage-engine=INNODB
- 在创建表时指定存储引擎:
USE 库名; CREATE TABLE 表名(字段1 数据类型,...) ENGINE=MyISAM;
InnoDB行锁与索引的关系
InnoDB的行锁是通过给索引项加锁来实现的。不同的查询条件会导致不同的锁行为。
-
主键索引:
DELETE FROM t1 WHERE id=1;
如果
id
字段是主键,InnoDB会直接锁住整行记录。 -
普通索引:
DELETE FROM t1 WHERE name='aaa';
如果
name
字段是普通索引,InnoDB会锁住索引对应的两行记录(即满足条件的记录的前后两行,形成一个范围锁)。 -
无索引:
DELETE FROM t1 WHERE age=23;
如果
age
字段没有索引,InnoDB会使用全表扫描来过滤记录。这时,表上的各行记录都可能被加上锁,导致性能下降。
因此,在使用InnoDB时,为了提高并发性能和避免不必要的锁等待,建议尽量为查询条件中的字段建立索引。
死锁
死锁是指两个或多个事务在执行过程中,因争夺资源(如锁)而造成的一种互相等待的现象。这种等待若无外力介入,将无限持续下去,导致事务无法继续运行,系统处于死锁状态。
例如,如果事务A锁住了记录1并等待记录2,而事务B锁住了记录2并等待记录1,这样两个事务就发生了死锁现象。计算机系统中,如果系统的资源分配策略不当,更常见的可能是程序员写的程序有错误等,则会导致进程因竞争资源不当而产生死锁的现象。
死锁危害
众所周知,数据库的连接资源是很珍贵的,如果一个连接因为事务阻塞长时间不释放,那么后面新的请求要执行的sql也会排队等待,越积越多,最终会拖垮整个应用。一旦你的应用部署在微服务体系中而又没有做熔断处理(当某服务出现不可用或响应超时的情况时,会暂时停止对该服务的调用),由于整个链路被阻断,那么就会引发雪崩效应,导致很严重的生产事故。
- 资源占用:死锁会导致数据库连接、锁资源等长时间被占用,无法释放。
- 性能下降:新请求因资源被占用而排队等待,导致系统响应时间变长,性能下降。
- 系统崩溃:在微服务体系中,若未做熔断处理,死锁可能引发雪崩效应,导致整个应用崩溃。
死锁案例
场景描述
在这个案例中,我们有两个数据库会话(session 1 和 session 2),它们各自开始了一个事务,并尝试锁定不同的行,然后尝试锁定对方已经锁定的行。
表结构和数据
create table t1(id int primary key, name char(3), age int);
insert into t1 values(1,'aaa',22);
insert into t1 values(2,'bbb',23);
insert into t1 values(3,'aaa',24);
insert into t1 values(4,'bbb',25);
insert into t1 values(5,'ccc',26);
insert into t1 values(6,'zzz',27);
会话操作
Session 1:
begin;
select * from t1 where id=1 for update; -- 锁定了id=1的行
-- 此时session 1等待session 2释放id=2的锁
select * from t1 where id=2 for update; -- 等待,因为id=2的行被session 2锁定了
Session 2:
begin;
select * from t1 where id=2 for update; -- 锁定了id=2的行
-- 此时session 2等待session 1释放id=1的锁
select * from t1 where id=1 for update; -- 死锁发生,因为id=1的行被session 1锁定了
分析
- 锁的类型:
for update
语句会为选中的行加上排他锁(X锁)。这意味着在事务完成之前,其他事务无法修改或删除这些行,也无法对这些行加上排他锁或共享锁(在大多数数据库隔离级别下)。
- 共享锁(S锁):允许事务读取一行数据,但不允许修改。多个事务可以同时持有对同一行的共享锁。
- 排他锁(X锁):允许事务读取和修改一行数据。在事务持有排他锁期间,其他事务无法对该行加任何锁。
-
死锁的发生:
- Session 1 锁定了
id=1
的行,并尝试锁定id=2
的行(但此时id=2
被 Session 2 锁定了)。 - Session 2 锁定了
id=2
的行,并尝试锁定id=1
的行(但此时id=1
被 Session 1 锁定了)。 - 这导致了一个循环等待条件:Session 1 等待 Session 2 释放
id=2
的锁,而 Session 2 等待 Session 1 释放id=1
的锁。
- Session 1 锁定了
-
死锁的检测和解决:
- 大多数现代数据库管理系统(如MySQL的InnoDB存储引擎)都具备死锁检测机制。
- 当检测到死锁时,数据库会选择一个事务进行回滚,以打破循环等待条件。
- 在这个案例中,数据库可能会选择回滚 Session 1 或 Session 2 中的一个事务,以释放锁并允许另一个事务继续执行。
避免死锁的策略
-
设置锁等待超时:
- 通过设置
innodb_lock_wait_timeout
参数,可以指定事务在等待锁资源时的超时时间。一旦超时,事务将回滚并释放锁资源。 innodb_rollback_on_timeout
参数控制是否在等待超时时回滚事务(默认不回滚)。
- 通过设置
-
开启死锁检测:
- InnoDB存储引擎支持死锁检测。当检测到死锁时,InnoDB会自动选择一个事务进行回滚,以打破死锁。
- 可以通过
innodb_deadlock_detect
参数查看和设置死锁检测是否开启。
-
优化业务逻辑:
- 尽量按照相同的顺序访问数据库表和记录,以减少死锁的可能性。
- 避免同时锁定多个资源,尽量将锁定操作限制在最小范围内。
-
保持事务简短:
- 尽量减少事务的持续时间,以减少对资源的占用时间和范围。
- 避免长事务,以减少完成事务可能的延迟和锁资源的占用。
-
添加合理索引:
- 为表添加合适的索引,以减少全表扫描的次数和时间。
- 索引可以加快查询速度,减少锁资源的占用时间。
-
降低隔离级别:
- 如果业务允许,可以降低数据库的隔离级别。例如,将隔离级别从可重复读(RR)调整为读已提交(RC),以减少间隙锁的使用和死锁的发生。
-
使用乐观锁:
- 在读多写少的场景下,可以使用乐观锁机制。乐观锁不会在上锁时阻塞其他事务的读取操作,而是在更新时检查数据是否被其他事务修改过。
- 如果数据被修改过,则放弃更新操作;否则,执行更新操作。
乐观锁与悲观锁
- 乐观锁:基于数据版本记录机制实现。在更新数据时,会检查数据版本是否发生变化。如果版本未变,则执行更新操作;如果版本已变,则放弃更新。适用于读多写少的场景。
- 悲观锁:在读取数据时直接加锁,以防止其他事务修改数据。加锁期间,其他事务无法读取或修改被锁定的数据。适用于写多或需要保证数据一致性的场景。
总结
存储引擎定义
存储引擎时MySQL数据库的一个核心组件,负责执行实际的数据IO操作(数据的存储和提取)。
工作在文件系统之上,数据库的存储数据会先将数据传输到存储引擎,再按照存储引擎的存储格式保存到文件系统。
InnoDB和MyISAM存储引擎的特点
存储引擎 | 事务支持 | 外键约束 | 锁定级别 | 读写并发能力 | 全文索引支持 | 文件存储 | 适用场景 |
---|---|---|---|---|---|---|---|
MyISAM | 不支持 | 不支持 | 表级锁定 | 较低(读写会相互阻塞) | 支持 | .frm(表结构文件), .MYD(表数据文件), .MYI(索引文件) | 不需要事务处理,单独写入或查询的应用场景 |
InnoDB | 支持 | 支持 | 行级锁定(全表扫描时为表级锁定) | 较好 | 5.5版本后开始支持 | .frm(表结构文件), .ibd(表空间文件) | 需要事务支持,一致性要求高,数据更新频繁的应用场景 |
存储引擎管理操作
操作类型 | SQL语句/命令 | 描述 |
---|---|---|
修改已存在表的存储引擎 | ALTER TABLE 表名 ENGINE=InnoDB/MyISAM; |
针对已经存在的表,修改其存储引擎为InnoDB或MyISAM |
新建表时指定存储引擎 | CREATE TABLE 表名 (...) ENGINE=InnoDB/MyISAM; |
在新建表时,指定表的存储引擎为InnoDB或MyISAM |
设置默认存储引擎(会话级) | SET SESSION default_storage_engine=InnoDB/MyISAM; |
为当前会话设置默认的存储引擎为InnoDB或MyISAM |
设置默认存储引擎(全局级) | SET GLOBAL default_storage_engine=InnoDB/MyISAM; |
为全局设置默认的存储引擎为InnoDB或MyISAM,影响之后新建的所有表(直到服务器重启或再次更改) |
持久化设置默认存储引擎(配置文件) | vim /etc/my.cnf 并添加 default-storage-engine=InnoDB/MyISAM |
在MySQL的配置文件中设置默认的存储引擎,重启服务器后生效 |
查看当前会话的默认存储引擎 | SHOW SESSION VARIABLES LIKE 'default_storage_engine'; |
显示当前会话的默认存储引擎设置 |
查看全局的默认存储引擎 | SHOW GLOBAL VARIABLES LIKE 'default_storage_engine'; |
显示全局的默认存储引擎设置 |
查看表的存储引擎 | SHOW CREATE TABLE 表名; |
显示表的创建语句,其中包括存储引擎信息 |
查看表状态(包含存储引擎信息) | SHOW TABLE STATUS WHERE name='表名'\G |
以垂直格式显示指定表的状态信息,其中包括存储引擎 |
问答环节
如何尽可能避免死锁?
1)设置锁等待超时时间:即两个事务相互等待时,一旦等待时间超过了这个时间之后,那么超时事务回滚释放资源,另一个事务就能正常执行了。
在 InnoDB 存储引擎中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值为 50 秒。 show VARIABLES like ‘innodb_lock_wait_timeout’;
参数 innodb_rollback_on_timeout 表示是否在等待超时时对进行中的事务进行回滚操作(默认是OFF,代表不回滚)。
2)主动开启死锁检测:当 innodb 检测发现死锁之后,就会进行回滚死锁的事物。
show VARIABLES like ‘innodb_deadlock_detect’; #查看当前死锁检测是否开启
set global innodb_deadlock_detect = ON; #ON为开启死锁检测,OFF为关闭
3)使用更合理的业务逻辑。对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定多个资源。
4)保持事务简短。减少对资源的占用时间和占用范围,避免长事务,减少完成事务可能的延迟并释放锁。
5)为表添加合理的索引。如果不使用索引将会发生全表扫描,扫描时间长,占用资源多,且耗时,会导致死锁的概率大大增加。
6)降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为间隙锁造成的死锁。
7)读多写少的场景下使用乐观锁机制,读取数据不上锁,在读的情况下可以共享资源,这样可以省去了锁的开销,提高了吞吐量。
乐观锁是什么,如何配置?
乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。适用于读多写少的场景。
SELECT * from t1 where id = 1 lock in share MODE;
悲观锁是什么,如何配置?
悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。一般适用于写多的场景。
SELECT * from t1 where id = 1 for update;