【数据库】事务并发的三大问题以及事务的隔离级别
事务并发引起的三大问题1.脏读脏读:一个事务会读取到另一个事务未提交的数据。(读取到未提交数据)例子:事务A修改了数据但还未提交,事务B读取到了事务A修改的数据。然后事务A因为某些错误回滚了,这个时候事务B读取到的数据就是脏的,这就是脏读。脏读的发生流程:使用数据库隔离级别为:读已提交(可避免脏读)2.不可重复读不可重复读:在同一事务内,事务两次读取到的数据是不一样的。(原数据中同一条数据被修改或
事务并发引起的三大问题
1.脏读
-
脏读:一个事务会读取到另一个事务未提交的数据。(读取到未提交数据)
例子:事务A修改了数据但还未提交,事务B读取到了事务A修改的数据。然后事务A因为某些错误回滚了,这个时候事务B读取到的数据就是脏的,这就是脏读。
脏读的发生流程:
使用数据库隔离级别为:读已提交(可避免脏读)
2.不可重复读
-
不可重复读:在同一事务内,事务两次读取到的数据是不一样的。(原数据中同一条数据被修改或被删除)
例子:事务A读取了一条数据之后,事务B修改了这条数据并提交了事务,然后事务A再次读取这条数据,就会发现两次结果不一致。这就是不可重复读。
不可重复读,示例1
不可重复读,示例2
左边的事务先查询一次数据,为salary值5000
右边的事务使用update更新salary为4500,但未提交commit事务。左边事务第二次查询,salary还是为5000(事务隔离级别为读已提交,已避免脏读)
右边的事务commit提交事务,左边事务第三次查询,salary为4500(左边事务还没有提交,多次查询中读取到右边的事务提交的数据)
使用数据库隔离级别为:可重复读(可避免不可重复读)
在可重复读的事务隔离级别中,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
例子如下:第一个事务是写事务,那么第二个事务也禁止写
但是事务在隔离级别为可重复读,并不能避免幻读(使用mysql InnoDB存储引擎,可以在可重复读隔离级别下阻止读数据下的幻读,原因见下)
3.幻读
-
幻读:事务中的同一个查询在不同的时间产生不同的行集,这个就是幻读问题。(数据总条数新增)
例子:事务A使用一定的条件查询,然后事务B增加了符合条件的记录,当事务A再次查询的时候,发现两次查询的结果集不一样,好像产生了幻觉。这就是幻读。
不可重复读和幻读,都是读取到其他事务已经提交的数据。而脏读是读取到其他事务还未提交的数据
解决幻读:(1)可以使用数据库隔离级别是串行化
(2)使用mysql innoDB存储引擎,在数据库隔离级别是可重复读也能解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,下面将详细讨论
场景一:读数据情况下的幻读,使用mysql innoDB存储引擎,在数据库隔离级别是可重复读,能解决
场景二:事务1更新数据,而事务2插入数据,这种情况使用mysql innoDB存储引擎,在数据库隔离级别是可重复读,不能解决
根据上面的结果我们期望的结果是这样的:
id name
1 财务部
2 研发部
但是实际上我们的结果是:
场景一和场景二形成的根本原因是:快照读和当前读
对于场景一:
select 快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。
快照的生成是在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。
所以场景一中,事务1先select之后,已经生成快照。第二次select也是返回快照(历史数据),不会返回事务2新加的数据,不会出现幻读
对于场景二:
当前读
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。
如以下sql都是执行的是当前读
insert;
update; //如 update .... set .. where ...
delete; //如 delete from. . where ..
在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。
也正是因为这样,select快照读和update当前读混合使用,就出现了场景二的幻读现象。
select的当前读需要手动的加锁:
select * from table where ? lock in share mode;
select * from table where ? for update;
那么只用当前读可以避免幻读。当前读会阻塞新数据的插入,主要是间隙锁的加锁机制。
可以看到Session 2 被阻塞了。需要等到Session1 提交事务后才能完成。当我们在事务中每次读取都使用当前读,也就是人工把InnoDB变成了串行化。一定程度上降低了并发性,但是也同样避免了幻读的情况。
只用快照读也可以避免幻读,但是快照读不加锁,所以不阻塞插入新数据,如上述场景一,同样能避免幻读(参考https://zhuanlan.zhihu.com/p/103580034?utm_source=wechat_session)
快照读和当前读混合使用,如上述的场景二,就会出现幻读的情况
参考链接
当前读与快照读的知识https://blog.csdn.net/hello_world_cy/article/details/82109806
数据库的四个隔离级别
读未提交(Read Uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。解决更新丢失问题。如果一个事务已经开始写操作,那么其他事务则不允许同时进行写操作,但允许其他事务读此行数据。
读已提交(Read Committed):一个事务提交之后,它做的变更才会被其他事务看到。解决了脏读。读取数据的事务允许其他事务继续访问(访问指读和写)该行数据,但是未提交的写事务将会禁止其他事务访问该行。
可重复读取(Repeatable Read):可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。解决了不可重复读取和脏读取,但是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。Mysql默认使用该隔离级别。
串行化(Serializable):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。解决了幻读的提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。
关于串行化的示例:
若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住(被锁住的原因见下)。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2
现象解释:
事务A一开始查询值1的时候就获得了读锁,根据两阶段加锁,事务A获得的锁要在commit的时候才释放,所以事务B在修改1为2的时候申请写锁会阻塞直到事务A提交,事务A提交之前获取的值都是1,所以V1 V2都是1,事务A提交后事务B获取到写锁完成更新操作,所以V3是2
实现4种隔离级别的实现方式
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
在“读已提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;
而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
参考链接:
推荐以下三篇文章
https://juejin.im/post/6844903799534911496
https://www.cnblogs.com/CoderAyu/p/11525408.html
https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB(%E5%9B%BE%E6%96%87%E8%AF%A6%E8%A7%A3).md#%E8%84%8F%E8%AF%BB%E8%AF%BB%E6%9C%AA%E6%8F%90%E4%BA%A4
https://juejin.im/entry/6844903665367547918
查看数据库隔离级别的方法
如果要把隔离级别设置成“读已提交”
transaction-isolation 的值设置成 READ-COMMITTED。你可以用 show variables 来查看当前的值
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
事务之间相互隔离的实现方式
以可重复读为例
MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作(除了记录变更记录,还会记录一条变更相反的回滚操作记录,前者记录在redo log,后者记录在undo log)。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view(在可重复读隔离级别下,每个事务在启动时都会新建一个事务视图,在接下来的事务中,都会使用这个视图)。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。
回滚日志删除的时机:当系统里没有比这个回滚日志更早的 read-view 的时候。如:read-viewA已经没了,那么对应的2改为1的回滚段就可以删掉了
事务的启动方式
主要是两种:
(1)显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
PS:
在自动提交模式关闭(关闭隐式提交)的情况下,开启一个事务上下文。首先数据库会隐式提交,之前隐式事务的还未被提交的操作
(2)隐式启动事务。set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
关闭autocommit=0,示例SQL如下:
select name from student where id=1;
commit;
PS:如果autocommit=1,数据库会自动提交事务。但是在autocommit=1的情况下,显式启动事务( begin 或 start transaction),事务也不会自动提交,除非手动commit。
如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销
总结:
1、不管autocommit 是1还是0 ,START TRANSACTION 后,只有当commit数据才会生效,ROLLBACK后就会回滚。
2、当autocommit 为 0 时,不管有没有START TRANSACTION。只有当commit数据才会生效,ROLLBACK后就会回滚。
3、如果autocommit 为1 ,并且没有START TRANSACTION 。调用ROLLBACK是没有用的。即便设置了SAVEPOINT。
查询长事务
information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
参考链接
https://www.cnblogs.com/hider/p/9103051.html
https://blog.csdn.net/aitangyong/article/details/50481161
https://www.cnblogs.com/kerrycode/p/8649101.html
https://zhuanlan.zhihu.com/p/57302592
关于事务是否隔离
在 MySQL 里,有两个“视图”的概念:
- 一个是 view。它是一个用查询语句定义的虚拟表(view的数据不占物理存储空间,但view本身会有物理结构),在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。
- 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图(没有物理结构),即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)