likes
comments
collection
share

Mysql事务隔离级别演示解析

作者站长头像
站长
· 阅读数 17

概述

数据库一般都会并发执行多个事务, 多个事务可能会并发地对桐乡的一屁数据进行增删查改操作, 可能就会导致所谓的脏写, 脏读, 不可重复度, 幻读等问题.

这些问题的本质都是数据库的多事务并发问题, 为了解决多事务并发问题, 数据库设计了事务隔离机制, 锁机制, MVCC多版本并发控制隔离机制, 用一整套机制来解决多事务并发问题.

事务及其ACID属性

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

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

  • 一致性(Consistent): 在事务开始和完成时, 数据都必须保持一致状态. 这意味着所有相关的数据规则都必须应用于事务的修改, 以保证数据的完成性.

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

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

并发事务处理带来的问题

脏写(更新丢失 Lost Update)

当两个或多个事务选择同一行, 然后基于最初选定的值更新该行时, 由于每个事务都不知道其它事务的存在, 就会发生丢失更新问题, 最后的更新覆盖了由其它事务所做的更新.

脏读(Dirty Reads)

一个事务正在对一条记录做修改, 在这个事务完成并提交之前, 这条记录的数据就处于不一致的状态; 这时, 另一个事务也来读取同一条记录, 如果不加控制, 第二个事务读取了这些脏数据, 并据此做出进一步的处理, 就会产生未提交的数据依赖关系, 这种现象被称为脏读.

简洁来说就是, 事务A读取到了事务B已经修改但尚未提交的数据, 还在这个数据基础上做了操作.

不可重复读(Non-Repeatable Reads)

一个事务在读取某些数据后的某个时间, 再次读取以前读过的数据, 却发现其读出的数据已经发生了改变, 或某些记录已经被删除了, 这种情况就叫做不可重复读.

简洁来说就是, 事务A内部的相同查询语句在不同时刻读出的结果不一致, 不符合隔离性.

幻读(Phantom Reads)

一个事务按照相同的查询条件重新读取以前检索过的数据, 却发现其它事务插入了满足其查询条件的新数据, 这种情况称为幻读.

简洁来说就是, 事务A读取到了事务B提交的新增数据, 不符合隔离性.

事务隔离级别

脏读, 不可重复读, 幻读, 其实都是数据库读一致性问题, 必须由数据库系统提供一定的事务隔离机制来解决.

隔离级别脏读(Dirty Reads)脏读(Dirty Reads)脏读(Dirty Reads)
读未提交可能可能可能
读已提交不可能可能可能
可重复读不可能不可能可能
可串行化不可能不可能不可能

数据库的事务隔离越严格, 并发副作用越小, 但是付出的代价也就越大, 因为事务隔离实际上就是使事务在一定程度上"串行化"地进行, 这显然与"并发"是矛盾的.

同时, 不同的应用对读一致性和事务隔离程度的要求也是不同的, 比如许多应用对"不可重复读"和"幻读"并不敏感, 可能更关心数据库并发访问的能力.

查看当前数据库的事务隔离级别: show variables like 'transaction_isolation'

Mysql事务隔离级别演示解析

设置事务隔离级别: set transaction_isolation = 'REPEATABLE-READ'

Mysql事务隔离级别演示解析

mysql默认的事务隔离级别是可重复读, 用spring开发程序时, 如果不设置隔离级别, 默认使用mysql设置的隔离级别, 如果spring设置了就用spring设置的隔离级别.

锁详解

锁是计算机协调多个进程或线程并发访问某一资源的机制.

在数据库中, 除了传统的计算资源(如CPI, RAM, IO等)的争用之外, 数据也是一种供需要用户共享的资源. 如何保证数据并发访问的一致性, 有效性是所有数据库必须解决的一个问题, 锁冲突也是影响数据库并发访问的一个重要因素.

锁分类

  • 从性能上分为乐观锁(用版本对比来实现)和悲观锁.

  • 从对数据库操作的类型分, 分为读锁和写锁(都属于悲观锁).

  1. 读锁(共享锁, S锁(Shared)): 针对同一份数据, 多个读操作可以同时进行而不会互相影响.

  2. 写锁(排它锁, X锁(Exclusive)): 当前写操作没有完成前, 它会阻断其它写锁和读锁.

  • 从对数据库的操作粒度分, 分为表锁和行锁.

表锁

每次操作锁住整张表, 开销小, 加锁快, 不会出现死锁, 锁定粒度大, 发生锁冲突的概率最高, 并发度最低, 一般用在整表数据迁移的场景.

表锁的基本操作

  • 手动增加表锁

lock table 表名1 read/write, 表名2 read/write

  • 查看表上加的锁

show open tables, 如果某个表被加了读/写锁, 返回的in_use字段将为1.

  • 删除表锁

unlock tables

表读锁

某个表如果被加了读锁, 那么当前执行加锁的session和其它session都可以读该表, 但是当前执行加锁的session中对该表进行插入或者更新的操作都会报错, 其它session插入或更新将会阻塞等待.

Mysql事务隔离级别演示解析

可以看到, 在左边客户端对user表进行了加read锁, 但是左右都可以进行查询, 当左边客户端执行更新操作时, 报错告知user表被加了读锁, 无法修改, 在右边客户端执行更新操作, 客户端的命令被阻塞了.

在左边取消加读锁, 右边客户端就可以执行了

Mysql事务隔离级别演示解析

表写锁

某个表如果被加了写锁, 那么在当前执行加锁的session的增删查改都可以, 但是其它session的任何操作都会被阻塞.

右边客户端读操作阻塞: Mysql事务隔离级别演示解析

右边客户端写操作阻塞: Mysql事务隔离级别演示解析

加锁客户端任何操作都ok

Mysql事务隔离级别演示解析

解锁后, 右边客户端命令放行, 最终修改数据是zhangthree

Mysql事务隔离级别演示解析

行锁

每次操作锁住一行数据. 开销大, 加锁慢, 会出现死锁, 锁定粒度最小, 发生锁冲突的概率最低, 并发度最高.

InnoDB与MyIsam的最大不同有两点:

  1. InnoDB支持事务(transaction)

  2. InnoDB支持行级锁

行锁演示

一个session开启事务之后, 更新数据了, 但是不提交事务, 那么另一个session更新同一条记录的时候, 就会被阻塞, 但是更新同表的不同记录将不会阻塞.

在左边客户端使用begin开启事务, 更新user表中id为3的数据, 但是不提交事务, 会发现右边客户端可以更新user表中id为2的数据, 但是当它更新id为3的数据的时候, 会被阻塞:

Mysql事务隔离级别演示解析

当左边客户端使用commit提交之后, 右边客户端解阻塞, 并更新数据成功.

Mysql事务隔离级别演示解析

行锁总结

一个session开启事务更新不提交, 另一个session更新同一条记录会阻塞, 更新不同记录不会阻塞.

MyIsam在执行查询语句select之前, 会自动给涉及的所有表加读锁, 在执行update, insert, delete操作会自动给涉及的表加写锁.

InnoDB在开启事务执行查询语句select时(非串行隔离级别), 不会加锁, 但是update, insert, delete操作会加行锁.

简而言之: 读锁会阻塞写, 但是不会阻塞读. 而写锁会把读和写都阻塞.

行锁与事务隔离级别

读未提交

打开客户端, 设置当前事务模式为read uncommitted(读未提交), 使用两个客户端来演示事务情况下的读未提交的特点.

设置当前事务为读未提交: set transaction_isolation = 'READ-UNCOMMITTED'

Mysql事务隔离级别演示解析

在左边客户端查询表balance的id为1的数据, 发现prize是500, 右边客户端打开事务, 更新其prize为666, 但是未提交事务, 在左边客户端又一次的查询, 查询到了右边客户端还没提交的数据.

Mysql事务隔离级别演示解析

这时候如果左边客户端执行update balance set prize = prize - 50 where id = 1, 将会被阻塞, 直到右边客户端执行commit和rollback之后, 才会解阻塞. 这里右边客户端执行了rollback之后, 左边客户端执行sql成功, 但是查询到的数据是450, 而不是611. 这在左边客户端眼里来看, 是不符合逻辑的.

Mysql事务隔离级别演示解析

发生上述不合理的逻辑的原因, 是采用了读未提交(read-uncommitted)的事务隔离级别. 就是某个session可以在执行查询的时候, 读取到了其它事务修改完还没提交的数据, 解决这个问题的方法, 就是设置读已提交(read-committed)的事务隔离级别.

读已提交

打开左边客户端, 使用set transaction_isolation = 'READ-COMMITTED', 将左边客户端的事务隔离级别设置为读已提交, 此时右边客户端的事务隔离级别仍然为可重复读.

Mysql事务隔离级别演示解析

左边客户端查询balance表中id为1的数据, prize值为450, 这时候右边客户端打开事务, 更新了prize为666, 但是还没提交事务, 这时候左边客户端继续查询同一条数据, 发现结果依然还是450, 只有当右边客户端程序真正提交了更新事务之后, 左边客户端才能查询到666的数据.

Mysql事务隔离级别演示解析

可重复读

在读已提交中, 左边客户端虽然不会读取到右边客户端没提交的事务, 但是在它自己事务内部, 却在读取同一条数据的时候, 得到了不同的结果, 这就是不可重复读, 这是不符合事务的隔离性的, 因此要解决这个问题, 需要使用更高一级的事务隔离级别, 可重复读(repeatable-read).

在左边客户端更新左边session中的当前事务隔离级别为可重复读: set transaction_isolation = 'REPEATABLE-READ'

左边客户端begin开启事务, 查询balance表中id为1的数据, prize值为666. 这时候打开右边客户端的事务, 更新balance表中id为1的数据, 设置其prize为777, 再在左边客户端中继续查询这条数据, 发现即使右边客户端提交了事务, 左边客户端读取的数据依然还是666, 只有当左边客户端自己回滚/提交事务之后, 再查询, 才能查询到右边客户端提交事务之后的更新.

Mysql事务隔离级别演示解析

可重复读情况下的幻读

左边客户端查询prize为666的数据, 查询到了两条, 右边客户端执行一插入操作, 并且新插入的prize为666, 然后直接提交事务, 新插入的数据的id为6.

右边客户端提交事务之后, 在左边客户端继续查询prize为666的数据, id为6的不在内, 没有出现幻读.

但是右边客户端执行update balance set owner = 'lxxxxx' where id = 6之后, 发现更新了一条数据, 然后再次以同样的条件去查询prize为666的数据, id为6的数据出现了, 这时候对于左边客户端来说, 还在同一个事务当中, 多次查询结果查询到了其它事务新增的数据, 因此出现了幻读.

Mysql事务隔离级别演示解析

串行化

左边客户端设置当前session事务隔离级别为串行化: set transaction_isolation = 'SERIALIZABLE'

左右分别打开事务, 在左边客户端中执行select * from balance where prize = 666, 查询数据之后, 在右边客户端中执行insert into balance(owner, prize) values ('lzzzz', 666), 发现sql被阻塞了. 即使执行insert into balance(owner, prize) values ('lzzzz', 888)也会被阻塞.

Mysql事务隔离级别演示解析

再测试另一种情况. 左边客户端继续打开事务, 然后使用select * from balance where prize = 666, 右边客户端开启事务, 发现不论更新任何数据, sql都是被阻塞的.

Mysql事务隔离级别演示解析

再测试一种情况. 左右客户端开启事务, 左边客户端执行select * from balance where id = 1, 右边客户端插入一条数据, 发现没有问题, 但是右边客户端执行update balance set prize = 999 where id = 1, sql就被阻塞了, 但是执行 update balance set prize = 999 where id = 2, 就能正常执行.

Mysql事务隔离级别演示解析

因此我们可以得到一个结论: 当事务隔离级别为串行化时, InnoDB引擎在查询操作的时候, 可能给表加表锁, 也可能给表加行锁.

在使用prize = 666的条件时, 由于prize = 666无法定位到精确的某一/几行, 因此直接给表加了表锁, 其它事务的insert, update, delete都是无法拿到锁的.

在使用id = 1的条件时, 由于id = 1是索引主键, 可以精确定位到一行, 因此就只给id = 1的这一行加了行锁, 其它事务无法对该行执行update, delete操作, 但是其它行可以随便执行.

串行化解决幻读问题

因此串行化可以解决幻读问题. 串行化使得一个事务在执行查询操作时, 给满足相应条件的行都加了行锁, 如果无法精确定位, 直接加表锁, 因此这个事务在未提交/回滚事务时, 再以相同条件继续查询, 是不可能查询到其它事务新增的数据的, 就避免了幻读.

在串行化中, 客户端如果执行的是一个范围查询, 那么范围内所有行包含每行记录之间的间隙区间范围(就算该行还没插入也会被加锁, 这种是间隙锁)都会被加锁. 此时其它客户端执行增删改操作都会被阻塞.