MySQL学习笔记(1)——事务

事务

事务是指在一系列操作中,所有的操作必须全部成功完成,一旦有一个操作失败,要撤销所有的更改。

事务的特性(ACID)

  • 原子性Atomicity):事务是不可分割的最小工作单位,一次事务中的所有操作要么全部成功提交,要么失败回滚,不可能只执行其中一部分操作。
  • 一致性Consistency):事务执行前后,数据库的完整性保持一致。
  • 隔离性Isolation):一个事务所作的修改在最终提交前对其他事务是不可见的。
  • 持久性Durability):事务一旦成功提交,更改会永久性地保存在数据库中,即使数据库故障,所做的修改也不丢失。

事务隔离级别

并发事务可能发生的问题

  • 脏读:事务A修改数据之后还未提交,此时事务B读取数据,读取的是事务A修改后的数据,然后事务A失败回滚,此时B读到了脏数据。
  • 不可重复读:事务A读取某个数据后,再次读取该数据,发现读取的数据已经发生更改或者被删除。
  • 幻读:事务A查询某个范围的记录,此时事务B在该范围内插入新的记录/删除部分记录,A再次读取该范围的记录时,发现比原来多/少了几行记录。

MySQL支持的隔离级别

  • 未提交读READ UNCOMMITTED):可能出现脏读、不可重复读、幻读。
  • 提交读READ COMMITTED):解决脏读,可能出现不可重复读、幻读。
  • 可重复读REPEATABLE READ):解决脏读、不可重复读,可能出现幻读。是MySQL的默认隔离级别。InnoDB引擎通过多版本并发控制(MVCC)和next-key lock解决了幻读问题。
  • 串行化SERIALIZABLE):解决脏读、不可重复读、幻读。

以上隔离级别的隔离强度逐渐变强,并发性能逐渐变差。

MySQL锁机制

  • 共享锁:又称S锁,用于读的场景,当事务对记录加S锁,其他事务可以对该记录加S锁,但不能加X锁。SELECT ... LOCK IN SHARE MODE会为记录加S锁。
  • 排他锁:又称X锁,用于写的场景,当事务对记录加X锁,其他事务不能对该记录加任何锁,直到事务结束。INSERTUPDATEDELETESELECT ... FOR UPDATE会为记录加X锁。
  • 行锁:锁粒度细,占用更多资源,并发行能好。
  • 表锁:锁粒度粗,占用资源少,并发行能差,不会出现死锁。
  • 意向锁:分为意向共享锁(IS)和意向排他锁(IX),作用于表,当表里某个记录被加X锁,那么这个表就会自动获得IX锁,其他事务想要对该表加锁就只要先检测这个表是否被上了意向锁,无需对整个表进行遍历。
  • record lock:即行锁,仅仅把一条记录加锁(S锁或X锁)。
  • 间隙锁gap lock):,锁住记录之间的间隙,防止其他事务在这个间隙中修改或插入记录。
  • next-key lock:是record lockgap lock的组合,用于RR隔离级别防止幻读。

MySQL隔离级别的实现

  • 未提交读:读不加任何锁,写加排他锁直到事务结束。
  • 提交读:读不加锁,而是在每次读时,利用MVCC生成ReadView;写加排他锁直到事务结束。
  • 可重复读:读不加锁,而是在事务第一次读时,利用MVCC生成ReadView;写加排他锁直到事务结束。
  • 串行化:读写都加排他锁,将事务串行执行,后来的事务必须等待前面的事务执行结束才开始执行,因此隔离效果最好,效率最差。

多版本并发控制(MVCC)

  • InnoDB存储引擎通过MVCC实现了RC和RR隔离级别。
  • InnoDB的行记录中,存在几个隐藏的字段:
    • trx_id记录最近修改该行记录的事务的ID;
    • db_roll_ptr指向修改前版本的undo log,通过这个指针可以不断回溯到该记录上一个版本。
  • ReadView用于判断记录的某个版本是否对当前事务可见。它包含以下内容:
    • trx_ids:生成此ReadView时系统中未提交的事务列表。
    • up_limit_idtrx_ids中最小的事务ID,小于此版本号的事务对当前ReadView是可见的。
    • low_limit_id:当前最大的事务版本号+1,即下一个将被分配的事务id,大于此版本号的事务对当前ReadView是不可见的。
    • creator_trx_id:当前创建事务的ID。
  • ReadView与记录的trx_id比较算法:
    • trx_id<up_limit_id:表示这条记录在当前事务创建快照之前就已经提交了,所以该行数据对当前事务可见;
    • trx_id>=low_limit_id:表示这条记录在当前事务创建快照之后才被修改,所以该行数据对当前事务不可见;
    • up_limit_id<=trx_id<low_limit_id:表示这条记录在当前事务创建快照之时可能处于活动状态或者已提交状态,需要对trx_ids进行查找,如果能找到ID为trx_id的事务,这条记录对当前事务是不可见的;如果没有找到,说明ID为trx_id的事务已经提交,这条记录对当前事务是可见的;
    • 如果判断这条记录对当前事务不可见,则从这条记录的db_roll_ptr中找到它上一个版本号作为trx_id重新开始判断;
    • 如果判断这条记录对当前事务可见,则将这条记录的值返回。
  • 在RC级别,每次查询都会生成最新版本的ReadView,因此保留了不可重复读和幻读的问题。
  • 在RR级别,只有在事务第一次SELECT时生成ReadView,此后的查询都基于这个快照。但如果在两次读的过程中,出现了写的情况,仍然会发生幻读的问题。

快照读和当前读

  • 在RR隔离级别,事务会在第一次SELECT时生成快照,此后的普通查询都在这个快照上完成,此过程不会加锁,为快照读,或称为非阻塞读。在单纯的快照读情况下,由MVCC保证不会发生幻读。
  • 对于SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEDELETEUPDATE等需要加锁的操作,此时为当前读,数据库会使用next-key lock来防止幻读。