本系列文章:
Mysql(一)三大范式、数据类型、常用函数、事务
Mysql(二)Mysql SQL练习题
Mysql(三)索引、视图、存储过程、触发器、分区表
Mysql(四)存储引擎、锁
Mysql(五)Mysql架构、数据库优化、主从复制
Mysql(六)慢查询、执行计划、SQL语句优化
MySQL 5.0支持的存储引擎包括MyISAM、InnoDB、BDB、MEMORY等,其中InnoDB和BDB提供事务安全表,其他存储引擎都是非事务安全表。
查看系统变量值,命令为:show variables
。
查看某个系统变量,命令为:SHOW VARIABLES like '变量名'
。
查看Mysql提供的所有存储引擎,命令为:show engines
,示例:
特点 | MyISAM | InnoDB | MEMORY | MERGE | NDB |
---|---|---|---|---|---|
存储限制 | 有 | 64TB | 有 | 没有 | 有 |
事务安全 | 支持 |
||||
锁机制 | 表锁 |
行锁 |
表锁 | 表锁 | 行锁 |
B树索引 | 支持 | 支持 | 支持 | 支持 | 支持 |
哈希索引 | 支持 | 支持 | |||
全文索引 | 支持 | ||||
集群索引 | 支持 | ||||
数据缓存 | 支持 | 支持 | 支持 | ||
索引缓存 | 支持 | 支持 | 支持 | 支持 | 支持 |
数据可压缩 | 支持 | ||||
空间使用 | 低 | 高 | N/A | 低 | 低 |
内存使用 | 低 | 高 | 中等 | 低 | 高 |
批量插入的速度 | 高 | 低 | 高 | 高 | 高 |
外键 | 支持 |
MyISAM不支持事务、也不支持外键,其优势是访问的速度快,对事务完整性没有要求或者以SELECT、INSERT为主的应用基本上都可以使用这个引擎来创建表
。
每个MyISAM在磁盘上存储成3个文件,其文件名都和表名相同,扩展名分别是:
frm(存储表定义);
MYD(MYData,存储数据);
MYI (MYIndex,存储索引)。
数据文件和索引文件可以放置在不同的目录。
MyISAM类型的表可能会损坏,原因可能是多种多样的,损坏后的表可能不能访问,会提示需要修复或者访问后返回错误的结果。MyISAM类型的表提供修复的工具,可以用CHECK TABLE语句来检查MyISAM表的健康,并用REPAIR TABLE语句修复一个损坏的MyISAM表。
表损坏可能导致数据库异常重新启动。
InnoDB存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全。对比MyISAM的存储引擎,InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。
InnoDB表的自动增长列可以手工插入,但是插入的值如果是空或者0,则实际插入的将是自动增长后的值。
可以通过ALTER TABLE *** AUTO_INCREMENT = n;
语句强制设置自动增长列的初识值,
默认从1开始,但是该强制的默认值是保留在内存中的,如果该值在使用之前数据库重新启动,那么这个强制的默认值就会丢失,就需要在数据库启动以后重新设置。
可以使用 LAST_INSERT_ID()函数,查询当前线程最后插入记录使用的值。如果一次插入了多条记录,那么返回的是第一条记录使用的自动增长值。
对于InnoDB表,自动增长列必须是索引。如果是组合索引,也必须是组合索引的第一
列,但是对于MyISAM表,自动增长列可以是组合索引的其他列,这样插入记录后,自动增长列是按照组合索引的前面几列进行排序后递增的。
MySQL支持外键的存储引擎只有InnoDB,在创建外键的时候,要求父表必须有对应的索引,子表在创建外键的时候也会自动创建对应的索引。
在创建索引的时候,可以指定在删除、更新父表时,对子表进行的相应操作,包括RESTRICT、CASCADE、SET NULL和NO ACTION。其中RESTRICT和NO ACTION相同,是指限制在子表有关联记录的情况下父表不能更新;CASCADE表示父表在更新或者删除时,更新或者删除子表对应记录;SET NULL则表示父表在更新或者删除的时候,子表的对应字段被 SET NULL。选择后两种方式的时候要谨慎,可能会因为错误的操作导致数据的丢失。
当某个表被其他表创建了外键参照,那么该表的对应索引或者主键禁止被删除。
InnoDB存储表和索引有以下两种方式:
要使用多表空间的存储方式,需要设置参数innodb_file_per_table,并重新启动服务后才可以生效,对于新建的表按照多表空间的方式创建,已有的表仍然使用共享表空间存储。如果将已有的多表空间方式修改回共享表空间的方式,则新建表会在共享表空间中创建,但已有的多表空间的表仍然保存原来的访问方式。所以多表空间的参数生效后,只对新建的表生效。
多表空间的数据文件没有大小限制,不需要设置初始大小,也不需要设置文件的最大限制、扩展大小等参数。
对于使用多表空间特性的表,可以比较方便地进行单表备份和恢复操作,但是直接复制.ibd文件是不行的,因为没有共享表空间的数据字典信息,直接复制的.ibd文件和.frm文件恢复时是不能被正确识别的,但可以通过以下命令:
ALTER TABLE tbl_name DISCARD TABLESPACE;
ALTER TABLE tbl_name IMPORT TABLESPACE;
将备份恢复到数据库中,但是这样的单表备份,只能恢复到表原来在的数据库中,而不能恢复到其他的数据库中。如果要将单表恢复到目标数据库,则需要通过mysqldump和mysqlimport来实现。
MEMORY存储引擎使用存在内存中的内容来创建表。每个MEMORY表只实际对应一个磁盘文件,格式是.frm
。MEMORY类型的表访问非常得快,因为它的数据是放在内存中的,并且默认使用HASH索引,但是一旦服务关闭,表中的数据就会丢失掉。
给MEMORY表创建索引的时候,可以指定使用HASH索引还是BTREE索引
。
服务器需要足够内存来维持所有在同一时间使用的MEMORY表,当不再需要MEMORY表的内容之时,要释放被MEMORY表使用的内存,应该执行DELETE FROM或TRUNCATE TABLE,或者整个地删除表(使用 DROP TABLE 操作)。
每个MEMORY表中可以放置的数据量的大小,受到max_heap_table_size系统变量的约束,这个系统变量的初始值是16MB,可以按照需要加大
。此外,在定义MEMORY表的时候,可以通过MAX_ROWS子句指定表的最大行数。
MEMORY类型的存储引擎主要用在那些内容变化不频繁的代码表,或者作为统计操作的中间结果表,便于高效地对中间结果进行分析并得到最终的统计结果。对MEMORY存储引擎的表进行更新操作要谨慎,因为数据并没有实际写入到磁盘中,所以一定要对下次重新启动服务后如何获得这些修改后的数据有所考虑。
存储引擎:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现
。常用的存储引擎有:
Innodb引擎提供了对数据库ACID事务的支持,并且还提供了行级锁和外键的约束
。它的设计的目标就是处理大数据容量的数据库系统。不提供事务的支持,也不支持行级锁和外键
。所有的数据都在内存
中,数据的处理速度快,但是安全性不高。MyISAM与InnoDB区别:
MyISAM | Innodb | |
---|---|---|
存储结构 | 每张表被存放在三个文件 :frm:表结构 MYD:数据文件 MYI:索引文件 |
frm:表定义文件 ibd :数据文件 InnoDB表的大小只受限于操作系统文件的大小,一般为2GB |
存储空间 | MyISAM可被压缩,存储空间较小 | InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引 |
可移植性、备份及恢复 | 由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作 | 免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了 |
记录存储顺序 | 按记录插入顺序保存 | 按主键大小有序插入 |
外键 | 不支持 |
支持 |
事务 | 不支持 |
支持 |
锁支持 | 支持表级锁 |
支持行级锁、表级锁 |
SELECT | MyISAM更优 |
|
INSERT、UPDATE、DELETE | InnoDB更优 |
|
select count(*) | myisam更快,因为myisam内部维护了一个计数器,可以直接调取。 | |
索引的实现方式 | B+树索引 ,myisam 是堆表 |
B+树索引 ,Innodb 是索引组织表 |
哈希索引 | 不支持 | 支持 |
全文索引 | 支持 | 不支持(在5.6之后支持) |
索引类型 | 非聚簇索引 | 聚簇索引 |
适合操作类型 | 大量select | 大量insert、delete、update |
MVCC | 不支持 | 支持 |
Mysql默认引擎:5.1版本之前默认引擎是MyISAM,之后是InnoDB
。
聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引
。 如果没有特别的需求,使用默认的Innodb即可。
MyISAM:以读写插入为主的应用程序
,比如博客系统、新闻门户网站。
Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键
。比如OA自动化办公系统。
不同的业务表,应该选择不同的存储引擎,例如:查询插入操作多的业务表,用MyISAM。临时数据用Memery。常规的并发大更新多的表用InnoDB
。
字段定义原则:使用可以正确存储数据的最小数据类型
。
非空字段尽量定义成NOT NULL,提供默认值,或者使用特殊值、空串代替NULL 。NULL 类型的存储、优化、使用都会存在问题
。 通过ENGINE=xxx
设置引擎。示例:
create table person(
id int primary key auto_increment,
username varchar(32)
) ENGINE=InnoDB
1)如果表的存储引擎是MyISAM,那么是18。因为 MyISAM表会把自增主键的最大ID记录到数据文件里,重启MySQL自增主键的最大ID也不会丢失
。
2)如果表的存储引擎是InnoDB,那么是15。InnoDB 表只是把自增主键的最大ID记录到内存中
,所以重启数据库或者是对表进行OPTIMIZE 操作,都会导致最大ID丢失。
MyISAM:把一个表的总行数存在了磁盘上,执行count(*)
的时候直接返回这个数值,效率高。
InnoDB:执行count(*)
的时候,需要把数据一行一行从引擎读出来然后累积计数。
事务进行过程中,每次sql语句执行,都会记录undo log和redo log,然后更更新数据形成脏页,然后redo log按照时间或者空间等条件进行落盘,undo log和脏页按照checkpoint进行落盘,落盘后相应的redo log就可以删除了。此时,事务还未COMMIT,如果发生崩溃,则首先检查checkpoint记录,使用相应的redo log进行数据和undo log的恢复,然后查看undo log的状态发现事务尚未提交,然后就使用undo log进行行事务回滚。事务执⾏行行COMMIT操作时,会将本事务相关的所有redo log都进行落盘,只有所有redo log落盘成功,才算COMMIT成功。然后内存中的数据脏⻚页继续按照checkpoint进行落盘。如果此时发生了崩溃,则只使用redo log恢复数据。
锁是计算机协调多个进程或线程并发访问某一资源的机制。
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是:不同的存储引擎支持不同的锁机制:
MyISAM和MEMORY存储引擎采用的是表级锁
;InnoDB存储引擎既支持行级锁,也支持表级锁,但默认情况下是采用行级锁
。表级锁是mysql锁中粒度最大的一种锁
,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
。行锁的是mysql锁中粒度最小的一种锁
,因为锁的粒度很小,所以发生资源争抢的概率也最小,并发性能最大,但是也会造成死锁,每次加锁和释放锁的开销也会变大。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
。
表级锁更适合于以查询为主
,只有少量按索引条件更新数据的应用,如Web应用;行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用
,如一些在线事务处理(OLTP)系统。
OLTP是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,例如银行交易;
OLAP是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。
- OLTP系统强调数据库内存效率,强调内存各种指标的命令率,强调绑定变量,强调并发操作;
- OLAP系统则强调数据分析,强调SQL执行市场,强调磁盘I/O,强调分区等。
MyISAM存储引擎只支持表锁。
可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁的使用情况。如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。
MySQL的表级锁有两种模式:表共享读锁
(Table Read Lock)和表独占写锁
(Table Write Lock)。锁模式的兼容性:
对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作。MyISAM表的读操作与写操作之间,以及写操作之间是串行的。
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预。
在一定条件下,MyISAM表也支持查询和插入操作的并发进行。
MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。
当concurrent_insert设置为0时,不允许并发插入。
当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。
当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。
MyISAM表的读和写是串行的,这是就总体而言的,在一定条件下,MyISAM也支持查询和插入操作的并发执行。
假如session1可以通过lock table mylock read local
获取表的read local锁定,session1不能对表进行更新或者插入操作,session1不能查询没有锁定的表,session1不能访问其他session插入的记录。此时session2可以查询该表的记录,session2可以进行插入操作,但是更新会阻塞,。
然后session1通过unlock tables
释放锁资源,session1可以查看session2插入的记录,session2获取锁,更新操作完成。
可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺。
mysql> show status like 'table%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 352 |
| Table_locks_waited | 2 |
+-----------------------+-------+
如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。
MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。如果一个进程请求某个MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL会先让写进程获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前。这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因。大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。
开发者可以通过一些设置来调节MyISAM的调度行为。
- 通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
- 通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。
- 通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。
另外,MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。
可以通过检查InnoDB_row_lock状态变量,来分析系统上的行锁的争夺情况。
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 18702 |
| Innodb_row_lock_time_avg | 18702 |
| Innodb_row_lock_time_max | 18702 |
| Innodb_row_lock_waits | 1 |
+-------------------------------+-------+
如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
创建和删除监视器示例:
CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;
DROP TABLE innodb_monitor;
设置监视器后,在SHOW INNODB STATUS的显示内容中,会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析和问题的确定。打开监视器以后,默认情况下每15秒会向日志中记录监控的内容,如果长时间打开会导致.err 文件变得非常的巨大,所以用户在确认问题原因之后,要记得删除监控表以关闭监视器,或者通过使用“–console”选项来启动服务器以关闭写日志文件。
InnoDB实现了以下两种类型的行锁:共享锁和排他锁。
共享锁
就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改
。update、delete、insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型
。select …for update
语句,加共享锁可以使用select … lock in share mode
语句。InnoDB行锁是通过给索引上的索引项加锁来实现的
。InnoDB只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁
。 create table tab_no_index(id int,name varchar(10)) engine=innodb;
insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
session1可以通过select * from tab_no_index where id = 1 for update
命令,只给一行加了排他锁,但是session2在请求其他行的排他锁的时候,会出现锁等待。原因是在没有索引的情况下,innodb只能使用表锁。
2、创建带索引的表进行条件查询,innodb使用的是行锁。
比如创建一张表:
create table tab_with_index(id int,name varchar(10)) engine=innodb;
alter table tab_with_index add index id(id);
insert into tab_with_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
此时当一个session对一个行加锁时,不影响另一个session对别的行的操作。
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意
向锁(Intention Locks),这两种意向锁都是表锁。
意向共享锁(IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
意向互斥锁(IX):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
意向共享锁和意向排它锁总称为意向锁。意向锁的出现是为了支持Innodb支持多粒度锁。
意向锁是表级别锁。
当需要给一个加表锁的时候,需要根据意向锁去判断表中有没有数据行被锁定,以确定是否能加成功。如果意向锁是行锁,那么我们就得遍历表中所有数据行来判断。如果意向锁是表锁,则直接判断一次就知道表中是否有数据行被锁定了。所以说将意向锁设置成表级别的锁的性能比行锁高的多。
有了意向锁之后,前面例子中的事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,因为表上有意向排他锁之后事务B申请表的写锁时会被阻塞。
意向锁的作用就是:当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。
InnoDB2种行锁和2种表锁的兼容情况:
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
//共享锁(S)
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
//排他锁(X)
SELECT * FROM table_name WHERE ... FOR UPDATE
用SELECT ... IN SHARE MODE
获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE
方式获得排他锁。
当用范围条件而不是相等条件检索数据,并请求共享或排他锁时,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(这些记录并不存在)的“间隙”加锁。
InnoDB 使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求。另外一方面,是为了满足其恢复和复制的需要。
在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁。
MySQL通过BINLOG记录执行成功的INSERT、UPDATE、DELETE等更新数据的SQL语句,并由此实现MySQL数据库的恢复和主从复制。
MySQL的恢复机制有两个特点:
对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁。
当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM表了。
在InnoDB下,使用表锁要注意以下两点:
#如果需要写表 t1 并从表 t 读,可以按如下做:
SET AUTOCOMMIT=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
[do something with tables t1 and t2 here];
COMMIT;
UNLOCK TABLES;
在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,这就决定了在InnoDB中发生死锁是可能的。
比如,两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数innodb_lock_wait_timeout来解决。需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。
以下是几种避免死锁的常用方法。
SELECT...FOR UPDATE
加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题。SELECT...FOR UPDATE
,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第 1 个线程提交后,第 2 个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第 3 个线程又来申请排他锁,也会出现死锁。对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK 释放获得的排他锁。 如果出现死锁,可以用SHOW INNODB STATUS
命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。
处理死锁的两种方式:
- 设置超时时间,超时后自动释放。
- 发起死锁检测,主动回滚其中一条事务,让其他事务继续执行。
乐观锁实现方式:一般会使用版本号机制或CAS算法实现
。乐观锁不是数据库自带的,需要我们自己去实现
,示例:
- SELECT data AS old_data, version AS old_version FROM …;
- 根据获取的数据进行业务操作,得到new_data和new_version
- UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
// 乐观锁获取成功,操作完成
} else {
// 乐观锁获取失败,回滚并重试
}
乐观锁的优点:乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。
乐观锁的缺点:乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
总结:读用乐观锁,写用悲观锁
。
悲观锁
:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制(select ... for update)
。当数据库执行select for update时会获取被select中的数据行的行锁
,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用
。MySQL中,select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在MySQL中用悲观锁务必要确定走了索引,而不是全表扫描
。set autocommit=0;
关闭Mysql的autoCommit属性,因为查询出数据之后就要将该数据锁定。
- 开始事务
begin; 或者 start transaction;- 查询出商品信息,然后通过for update锁定数据防止其他事务修改
select status from t_goods where id=1 for update;- 根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);- 修改商品status为2
update t_goods set status=2;- 提交事务
commit; --执行完毕,提交事务
在第2步我们将数据查询出来后直接加上排它锁(X)锁,防止别的事务来修改事务1,直到我们commit后,才释放了排它锁。
悲观锁的优点:保证了数据处理时的安全性。
悲观锁的缺点:加锁造成了开销增加,并且增加了死锁的机会。降低了并发性。
select ... for update
前加个事务就可以防止更新丢失。悲观锁和乐观锁大部分场景下差异不大,一些独特场景下有一些差别,一般可以从如下几个方面来判断。
响应速度
:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。冲突频率
:如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大。重试代价
:如果重试代价大,建议采用悲观锁。
间隙锁、记录锁、临键锁都是Innodb的行锁,前面我们说过行锁是基于索引实现的,一旦加锁操作没有操作在索引上,就会退化成表锁。
间隙锁:作用于非唯一索引上,主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。
如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
记录锁:它封锁索引记录,作用于唯一索引上。
临键锁:作用于非唯一索引上,是记录锁与间隙锁的组合。
死锁是指两个或两个以上事务在执行过程中因争抢锁资源而造成的互相等待的现象。
如何解决死锁?
- 等待事务超时,主动回滚。在InnoDB中,参数innodb_lock_wait_timeout是用来设置超时时间的。
- 进行死锁检查,主动回滚某条事务,让别的事务能继续走下去。
- 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。
- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;如果业务处理不好可以用分布式事务锁或者使用乐观锁。
- 在更新操作时,我们应该尽量使用主键来更新表字段,这样可以有效避免一些不必要的死锁发生。
- 在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁。
- 在允许幻读和不可重复读的情况下,尽量使用RC事务隔离级别,可以避免gap lock(间隙锁) 导致的死锁问题。
- 避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率。
InnoDB是基于索引来完成行锁
。示例 select * from tab_with_index where id = 1 for update
;
for update 可以根据条件来完成行锁锁定
,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁。
在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。
在了解InnoDB锁特性后,用户可以通过设计和SQL调整等措施减少锁冲突和死锁,包括:
- 尽量使用较低的隔离级别; 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
- 选择合理的事务大小,小事务发生锁冲突的几率也更小;给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
- 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
- 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
- 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。
在Read Uncommitted(读未提交)级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突。
在Read Committed(读已提交
)级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁
。
在Repeatable Read(可重复读
)级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁
。
SERIALIZABL(串行化
) 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成
。
MVCC( Multiversion concurrency control ) 就是同一份数据保留多版本的一种方式,进而实现并发控制。在查询的时候,通过read view和版本链找到对应版本的数据。
MVCC作用:提升并发性能。对于高并发场景,MVCC比行级锁开销更小。
MVCC的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。
DB_TRX_ID
:当前事务id,通过事务id的大小判断事务的时间顺序。
DB_ROLL_PRT
:回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成undo log版本链。
DB_ROLL_ID
:主键,如果数据表没有主键,InnoDB会自动生成主键。
每条表记录大概是这样的:
使用事务更新行记录的时候,就会生成版本链,执行过程如下:
- 用排他锁锁住该行;
- 将该行原本的值拷贝到 undo log ,作为旧版本用于回滚;
- 修改当前行的值,生成一个新版本,更新事务 id,使回滚指针指向旧版本的记录,这样就形成一条版本链。
举个例子。
read view可以理解成将数据在每个时刻的状态拍成“照片”记录下来。在获取某时刻t的数据时,到t时间点拍的“照片”上取数据。
在read view内部维护一个活跃事务链表,表示生成read view的时候还在活跃的事务。这个链表包含在创建read view之前还未提交的事务,不包含创建read view之后提交的事务。
不同隔离级别创建read view的时机不同
。
DATA_TRX_ID表示每个数据行的最新的事务ID;up_limit_id表示当前快照中的最先开始的事务; low_limit_id表示当前快照中的最慢开始的事务,即最后一个事务。
总结:InnoDB的MVCC是通过read view和版本链实现的,版本链保存有历史版本记录,通过read view判断当前版本的数据是否可见,如果不可见,再从版本链中找到上一个版本,继续进行判断,直到找到一个可见的版本。
表记录有两种读取方式。
快照读
:读取的是快照版本。普通的SELECT就是快照读。通过mvcc来进行并发控制的,不用加锁。
当前读
:读取的是最新版本。 UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。
快照读情况下,InnoDB通过mvcc机制避免了幻读现象。而mvcc机制无法避免当前读情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。
SELECT的读取锁定主要分为两种方式:共享锁和排他锁。示例:
select * from table where id<6 lock in share mode;--共享锁
select * from table where id<6 for update;--排他锁
这两种方式主要的不同在于LOCK IN SHARE MODE多个事务同时更新同一个表单时很容易造成死锁。
申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。在进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程对这些数据的更改或删除操作会被阻塞(只能读操作),直到该语句的事务被commit语句或rollback语句结束为止。
SELECT… FOR UPDATE使用注意事项:
- for update仅适用于innodb,且必须在事务范围内才能生效。
- 根据主键进行查询,查询条件为like或者不等于,主键字段产生表锁。
- 根据非索引字段进行查询,会产生表锁。
InnoDB行锁是通过给索引上的索引项加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
其实MySQL的并发事务调优和Java的多线程编程调优非常类似,都是可以通过减小锁粒度和减少锁的持有时间进行调优。在MySQL的并发事务调优中,我们尽量在可以使用低事务隔离级别的业务场景中,避免使用高事务隔离级别。