SQL优化-锁问题

不同的存储引擎支持不同的锁机制 

MyISAM MEMORY支持表级锁

BDB存储索引采用页面锁+表级锁

InnoDB支持行级锁 (default)表级锁

--锁的特性

表级锁:开销小 加锁快 不会出现死锁 锁定粒度大 发生锁冲突概率最高 并发读最低

行级锁:开销大 加锁慢 会出现死锁 锁定粒度最小 发生锁冲突概率最低 并发度最高

页面锁:开销和加锁时间介于表级锁 行级锁之间 会出现死锁 锁定粒度介于两者之间

具体应用的特点来说哪种锁更合适

仅从锁的角度:

表级锁适合查询为主 只有少量按索引条件更新数据的应用 WEB应用

行级锁更适合大量按索引条件并发更新少量不同数据 同时又有并发查询的应用     OLTP应用

MyISAM表锁

--查看表锁争用情况

值越高 表名存在较严重的表级锁争用情况

show status like 'table%';

--表级锁的锁模式

表共享锁和表独占写锁

读操作的时候 不会阻塞其他用户对同一表的读请求 会阻止对同一表的写请求

写操作的时候 会阻塞其他用户对同一表的读写操作 

读和写操作之间 写操作之间是串行的

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE 命令给MyISAM 表显式加锁。

--如何加表锁

Lock tables orders read local, order_detail read local;

Select sum(total) from orders;

Select sum(subtotal) from order_detail;

Unlock tables;

要特别说明以下两点内容。

¡ 上面的例子在LOCK TABLES 时加了“local”选项,其作用就是在满足MyISAM 表并发插入条件的情况下,允许其他用户在表尾并发插入记录,有关MyISAM 表的并发插入问题,在后面的章节中还会进一步介绍。

¡ 在用LOCK TABLES 给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL 不支持锁升级。也就是说,在执行LOCK TABLES 后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。其实,在自动加锁的情况下也基本如此,MyISAM 总是一次获得SQL 语句所需要的全部锁。这也正是MyISAM 表不会出现死锁(Deadlock Free)的原因。

当使用LOCK TABLES 时,不仅需要一次锁定用到的所有表,而且,同一个表在SQL 语句中出现多少次,就要通过与SQL 语句中相同的别名锁定多少次,否则也会出错。

并发插入

一定条件下,MyISAM表也支持查询和插入操作的并发进行。

MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。

l 当concurrent_insert设置为0时,不允许并发插入。

l 当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。

l 当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。

锁调度:先写进程获得锁 后来读锁  MySQL认为写请求比读请求重要 MyISAM表不太适合有大量更新操作和查询操作应用的原因。大量的更新操作造成查询无法获得读锁 可能永远阻塞

设置MyISAM的调度行为

通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。

通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。

通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。

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

InnoDB行锁

事务(Transaction)及其ACID 属性

事务是由一组SQL 语句组成的逻辑处理单元,事务具有以下4 个属性,通常简称为事务的ACID 属性。

l 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。

l 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B 树索引或双向链表)也都必须是正确的。

l 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。

l 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发事务处理带来的问题

更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题

--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。

l 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"

l 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。

l 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

事务隔离级别

“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本上可分为以下两种。

l 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。

l 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MCC),也经常称为多版本数据库。

数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。

--查看innodb锁争用情况

show status like 'innodb_row_lock%';

如果发现锁争用比较严重,如InnoDB_row_lock_waits 和InnoDB_row_lock_time_avg 的值比较高,还可以通过设置InnoDB Monitors 来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。

1--

select * from information_schema.innodb_locks\G;

select * from information_schema.innodb_lock_waits \G;

2--

CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;

Show engine innodb status\G;

InnoDB 实现了以下两种类型的行锁。

l 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。

l 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。

l 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS 锁。

l 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX 锁。

如果一个事务请求的锁模式与当前的锁兼容,InnoDB 就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。

意向锁是InnoDB 自动加的,不需用户干预。对于UPDATE、DELETE 和INSERT 语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT 语句,InnoDB 不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。(Exclusive lock)

¡ 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。

¡ 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE

当使用SELECT...FOR UPDATE加锁后再更新记录(update insert delete使用到Exclusive Lock)

InnoDB 行锁实现方式

InnoDB 行锁是通过给索引上的索引项加锁来实现的。InnoDB 这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!

在实际应用中,要特别注意InnoDB 行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

--example

只是锁住了行 ---而不是表

2)由于MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。

(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。

(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL 通过判断不同执行计划的代价来决定的,如果MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB 将使用表锁,而不是行锁。

--example

mysql> alter table tab_no_index add index name(name);

mysql> explain select * from tab_with_index where name = 1 \G;

间隙锁(Next-Key 锁)

当用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key 锁)。

恢复和复制的需要,对InnoDB 锁机制的影响

MySQL 通过BINLOG 录执行成功的INSERT、UPDATE、DELETE 等更新数据的SQL 语句,并由此实现MySQL 数据库的恢复和主从复制

MySQL 的恢复机制(复制其实就是在Slave Mysql 不断做基于BINLOG 的恢复)有以下特点。

l 一是MySQL 的恢复是SQL 语句级的,也就是重新执行BINLOG 中的SQL 语句。这与Oracle 数据库不同,Oracle 是基于数据库文件块的。

l 二是MySQL 的Binlog 是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。

这点也与Oralce 不同,Oracle 是按照系统更新号(System Change Number,SCN)来恢复数据的,每个事务开始时,Oracle 都会分配一个全局唯一的SCN,SCN 的顺序与事务开始的时间顺序是一致的。

MySQL支持3种日志格式 基于语句的日志格式SBL 基于行的日志格式RBL 混合格式

--支持4种复制模式

基于SQL语句的复制SBR:最早支持的复制模式

基于行数据的复制RBR:支持非安全SQL复制

混合复制模式:对安全SQL语句采用基于SQL语句的复制模式 对非安全的SQL语句采用居于行的复制模式

使用全局事务ID的复制:主要解决主从自动同步问题

MySQL 的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读,这已经超过了ISO/ANSISQL92“可重复读”隔离级别的要求,实际上是要求事务要串行化。

对于“insert into target_tab select * from source_tab where ...”和“create table new_tab ...select ... From source_tab where ...(CTAS)”这种SQL 语句,用户并没有对source_tab 做任何更新操作,但MySQL 对这种SQL 语句做了特别处理。

以上只是简单地读source_tab 表的数据,相当于执行一个普通的SELECT语句,用一致性读就可以了   

oracle它通过MVCC 技术实现的多版本数据来实现一致性读,不需要给source_tab 加任何锁

InnoDB 也实现了多版本数据,对普通的SELECT 一致性读,也不需要加任何锁;但这里InnoDB 却给source_tab加了共享锁,并没有使用多版本数据一致性读技术

原因:为了保证恢复和复制的正确性。因为不加锁的话,如果在上述语句执行过程中,其他事务对source_tab 做了更新操作,就可能导致数据恢复的结果错误。

查看binlog日志的时候

从上可见,设置系统变量innodb_locks_unsafe_for_binlog的值为“on”后,InnoDB不再对source_tab加锁,结果也符合应用逻辑,但是如果分析BINLOG的内容:

可以发现,在BINLOG中,更新操作的位置在INSERT...SELECT之前,如果使用这个BINLOG进行数据库恢复,恢复的结果与实际的应用逻辑不符;如果进行复制,就会导致主从数据库不一致!(MVCC时间点快照 不与逻辑顺序一致)

通过上面的例子,我们就不难理解为什么MySQL 在处理“Insert into target_tab select * from source_tab where ...”和“create table new_tab ...select ... From source_tab where ...”时要给source_tab 加锁,而不是使用对并发影响最小的多版本数据来实现一致性读。还要特别说明的是,如果上述语句的SELECT 是范围条件,InnoDB 还会给源表加间隙锁(Next-Lock)。

因此,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 在不同隔离级别下的一致性读及锁的差异

对于许多SQL,隔离级别越高,InnoDB 给记录集加的锁就越严格(尤其是使用范围条件的时候),产生锁冲突的可能性也就越高,从而对并发性事务处理性能的影响也就越大。因此,我们在应用中,应该尽量使用较低的隔离级别,以减少锁争用的机率。

实际上,通过优化事务逻辑,大部分应用使用Read Commited 隔离级别就足够了。

对于一些确实需要更高隔离级别的事务,可以通过在程序中执行SET SESSION TRANSACTION   ISOLATIONLEVEL REPEATABLE READ 或SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE 动态改变隔离级别的方式满足需求。

什么时候使用表锁

对于InnoDB 表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB 表的理由。但在个别特殊事务中,也可以考虑使用表级锁。

¡ 第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。

¡ 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

在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 释放表锁。

关于死锁

死锁

所谓死锁<DeadLock>: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等竺的进程称为死锁进程.表级锁不会产生死锁.所以解决死锁主要还是真对于最常用的InnoDB.

遇到死锁的处理方式

mysql -uxxx -pxxx -h服务器ip --port=服务器端口;(如果服务器设置了ip和端口访问的话,一定要带ip和端口)

mysql> show processlist; #查看正在执行的sql (show full processlist;查看全部sql)

mysql> kill id #杀死sql进程;

如果进程太多找不到,就重启mysql吧

/ect/init.d/mysql restart 

或/ect/init.d/mysql stop(如果关不掉就直接kill -9 进程id)  再/ect/init.d/mysql start 

去看看mysql日志文件是否保存死锁日志:

——————————————————————————————————

发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁,这需要通过设置锁等待超时参数innodb_lock_wait_timeout 来解决。需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。

通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL 语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法。

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

在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁

前面讲过,在REPEATABLE-READ 隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE 加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题

当隔离级别为READ COMMITTED 时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1 个线程提交后,第2 个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3 个线程又来申请排他锁,也会出现死锁。

可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK 释放获得的排他锁

如果出现死锁,可以用SHOW INNODB STATUS 命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。

mysql> show innodb status \G;

猜你喜欢

转载自blog.csdn.net/ichglauben/article/details/81231152