Mysql事务隔离级别演示解析
概述
数据库一般都会并发执行多个事务, 多个事务可能会并发地对桐乡的一屁数据进行增删查改操作, 可能就会导致所谓的脏写, 脏读, 不可重复度, 幻读
等问题.
这些问题的本质都是数据库的多事务并发问题, 为了解决多事务并发问题, 数据库设计了事务隔离机制, 锁机制, 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'
设置事务隔离级别: set transaction_isolation = 'REPEATABLE-READ'
mysql默认的事务隔离级别是可重复读
, 用spring开发程序时, 如果不设置隔离级别, 默认使用mysql设置的隔离级别, 如果spring设置了就用spring设置的隔离级别.
锁详解
锁是计算机协调多个进程或线程并发访问某一资源的机制
.
在数据库中, 除了传统的计算资源(如CPI, RAM, IO等)的争用之外, 数据也是一种供需要用户共享的资源. 如何保证数据并发访问的一致性, 有效性是所有数据库必须解决的一个问题, 锁冲突也是影响数据库并发访问的一个重要因素.
锁分类
-
从性能上分为乐观锁(用版本对比来实现)和悲观锁
. -
从对数据库操作的类型分, 分为读锁和写锁(都属于悲观锁)
.
-
读锁(共享锁, S锁(Shared))
: 针对同一份数据, 多个读操作可以同时进行而不会互相影响. -
写锁(排它锁, X锁(Exclusive))
: 当前写操作没有完成前, 它会阻断其它写锁和读锁.
从对数据库的操作粒度分, 分为表锁和行锁
.
表锁
每次操作锁住整张表, 开销小, 加锁快, 不会出现死锁, 锁定粒度大, 发生锁冲突的概率最高, 并发度最低
, 一般用在整表数据迁移
的场景.
表锁的基本操作
手动增加表锁
lock table 表名1 read/write, 表名2 read/write
查看表上加的锁
show open tables, 如果某个表被加了读/写锁, 返回的in_use字段将为1
.
删除表锁
unlock tables
表读锁
某个表如果被加了读锁, 那么当前执行加锁的session和其它session都可以读该表, 但是当前执行加锁的session中对该表进行插入或者更新的操作都会报错, 其它session插入或更新将会阻塞等待
.
可以看到, 在左边客户端对user表进行了加read锁, 但是左右都可以进行查询, 当左边客户端执行更新操作时, 报错告知user表被加了读锁, 无法修改, 在右边客户端执行更新操作, 客户端的命令被阻塞了.
在左边取消加读锁, 右边客户端就可以执行了
表写锁
某个表如果被加了写锁, 那么在当前执行加锁的session的增删查改都可以, 但是其它session的任何操作都会被阻塞.
右边客户端读操作阻塞:
右边客户端写操作阻塞:
加锁客户端任何操作都ok
解锁后, 右边客户端命令放行, 最终修改数据是zhangthree
行锁
每次操作锁住一行数据. 开销大, 加锁慢, 会出现死锁, 锁定粒度最小, 发生锁冲突的概率最低, 并发度最高
.
InnoDB与MyIsam的最大不同有两点
:
-
InnoDB支持事务(transaction)
-
InnoDB支持行级锁
行锁演示
一个session开启事务之后, 更新数据了, 但是不提交事务, 那么另一个session更新同一条记录的时候, 就会被阻塞, 但是更新同表的不同记录将不会阻塞
.
在左边客户端使用begin开启事务, 更新user表中id为3的数据, 但是不提交事务, 会发现右边客户端可以更新user表中id为2的数据, 但是当它更新id为3的数据的时候, 会被阻塞:
当左边客户端使用commit提交之后, 右边客户端解阻塞, 并更新数据成功.
行锁总结
一个session开启事务更新不提交, 另一个session更新同一条记录会阻塞, 更新不同记录不会阻塞
.
MyIsam在执行查询语句select之前, 会自动给涉及的所有表加读锁, 在执行update, insert, delete操作会自动给涉及的表加写锁
.
InnoDB在开启事务执行查询语句select时(非串行隔离级别), 不会加锁, 但是update, insert, delete操作会加行锁
.
简而言之: 读锁会阻塞写, 但是不会阻塞读. 而写锁会把读和写都阻塞
.
行锁与事务隔离级别
读未提交
打开客户端, 设置当前事务模式为read uncommitted(读未提交), 使用两个客户端来演示事务情况下的读未提交的特点.
设置当前事务为读未提交: set transaction_isolation = 'READ-UNCOMMITTED'
在左边客户端查询表balance的id为1的数据, 发现prize是500, 右边客户端打开事务, 更新其prize为666, 但是未提交事务, 在左边客户端又一次的查询, 查询到了右边客户端还没提交的数据.
这时候如果左边客户端执行update balance set prize = prize - 50 where id = 1, 将会被阻塞, 直到右边客户端执行commit和rollback之后, 才会解阻塞. 这里右边客户端执行了rollback之后, 左边客户端执行sql成功, 但是查询到的数据是450, 而不是611. 这在左边客户端眼里来看, 是不符合逻辑的.
发生上述不合理的逻辑的原因, 是采用了读未提交(read-uncommitted)的事务隔离级别. 就是某个session可以在执行查询的时候, 读取到了其它事务修改完还没提交的数据
, 解决这个问题的方法, 就是设置读已提交(read-committed)的事务隔离级别.
读已提交
打开左边客户端, 使用set transaction_isolation = 'READ-COMMITTED', 将左边客户端的事务隔离级别设置为读已提交, 此时右边客户端的事务隔离级别仍然为可重复读.
左边客户端查询balance表中id为1的数据, prize值为450, 这时候右边客户端打开事务, 更新了prize为666, 但是还没提交事务, 这时候左边客户端继续查询同一条数据, 发现结果依然还是450, 只有当右边客户端程序真正提交了更新事务之后, 左边客户端才能查询到666的数据.
可重复读
在读已提交中, 左边客户端虽然不会读取到右边客户端没提交的事务, 但是在它自己事务内部, 却在读取同一条数据的时候, 得到了不同的结果, 这就是不可重复读, 这是不符合事务的隔离性的, 因此要解决这个问题, 需要使用更高一级的事务隔离级别, 可重复读(repeatable-read)
.
在左边客户端更新左边session中的当前事务隔离级别为可重复读: set transaction_isolation = 'REPEATABLE-READ'
左边客户端begin开启事务, 查询balance表中id为1的数据, prize值为666. 这时候打开右边客户端的事务, 更新balance表中id为1的数据, 设置其prize为777, 再在左边客户端中继续查询这条数据, 发现即使右边客户端提交了事务, 左边客户端读取的数据依然还是666, 只有当左边客户端自己回滚/提交事务之后, 再查询, 才能查询到右边客户端提交事务之后的更新.
可重复读情况下的幻读
左边客户端查询prize为666的数据, 查询到了两条, 右边客户端执行一插入操作, 并且新插入的prize为666, 然后直接提交事务, 新插入的数据的id为6.
右边客户端提交事务之后, 在左边客户端继续查询prize为666的数据, id为6的不在内, 没有出现幻读.
但是右边客户端执行update balance set owner = 'lxxxxx' where id = 6之后, 发现更新了一条数据, 然后再次以同样的条件去查询prize为666的数据, id为6的数据出现了, 这时候对于左边客户端来说, 还在同一个事务当中, 多次查询结果查询到了其它事务新增的数据, 因此出现了幻读
.
串行化
左边客户端设置当前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)也会被阻塞.
再测试另一种情况. 左边客户端继续打开事务, 然后使用select * from balance where prize = 666, 右边客户端开启事务, 发现不论更新任何数据, sql都是被阻塞的.
再测试一种情况. 左右客户端开启事务, 左边客户端执行select * from balance where id = 1, 右边客户端插入一条数据, 发现没有问题, 但是右边客户端执行update balance set prize = 999 where id = 1, sql就被阻塞了, 但是执行 update balance set prize = 999 where id = 2, 就能正常执行.
因此我们可以得到一个结论: 当事务隔离级别为串行化时, InnoDB引擎在查询操作的时候, 可能给表加表锁, 也可能给表加行锁
.
在使用prize = 666的条件时, 由于prize = 666无法定位到精确的某一/几行, 因此直接给表加了表锁
, 其它事务的insert, update, delete都是无法拿到锁的
.
在使用id = 1的条件时, 由于id = 1是索引主键, 可以精确定位到一行, 因此就只给id = 1的这一行加了行锁
, 其它事务无法对该行执行update, delete操作, 但是其它行可以随便执行.
串行化解决幻读问题
因此串行化可以解决幻读问题. 串行化使得一个事务在执行查询操作时, 给满足相应条件的行都加了行锁, 如果无法精确定位, 直接加表锁, 因此这个事务在未提交/回滚事务时, 再以相同条件继续查询, 是不可能查询到其它事务新增的数据的, 就避免了幻读
.
在串行化中, 客户端如果执行的是一个范围查询, 那么范围内所有行包含每行记录之间的间隙区间范围(就算该行还没插入也会被加锁, 这种是间隙锁)都会被加锁. 此时其它客户端执行增删改操作都会被阻塞
.
转载自:https://juejin.cn/post/7078134380786352141