mysql事务与锁
事务隔离级别
sql事务有以下四种隔离级别:
Read Uncommitted(未提交读)
事务T在读取数据的时候并未对数据进行加锁,
事务T在修改数据的时候对数据增加行级共享锁,这种隔离级别会导致-脏读
set tx_isolation='READ-UNCOMMITTED';
Read Committed(已提交读)
事务T在读取数据时增加行级共享锁,读取一旦结束,立即释放;
事务T在修改数据时增加行级排它锁,直到事务结束才释放,解决了-脏读
set tx_isolation='READ-COMMITTED';
Repeatable Read(可重读):
mysql默认的事务隔离级别;
事务T在读取数据时,必须增加行级共享锁,直到事务结束;
事务T在修改数据时,必须增加行级排它锁,直到数据结束;这种隔离级别导致-幻读;
set tx_isolation='REPEATABLE-READ';
Serializable(序列化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。它是在每个读的数据行上加上共享锁,这可能导致大量的超时现象和锁竞争;
事务T在读取数据时,必须增加表级共享锁,直到事务结束才释放;
事务T在修改数据时,必须增加表级排它锁,直到事务结束才释放;
set tx_isolation='SERIALIZABLE';
脏读:
事务A在访问修改记录后(未commit)时,事务B读取并使用记录的情况;
不可重复读:
事务A在事务B访问修改记录前和改记录后读取记录不一致的情况;
幻读:
是指当事务不是独立执行时发生的一种现象,
例如事务A对表中的数据执行了一个范围查询,事务B向表中插入一行新数据,事务A再读取该范围的数据行时就会发现有新的“幻影” 行,InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了幻读;
查看事务隔离级别
select @@tx_isolation;
死锁
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,
它们都将无法再向前推进。
类如:事务A执行先锁数据m,再锁数据n的操作,同时事务B执行先锁数据n,再锁数据m的操作时;
事务A与事务B都需要等待对方释放自己想要的锁才能进行操作,造成互相等待的僵持局面。
两阶段锁协议(2PL)
官方定义:
两阶段锁协议是指所有事务必须分两个阶段对数据加锁和解锁,在对任何数据进行读、写操作之前,事务首先要获得
对该数据的封锁;在释放一个封锁之后,事务不再申请和获得任何其他封锁。
对应到 MySQL 上分为两个阶段:
- 扩展阶段(事务开始后,commit 之前):获取锁
- 收缩阶段(commit 之后):释放锁
就是说呢,只有遵循两段锁协议,才能实现 可串行化调度。
但是两阶段锁协议不要求事务必须一次将所有需要使用的数据加锁,并且在加锁阶段没有顺序要求,所以这种并发控
制方式会形成死锁。
MySQL有两种死锁处理方式: - 等待,直到超时(innodb_lock_wait_timeout=10s)。
发起死锁检测,主动回滚一条事务,让其他事务继续执行(innodb_deadlock_detect=on)。
手动处理死锁
手动删除死锁:
show processlist;
没有看到正在执行的慢SQL记录线程,再去查看innodb的事务表INNODB_TRX,看下里面是否有正在锁定的事务线
程,看看ID是否在show full processlist里面的sleep线程中,如果是,就证明这个sleep的线程事务一直没有commit
或者rollback而是卡住了,我们需要手动kill掉。
SELECT * FROM information_schema.INNODB_TRX;
如果有记录,则找到trx_mysql_thread_id这个字段对应的id, 将其kill掉
kill trx_mysql_thread_id
注意:死锁主要是靠预防,而不是产生后再去手动处理。如何避免发生死锁
- 操作完之后立即提交事务,特别是在交互式命令行中。比如中途不要去请求第三方接口或者执行一些慢逻辑,
能不放到事务里面的php代码, 就别放到事务里面 - 资源一次性分配
一次性锁协议,事务开始时,即一次性申请所有的锁,之后不会再申请任何锁,如果其中某个锁不可用,
则整个申请就不成功,事务就不会执行,在事务尾端,一次性释放所有的锁。一次性锁协议不会产生死锁
的问题,但事务的并发度不高。 - 尽量采用乐观锁,因为悲观锁都是要求mysql锁资源,而乐观锁不是
- 采用超时设置,时间越短,锁等待时间越短。到点就会自动超时,不会继续等待锁释放
锁超时设置:
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET innodb_lock_wait_timeout=10; - 保证没有长事务,长事务尽量拆分成短事务,2个短事务很难重合到一起。就是说,如果一个事务瞬间执行
完毕了,就很好,如果一直没执行完毕, 就很可能另外一个事务冲进来二者重合,就加大死锁几率。 - 修改多个表或者多个行的时候,将修改的顺序保持一致。死锁是因为锁定资源的顺序刚好相反。如果顺序是一
样的,就不会产生死锁。 - 创建索引,可以使创建的锁关联到的数据更少。如果where后面的字段没有索引,哪怕只操作一行数据, 也会
锁整张表, 因为锁是基于索引的。
InnoDB三种行锁算法
1,记录锁(Record-Lock):是加在索引记录上的。
2,间隙锁(Gap Lock):对索引记录间的范围加锁,或者加在最后一个索引记录的前面或者后面
3,Next-Key Lock:前两种锁的结合,锁定一个范围,并且锁定记录本身,主要目的是解决幻读的问题。
间隙锁主要是防止幻读,用在Repeated-Read(简称RR)隔离级别下。
在Read-Commited(简称RC)下,一般没有间隙锁
gap锁或next-key锁的作用:通过锁阻止特定条件的新记录插入,因为插入时也要获取gap锁(Insert Intention
Locks)
只在REPEATABLE READ或以上的隔离级别下的特定操作才会取得gap lock或nextkey lock
For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and DELETE:
锁定取决于语句是否使用具有唯一搜索的唯一索引条件或范围类型搜索条件。
对于具有唯一搜索条件的唯一索引,InnoDB只锁定找到的索引记录,而不锁定它前面的间隙。
对于其他搜索条件,InnoDB锁定扫描的索引范围,使用gap lock或nextkey lock来阻止通过其他会话进入范围所覆盖的间隙插入记录
where 字段与索引条件
1 如果where后面的字段是普通索引,就会加一个锁住[m-n)之间的数据的间隙锁
2 如果where后面的字段是唯一键索引,就不会加任何间隙锁
3 如果where后面的字段没有加索引,全表范围内加上间隙锁
如何防止间隙锁:
1 尽量采用乐观锁,乐观锁是在php等代码层面的锁,就不会锁住数据库资源
2 事务中update,where后面的字段尽量带上索引,不然间隙锁的范围很大
3 尽量不要出现长事务,否则事务中更新订单时间隙锁会被锁很久,另一事务插入订单就会执行很久
4 update订单表,begin和commit之间的时间不要太长,之间不要写一些慢代码,比如请求第三方接口
5 分表能防止不分表情况下整张表被锁住,分表后是锁住众多表中的其中一。