mysql如何保证ACID 参考

数据库通过原子性(A)、隔离性(I)、持久性(D)来保证一致性(C)。 其中一致性是目的,原子性、隔离性、持久性是手段 。因此数据库必须实现AID三大特性才有可能实现一致性。

MySQL如何保证原子性

利用InnoDB的undo log
undo log(回滚日志)记录需要回滚的日志信息,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句

MySQL如何保证持久性

利用redo log日志

mysql修改数据会先把数据加载到内存中,在内存中对数据进行修改,然后刷回磁盘,如果此时突然宕机,内存中的数据就会丢失。
解决办法 事务提交前直接把数据写入磁盘,
1.浪费资源,只修改一个字节,就把整个页面输入磁盘
2.速度慢,每个事务可能涉及多个数据页的修改,这些数据可能不相邻,属于随机操作IO
在事务提交的时候,将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘中)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和bin log内容决定回滚数据还是提交数据。

mysql如何保证隔离性

利用锁和MVCC机制

MVCC即多版本并发控制,一个行记录数据有多个版本对快照数据,这些快照数据在undo log中。
如果一个事务读取的行正在做DELETE或者UPDATE操作,读取操作不会等行上的锁释放,二是读取该行的快照版本。
在事务隔离级别为读已提交时,一个事务能够读到另一个事务已经提交的数据,是不满足隔离性的。但是当事务隔离级别为可重复读中,是满足隔离性的。

同一事务中未提交的写能读到吗 参考

读未提交 在事务中能读取到其他事务对数据的更改。
读已提交 在事务中读取不到其他事务对数据的修改,无论修改的操作是否在事务中,还是事务未提交或已提交,都无法获取到数据的修改,只能读取到原数据(快照时刻的数据)。
重复读 在事务中不能读取到其他事务(包括事务提交以及不提交)对数据的修改,只能读取到原始数据(快照时刻的数据),但是能读取到新增的数据。

mysql可重复读,如何解决幻读 参考

对于快照读,mysql使用mvcc利用历史数据部分避免了幻读(在某些场景看上去规避了幻读),要完全避免,需要手动加锁将快照读调整为当前读(mysql不会自动加锁),然后mysql使用next-key完全避免了幻读

可重复读避免幻读现象(并不是完全解决了),针对 当前读 (select...for update等语句),是 通过next-key lock(记录锁+间隙锁)方式解决了幻读 ,因为当执行select...for update语句的时候,会加上next-key lock,如果有其他事务在next-key lock锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以避免幻读问题。

快照读是如何避免幻读的?

可重复读隔离级是由MVCC(多版本并发控制)实现的,实现的方式是启动事务后,在执行第一个查询语句后,会创建一个Read View, 后续的查询语句利用这个Read VIew,通过这个Read View就可以在undo log版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的 ,即使中途有其他事务插入了新纪录,是查询不出来这条记录的,所以很好避免幻读问题。

当前读是如何避免幻读的?

mysql里除了普通查询都是快照读,其他都是 当前读 ,比如update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
另外, select...for update这种查询语句都是当前读,每次执行的时候都是读取最新的数据。
** 假设select...for update当前读是不会加锁的(实际上是会加锁的)。
这时,事务B插入的记录就会被事务A的第二条查询语句查询到(因为是当前读),这样就会出现前后两次查询的结果及不一样,这就出现了幻读。
所以,
Innodb引擎为了解决[可重复读]隔离级别使用[当前读]而造成的幻读问题,就引出了间隙锁**。
假设,表中有一个范围id为(3,5)间隙锁,那么其他事务就无法插入id=4这条记录,进而防止幻读发生。

v2-8a7baa97216d92a894bb985cc2dc7dff_720w.jpg
事务 A 执行了这面这条锁定读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁+记录锁的组合)。

然后, 事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务 。这就避免了由于事务 B 插入新记录而导致事务 A 发生幻读的现象。

并发写问题是如何通过行锁解决的?

事务A 更新表A中的id=1的行,事务B也进行更新表A中的id=1的行,但事务A未commit,事务B就会等待,此时的锁机制是怎么样的?如何加锁的

加锁过程分有索引和无索引两种情况 ,例如 update test4 set name="k8svip" where id=1; id是索引列 ,那么MySQL就在索引中直接找到这行数据,直接对行加锁即可。而如果是 update test4 set name="k8svip" where age=18; 而 age不是索引列 的时候,MySQL就无法定位到这行数据,那怎么处理呢?InnoDB也不是给表表锁,而是会把这张表中的 所有行加行锁 ,但是呢,加上行锁后,MySQL 会进行一次遍历过滤,发现不满足的行就释放锁,最终只保留符合条件的行,虽然最终只为符合条件的行加了行锁,但是这一锁一释放的过程对大表的性能影响极大,大表的话,还是建议合理建立索引。
如果是没有索引的话,所有可加间隙锁的地方,都会加上间隙锁。

幻读未被解决。

可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有完全解决幻读。
在一种情况下,A开启了事务,对student表进行了一次查询,有四条数据,此时B开启了事务对student表插入了一条数据,并提交了事务,此时A事务未提交,进行第二次查询,查询结果没有B事务插入的记录,此时A插入了一条数据,即进行了一次“当前读”操作,再次查询表就发现了B事务插入的数据,即未解决幻读问题。

在事务中执行了Select * from student for update;就对表进行了锁,其他事务的插入操作会阻塞,但是其他事务能执行查询操作。此外,如果其他事物执行insert操作,那么Select * from student for update;会处于阻塞状态

update没加索引锁全表(在事务中)

问题描述 :在线上执行一条 update 语句修改数据库数据的时候,where 条件没有带上索引,导致业务直接崩了

前提,接下来说的案例都是基于 InnoDB 存储引擎,且事务的隔离级别是可重复读

InnoDB 存储引擎自己实现了行锁,通过 next-key 锁(记录锁和间隙锁的组合)来锁住记录本身和记录之间的“间隙”,防止其他事务在这个记录之间插入新的记录,从而避免了幻读现象。

当我们执行update语句时,实际上是会对记录加独占锁(X锁)的,如果其他事务对持有独占锁的记录进行修改时是会被阻塞的,另外,这个锁并不是执行完update语句就会释放的,二是会等事务结束时才会释放
在 InnoDB 事务中,对记录加锁带基本单位是 next-key 锁,但是会因为一些条件会退化成间隙锁,或者记录锁。加锁的位置准确的说,锁是加在索引上的而非行上。

比如,在 update 语句的 where 条件使用了唯一索引,那么 next-key 锁会退化成记录锁,也就是只会给一行记录加锁。

但是,在 update 语句的 where 条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了。
这是因为事务 A的 update 语句中 where 条件没有索引列,触发了全表扫描,在扫描过程中会对索引加锁,所以全表扫描的场景下,所有记录都会被加锁,也就是这条 update 语句产生了 4 个记录锁和 5 个间隙锁,相当于锁住了全表。

那 update 语句的 where 带上索引就能避免全表记录加锁了吗?
关键还得看这条语句在执行过程中,优化器最终选择的是索引扫描,还是全表扫描,如果走了全表扫描,就会对全表的记录加锁了。

如何避免这种事故的发生(针对事务开启的情况下update才会锁表)

我们可以打开 MySQL sql_safe_updates 参数将其设置为1,这样可以预防 update 操作时 where 条件没有带上索引列。
update语句必须满足如下条件之一才能执行成功:

  • 使用where,并且where条件中必须有索引列;
  • 使用limit
  • 同时使用where和limit,此时where条件中可以没有索引列;
    delete语句必须满足以下条件:
  • 同时使用where和limit,此时where条件中可以没有索引列
  • 如果 where 条件带上了索引列,但是优化器最终扫描选择的是全表,而不是索引的话,我们可以使用 force index([index_name]) 可以告诉优化器使用哪个索引,以此避免有几率锁全表带来的隐患。

    开启sql_safe_updates安全模式

    在update操作中:当where条件中列(column)没有索引可用且无limit限制时会拒绝更新。where条件为常量且无limit限制时会拒绝更新。

    在delete操作中: 当①where条件为常量,②或where条件为空,③或where条件中 列(column)没有索引可用且无limit限制时拒绝删除。

    mysql> update users set status=1; ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column

    mysql 死锁,怎么办 参考

    排它锁 (X锁)和共享锁 (S锁).

    前提条件,没有开启死锁检测
    假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:

    90c1e01d0345de639e3426cea0390e80.png

    可以看到,两个事务都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。

    为什么会产生死锁?

    Innodb 引擎为了解决「可重复读」隔离级别下的幻读问题,就引出了 next-key 锁 ,它是记录锁和间隙锁的组合。

  • Record Lock,记录锁,锁的是记录本身;
  • Gap Lock,间隙锁,锁的就是两个值之间的空隙,以防止其他事务在这个空隙间插入新的数据,从而避免幻读现象。
    普通的 select 语句是不会对记录加锁的,因为它是通过 MVCC 的机制实现的快照读,如果要在查询时对记录加行锁,可以使用下面这两个方式:
  • begin;
    //对读取的记录加共享锁
    select ... lock in share mode;
    commit; //锁释放
    begin;
    //对读取的记录加排他锁
    select ... for update;
    commit; //锁释放
    

    行锁的释放时机是在事务提交(commit)后,锁就会被释放,并不是一条语句执行完就释放行锁。

    我们可以通过 select * from performance_schema.data_locks\G; 这条语句,查看事务执行 SQL 过程中加了什么锁。

    行锁:X 类型的间隙锁;

    这里我们重点关注行锁,图中 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思,通过 LOCK_MODE 可以确认是 next-key 锁,还是间隙锁,还是记录锁:

    如果 LOCK_MODE 为 X,说明是 X 型的 next-key 锁;
    如果 LOCK_MODE 为 X, REC_NOT_GAP,说明是 X 型的记录锁;
    如果 LOCK_MODE 为 X, GAP,说明是 X 型的间隙锁;
    

    当事务 B 往事务 A next-key 锁的范围 (1006, +∞] 里插入 id = 1008 的记录就会被锁住,因为当我们执行以下插入语句时,会在插入间隙上获取插入意向锁,而插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。而间隙锁与间隙锁之间是兼容的,所以所以两个事务中 select ... for update 语句并不会相互影响。

    间隙锁之间是兼容的(这里的兼容指的是select...for update),间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。
    这里的共同间隙包括两种场景(这里的兼容指的是select...for update):

    其一是两个间隙锁的间隙区间完全一样;
    其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。
    

    但是有一点要注意,next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。

    插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作
    另外,我补充一点,插入意向锁的生成时机:
    每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,那 Insert 语句应该被阻塞,并生成一个插入意向锁 。

    insert 怎么加行级锁

    Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为隐式锁来保护记录的。
    隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景:

    如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的;
    如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录;
    

    第一个场景就是insert遇到间隙锁会阻塞。
    第二个场景事务A执行insert隐式锁,事务B执行相同的inset插入语句,事务A上的隐式锁会变显示锁且锁类型为X类型的记录锁,所以事务B向获取S型next-key锁时会遇到锁冲突,事务B进入阻塞状态。 X型锁和S型锁冲突

    如何避免死锁

    死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。
    死锁发生两个避免策略:

    设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。
    当发生超时后,就出现下面这个提示:

    开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。
    当检测到死锁后,就会出现下面这个提示:
    上面这个两种策略是「当有死锁发生时」的避免方式。

  • 回归业务角度,对订单进行幂等性校验保证不会出现重复的订单,将order_no字段设置为唯一索引列,利用它的唯一性保证订单表不会出现重复的订单。

  • 面试题,加了什么锁导致死锁

    插入的位置,上一条记录是id=20,下一条记录是30。

    事务 A 和 事务 B 的间隙锁范围都是一样的,为什么不会冲突?

    由于并没有id=25或者26,两个update语句没有生效但是启动了间隙锁,两个事务的间隙锁之间是相互兼容的,不会产生冲突。
    间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享(S型)和排他(X型)的间隙锁是没有区别的,他们相互不冲突,且功能相同。

    插入意向锁是什么?

    插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。
    插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。所以,插入意向锁和间隙锁之间是冲突的。

    为什么会发生死锁?

    本次案例中,事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。

    两个事务即使生成的间隙锁的范围是一样的,也不会发生冲突,因为间隙锁目的是为了防止其他事务插入数据,因此间隙锁与间隙锁之间是相互兼容的。
    在执行插入语句时,如果插入的记录在其他事务持有间隙锁范围内,插入语句就会被阻塞,因为插入语句在碰到间隙锁时,会生成一个插入意向锁,然后插入意向锁和间隙锁之间是互斥的关系。
    如果两个事务分别向对方持有的间隙锁范围内插入一条记录,而插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。

    mysql加锁规则

    我这里总结下, 我这个 MySQL 版本的行级锁的加锁规则。

    唯一索引等值查询:

  • 当查询的记录是存在的,next-key lock 会退化成「记录锁」。
  • 当查询的记录是不存在的,next-key lock 会退化成「间隙锁」。
  • 非唯一索引等值查询:

  • 当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
  • 当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。
  •