mysql 锁机制、事务

锁机制

三大锁

表锁、行锁、页锁

锁类型。

  1. MyISAM 和 Memory 存储引擎使用的是表级锁,BDB 引擎使用的是页级锁,也支持表级锁。由于 BDB 引擎基本已经成为历史,因此就不再介绍了。
  2. InnoDB 存储引擎既支持行级锁,也支持表级锁,默认情况下使用行级锁。
  3. 表级锁,它直接锁住的是一个表,开销小,加锁快,不会出现死锁的情况,锁定粒度大,发生锁冲突的概率更高,并发度最低。
  4. 行级锁,它直接锁住的是一条记录,开销大,加锁慢,发生锁冲突的概率较低,并发度很高。
  5. 页级锁,它是锁住的一个页面,在 InnoDB 中一个页面为16KB,它的开销介于表级锁和行级锁中间,也可能会出现死锁,锁定粒度也介于表级锁和行级 锁中间,并发度也介于表级锁和行级锁中间。
  6. 仅仅从锁的角度来说,表级锁更加适合于以查询为主的应用,只有少量按照索引条件更新数据的应用
  7. 行级锁更适合大量按照索引条件并发更新少量不同的数据,同时还有并发查询的应用

InnoDB有两种类型的行级锁,两种内部使用的意向锁;

共享锁(S):允许一个事务读一行数据时,阻止其他的事务读取相同数据的排他锁。
排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据的共享锁和排他锁。 意向共享锁(IS):事务打算给数据行加行共享锁。事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁。事务在给一个数据行加排他锁前必须先取得该表的IX锁。

意向锁是InnoDB存储引擎自动加的,对于普通select语句,InnoDB不会加任何锁,对于insert,update,delete语句,InnoDB会自动改涉及的数据加排他锁,
InnoDB以通过以下语句显示添加的共享锁和排他锁。
共享锁语句
select * from table_name lock in share mode;
排他锁语句
select * from table_name for update;

加上共享锁的时候,排他锁就会等待

排它锁:锁住的这一条数据不能被别的锁而操作(读和写)
共享锁:锁住的这条数据可以被 共享锁操作(读)
都不会影响普通的查询

insert、update、delete 默认都会加上排它锁,普通查询不加锁

死锁

死锁是由于两个或以上的线程互相持有对方需要的资源,导致这些线程处于等待状态,无法执行。
共享锁 只可读,不可写 与任和锁一起使用都不会冲突(除了排它锁)
排它锁 不可读也不可写 与任何锁一起使用时都会冲突

产生死锁的四个必要条件

  1. 互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
  2. 请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
  3. 不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。
  4. 循环等待:发生死锁时,线程进入死循环,永久阻塞。

对于锁的建议

收集死锁信息:
利用命令 SHOW ENGINE INNODB STATUS查看死锁原因。
调试阶段开启 innodb_print_all_deadlocks,收集所有死锁日志。

减少死锁:
使用事务,不使用 lock tables 。
保证没有长事务。
操作完之后立即提交事务,特别是在交互式命令行中。
如果在用 (SELECT … FOR UPDATE or SELECT … LOCK IN SHARE MODE),尝试降低隔离级别。 修改多个表或者多个行的时候,将修改的顺序保持一致。
创建索引,可以使创建的锁更少。
最好不要用 (SELECT … FOR UPDATE or SELECT … LOCK IN SHARE MODE)。

InnoDB 行锁、表锁、innodb间隙锁

InnoDB 行级锁是通过给索引上的索引项加锁来实现的,InnoDB行级锁只有通过索引条件检索数据,才使用行级锁;否则,InnoDB使用表锁 在不通过索引(主键)条件查询的时候,InnoDB是表锁而不是行锁。
在没有使用索引的情况下InnoDB就会使用表级锁(共享锁不会有这个情况)
如果查询的条件没有带索引,那么行锁则会转为表锁即时表中字段有主键;所以在查询的时候建议使用索引字段查询

innodb间隙锁
可以理解为是对于一定范围内的数据进行锁定,如果说这个区间没有这条数据的话也是会锁住的;主要是解决幻读的问题,如果没有添加间隙锁,如果其他事物中
添加id在1到100之间的某条记录,此时会发生幻读;另一方面,视为了满足其恢复和赋值的需求。

默认情况下,innodb_locks_unsafe_for_binlog是0(禁用),这意味着启用了间隙锁定:InnoDB使用下一个键锁进行搜索和索引扫描。若要启用该变量,请将其设 置为1。

事务

回滚日志

想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,而在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的,所有事务进行的
修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。

重做日志

与原子性一样,事务的持久性也是通过日志来实现的,MySQL 使用重做日志(redo log)实现事务的持久性,重做日志由两部分组成,一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以它是易失的,另一个就是在磁盘上的重做日志文件,它是持久的。

回滚日志(undo log)和重做日志(redo log);在数据库系统中,事务的原子性和持久性是由事务日志 (transaction log)保证的,在实现时也就是上面提到的两种日志,前者用于对事务的影响进行撤销,后者在错误处理时对已经提交的事务进行重做,它们能保 证两点:

  1. 发生错误或者需要回滚的事务能够成功回滚(原子性);
  2. 在事务提交后,数据没来得及写会磁盘就宕机时,在下次重新启动后能够成功恢复数据(持久性);

事务的隔离级别

数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable

READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

隔离级别 脏读 不可重复读 幻影读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×
1
2
3
4
5
6
7
show variables like '%ios%'

-- 局部 @@session
-- 全局 @@global

set @@session.tx_ioslation = 'READ-COMMITTED';
set @@session.tx_ioslation = 'REPEATABLE-READ';

脏读(一个事务读取到另一个事务未提交的数据)
不可重复读(一个事务读取到另一个事务已经提交的数据)
幻读(一个事务多次查询整表数据,由于其他事务新增(删除)记录造成多次查询的记录条数不同(一个事务读取到另一个事务已经提交的数据))

事务不会自动回滚。

对于事务的建议 innodb存储引擎由于实现了行几所,颗粒更小,实现更复杂。但是innodb行锁在并发性能上远远要高于表锁页锁。在使用方面可以尽量做到以下几点;

  1. 控制事务大小,减少锁定的资源量和锁定时间长度。
  2. 人所有的数据检索都通过索引来完成,从而避免因为无法通过索引加锁而升级为表锁。
  3. 减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的数据。
  4. 在业务条件允许下,尽量使用较低隔离级别的事务隔离。减少隔离级别带来的附加成本。
  5. 使用索引,让innodb在索引上面加锁的时候更加准确。
  6. 在应用中尽可能做到访问的顺序执行