MySQL优化总结--锁问题

MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制

MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);

BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;

InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

从锁的角度来说:

表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web 应用;

而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理                          (OLTP)系统。BDB 已经被 InnoDB 取代。

MyISAM 表锁

查询表级锁争用情况
通过检查 table_locks_waited 和 table_locks_immediate 状态变量来分析系统上的表锁定争夺:
mysql> show status like 'table%';

MySQL 表级锁的锁模式
两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。
 

MyISAM 表的读操作与写操作之间,以及写操作之间是串行的。
LOCK TABLES 可以锁定用于当前线程的表。
UNLOCK TABLES 可以释放当前线程获得的任何锁定。

如何加表锁
MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。
在用 LOCK TABLES 给表显式加表锁时,必须同时取得所有涉及到表的锁,并且 MySQL 不支持锁升级。
在自动加锁的情况下MyISAM 总是一次获得 SQL 语句所需要的全部锁。这也正是 MyISAM 表不会出现死锁(Deadlock Free)的原因。
当使用 LOCK TABLES 时,不仅需要一次锁定用到的所有表,而且,同一个表在 SQL 语句中出现多少次,就要通过与 SQL 语句中相同的别名锁定多少次,否则也会出错。
需要对别名分别锁定:
mysql> lock table actor as a read,actor as b read;

并发插入(Concurrent Inserts)
MyISAM 表的读和写是串行的,但这是就总体而言的。在一定条件下,MyISAM表也支持查询和插入操作的并发进行。
MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。
 当concurrent_insert设置为0时,不允许并发插入。
 当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的       同时,另一个进程从表尾插入记录。这也是MySQL的默认设置
 当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。

MyISAM 的锁调度

MyISAM 表不太适合于有大量更新操作和查询操作应用的原因
因为 MySQL 认为写请求一般比读请求要重要,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。

通过一些设置来调节 MyISAM 的调度行为:

 通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
 通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。
 通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。

折中的办法来调节读写冲突
给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,
给读进程一定获得锁的机会。

InnoDB 锁问题

InnoDB 与 MyISAM 的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。

并发事务处理带来的问题
更新丢失(Lost Update)、脏读(Dirty Reads)、不可重复读(Non-Repeatable Reads)、幻读(Phantom Reads)



获取 InnoDB 行锁争用情况
通过检查 InnoDB_row_lock 状态变量来分析系统上的行锁的争夺情况:
mysql> show status like 'innodb_row_lock%';
如果发现锁争用比较严重,如 InnoDB_row_lock_waits 和 InnoDB_row_lock_time_avg 的值比较高,还可以通过设置 InnoDB Monitors 来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
具体方法如下:
mysql> CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;
然后就可以用下面的语句来进行查看:
mysql> Show innodb status\G;

InnoDB 的行锁模式及加锁方法
InnoDB 实现了以下两种类型的行锁。
 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意锁(Intention Locks),这两种意向锁都是表锁。
 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。

如果一个事务请求的锁模式与当前的锁兼容,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 行锁实现方式
InnoDB 行锁是通过给索引上的索引项加锁来实现的,这点 MySQL 与 Oracle 不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!
(1)在不通过索引条件查询的时候,InnoDB 确实使用的是表锁,而不是行锁。
(2)由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索             引键,是会出现锁冲突的。
(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索             引,InnoDB 都会使用行锁来对数据加锁。
(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定,如果                    MySQL 认为全表扫描效率更高,比如对一些很小的表它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。
(5)检索值的数据类型与索引字段不同,虽 MySQL 能够进行数据类型转换,但却不会使用索引,从而导致 InnoDB 使用表锁。

间隙锁(Next-Key 锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key 锁)。
InnoDB 使用间隙锁的目的,一方面是为防止幻读,以满足相关隔离级别的要求;另外一方面,是为满足其恢复和复制的需要。

带来的问题

在使用范围条件检索并锁定记录时,InnoDB 这种加锁机制会阻塞符合条件范围内键值的并发插入,这会造成严重的锁等待。

InnoDB 除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB 也会使用间隙锁!

恢复和复制的需要,对 InnoDB 锁机制的影响
MySQL 的恢复机制(复制其实就是在 Slave Mysql 不断做基于 BINLOG 的恢复)有以下特点:

 一是 MySQL 的恢复是 SQL 语句级的,也就是重新执行 BINLOG 中的 SQL 语句。这与Oracle 数据库不同,Oracle 是基于数      据库文件块的。
 二是 MySQL 的 Binlog 是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。这点也与 Oralce 不同,Oracle 是按    照系统更新号(System Change Number,SCN)来恢复数据的,每个事务开始时,Oracle 都会分配一个全局唯一的 SCN,      SCN 的顺序与事务 开始的时间顺序是一致的。
INSERT...SELECT...和CREATE TABLE...SELECT...语句,可能会阻止对源表的并发更新,造成对源表锁的等待。如果查询比较复杂的话,会造成严重的性能问题,我们在应用中应尽量避免使用。实际上,MySQL将这种SQL叫作不确定(non-deterministic)的SQL,不推荐使用。
如果应用中一定要用这种 SQL 来实现业务逻辑,又不希望对源表的并发更新产生影响,可以采取以下两种措施:
 一是采取上面示例中的做法,将 innodb_locks_unsafe_for_binlog 的值设置为“on”,强制 MySQL 使用多版本数据一致性读。       但付出的代价是可能无法用 binlog 正确地恢复或复制数据,因此,不推荐使用这种方式。
 二是通过使用“select * from source_tab ... Into outfile”和“load data infile ...”语句组合来间接实现,采用这种方式 MySQL 不会       给 source_tab 加锁。

什么时候使用表锁
 第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造     成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及     的表,从而避免死锁、减少数据库因事务回滚带来的开销。
在 InnoDB 下,使用表锁要注意以下两点。
(1)使用 LOCK TABLES 虽然可以给 InnoDB 加表级锁,但必须说明的是,表锁不是由 InnoDB存储引擎层管理的,而是由其上         一层──MySQL Server 负责的,仅当 autocommit=0、innodb_table_locks=1(默认设置)时,InnoDB 层才能知道 MySQL         加的表锁,MySQL Server也才能感知 InnoDB 加的行锁,这种情况下,InnoDB 才能自动识别涉及表级锁的死锁;否则,             InnoDB 将无法自动检测并处理这种死锁。有关死锁,下一小节还会继续讨论。
(2)在用 LOCK TABLES 对 InnoDB 表加锁时要注意,要将 AUTOCOMMIT 设为 0,否则MySQL 不会给表加锁;事务结束               前,不要用 UNLOCK TABLES 释放表锁,因为 UNLOCK TABLES会隐含地提交事务;COMMIT 或 ROLLBACK 并不能释           放用 LOCK TABLES 加的表级锁,必须用UNLOCK TABLES 释放表锁。

正确的方式见如下语句:
例如,如果需要写表 t1 并从表 t 读,可以按如下做:
SET AUTOCOMMIT=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
[do something with tables t1 and t2 here];
COMMIT;
UNLOCK TABLES;

关于死锁
避免死锁的常用方法:

(1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会;

(2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现            死锁的可能。

(3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为            当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
(4)在 REPEATABLE-READ 隔离级别下,如果两个线程同时对相同条件记录用 SELECT...FOR UPDATE 加排他锁,在没有符          合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么                做,就会出现死锁。这种情况下,将隔离级别改成 READ COMMITTED,就可避免问题。

(5)当隔离级别为 READ COMMITTED 时,如果两个线程都先执行 SELECT...FOR UPDATE,判断是否存在符合条件的记               录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第 1 个线程提交后,第 2 个           线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第 3 个线程又来申请排他锁,也会出现死         锁。对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行 ROLLBACK 释放         获得的排他锁。
如果出现死锁,可以用 SHOW INNODB STATUS 命令来确定最后一个死锁产生的原因。

总结

对于 MyISAM 的表锁,主要讨论了以下几点:
(1)共享读锁(S)之间是兼容的,但共享读锁(S)与排他写锁(X)之间,以及排他写锁(X)之间是互斥的,也就是说读和          写是串行的。
(2)在一定条件下,MyISAM 允许查询和插入并发执行,可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
(3)MyISAM 默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置 LOW_PRIORITY_UPDATES 参数,          或在 INSERT、UPDATE、DELETE 语句 中指定LOW_PRIORITY 选项来调节读写锁的争用。
(4)由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑          采用 InnoDB 表来减少锁冲突。
对于 InnoDB 表,本章主要讨论了以下几项内容:
 InnoDB 的行锁是基于锁引实现的,如果不通过索引访问数据,InnoDB 会使用表锁。
 介绍了 InnoDB 间隙锁(Next-key)机制,以及 InnoDB 使用间隙锁的原因。
 在不同的隔离级别下,InnoDB 的锁机制和一致性读策略不同。
 MySQL 的恢复和复制对 InnoDB 锁机制和一致性读策略也有较大影响。
 锁冲突甚至死锁很难完全避免。
在了解 InnoDB 锁特性后,可以通过设计和 SQL 调整等措施减少锁冲突和死锁,包括:
 尽量使用较低的隔离级别;
 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
 选择合理的事务大小,小事务发生锁冲突的几率也更小;
 给记录集显示加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修     改时再请求排他锁,这样容易产生死锁;
 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以     大大减少死锁的机会;
 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响;
 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。
 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值