1、事务的定义
事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成
2、事务的四大特性
(1)原子性
执行语句的时候1、2成功了,3失败了,所以要求1、2也要失败,那么怎么办呢?这个就需要undo log来实现了
(2)持久性
如果断电了、重启、崩溃恢复的特性,通过redo log来实现。
(3)隔离性
show global variables like “tx_isolation”;
我读的是脏数据,你操作你的,我们操作我的。把大家的操作进行隔离。
(1)如果没有隔离性,所带来的的问题(事务并发带来的问题)
脏读
不可重复读
幻读
不可重复读和幻读都是读到了已经提交的数据,幻读只跟插入有关。删除和查询所造成的不一样就是不可重复读。
事务的隔离级别
读已提交
(1)实现方式
版本链 对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不包含row_id列);
- trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
- roll_pointer: 每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记录修改前的信息
ReadView 对于使用READ UNCOMMITEED隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录。对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,就需要用到我们上边所说的版本链了,核心问题就是:需要判断一个版本链中的那个版本是当前事务可见的。
它会生成一个版本链,然后会生成一个ReadView。
A事务在执行查询操作的时候会生成一个ReadView,ReadView中的m_ids[200、82、81、80]记录了活跃的事务id
A进行查找的话,会查找没有在m_ids中的值,然后事务id为最近的数据。
可重复读
(1)可重复读如何实现?
MVCC指的就是在READCOMMITED、REPEATABLE READ 这两种隔离级别的事务在执行普通SELECT操作时访问记录的版本链过程,可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READCOMMIT、REPEATABLE READ这两个隔离级别的一个很大不同就是: 生成ReadView的时机不同,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。
综上所述:
因为MySQL的可重复读,对事务B进行查询时,事务A提交的更新不会影响到事务B。
但是事务Bn进行更新时,事务A提交的更新会影响到事务B。
3、什么是当前读还有快照读?
https://blog.csdn.net/javaanddonet/article/details/110674287
在我们更新表中的数据的时候,有的时候一次性的更新多个列的值,这多个待更新的列之间又有业务逻辑上的关系。这个时候我们在更新的时候一定要格外的注意,更新过程中的当前读,这里的当前读是指读取当前事务中的值。
CREATE TABLE `test` (
`id` int(255) DEFAULT NULL,
`a` int(255) DEFAULT NULL,
`b` int(255) DEFAULT NULL,
`c` int(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `test`(`id`, `a`, `b`, `c`) VALUES (1, 100, 100, 100);
更新语句如下:
update test
set a=a-1, b=a-1-1, c=a-1-1-1
where id=1;
此时,你先自己口算一下,最后这个表中的这一行数据的abc三列的值分别是多少呢?答案是:a=99, b=97, c=96。你可能会好奇,为什么不是:a=99, b=98, c=97呢?因为abc原先都为100,那么a=a-1=100-1=99;b=a-1-1=100-1-1=98才对呀?为什么b的值最后是97呢?
这里我们分析一下。
在执行a=a-1这个语句块,MySQL在计算a到底需要等于多少的时候,要获取数据库中a的值,此时得到的a为100,那100-1后赋值给了a,在当前这个更新语句的事务中,a此时为99了。
在执行b=a-1-1的时候,它会获取a的值然后再去为b赋值,因为这个更新语句是一个事务,所以这个事务前面修改的a的值,当前的事务还是会承认的,所以它读到a的值为99,此时再执行b=99-1-1=97,
在执行c=a-1-1-1的时候,同样会读取当前事务中a的值为99,然后计算出c=99-1-1-1=96。
4、如何解决幻读问题?
(1)解决方式
在快照读情况下,mysql通过mvcc来避免幻读。
在当前读情况下,mysql通过x锁或者next-key来避免其他事务修改
(2)幻读的影响
会导致一个事务中先产生的锁,无法锁住后加入的行,会产生数据一致性问题。
(3)产生幻读的原因:
(4)解决幻读
- 在两行记录之间加上间隙锁,阻止新记录的插入,与间隙锁产生冲突的只有“往这个间隙插入记录”这个操作;
- 同时添加间隙锁与行锁称为Next-key lock ,注意间隙锁只有在InnoDB的可重复度隔离级别下生效;
- MVCC只实现读取已提交和可重复读,InnoDB在可重复读的隔离级别下,使用MVCC+Next-key lock解决幻读
(5)可重复读下,MVCC的幻读问题
读操作不会出现幻读
关闭自动提交事务
SET AUTOCOMMIT = 0;
新增测试表测试数据
开启事务A,读取到两条测试数据
开启事务B,插入数据并提交
事务A再次读取仍为两条数据,没有出现幻读,注意不要执行begin,单独执行查询,否则开启了新的事务
更新操作会出现幻读问题
在测试表上新增一个测试字段,测试表和测试数据
开启事务A,更新数据,受影响行数3条
开启事务B,插入数据并提交
事务A再次更新数据,却仍然影响了事务B插入的数据,出现幻读,注意不要执行begin,单独执行更新,否则新开启了事务
这种现象的原因
快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就不会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。
当前读
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的版本号记录,写操作后把版本号改为了当前事务的版本号,所以即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致幻读。
(6)如何解决当前读导致的幻读问题
(1)使用可串行化的隔离级别
SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁竞争的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
(2)使用next-key锁
- 行锁:单个行记录的锁,主键和唯一索引都是行记录的锁模式,避免其它事务执行更新操作时,导致当前事务发生幻读;
- 间隙锁:间隙锁是索引行上的一段开区间,间隙锁基于非唯一索引实现,由于InnoDB中索引是有序的,当前事务基于非唯一索引更新数据时InnoDB会在非唯一索引上加上间隙锁,阻塞其他事务需要插入的数据行,避免其它事务执行插入操作时,导致当前事务发生幻读;
- next-key锁:next-key锁是索引行上的一段前开后闭的区间,是MySQL加锁的基本单位,next-key锁也是行锁和间隙锁的组合。
新增非唯一索引
ALTER TABLE tb_user ADD INDEX idx_batch(batch);
开启事务A,更新数据,受影响行数3条
开始事务B插入数据,此时由于next-key锁存在,插入被阻塞
所以在InnoDB中,在可重复读隔离级别中,MVCC可防止快照读引起的幻读,next-key锁可防止当前读引起的幻读
使用next-key锁不能完全解决幻读问题
当快照读和当前读同时发生时,请看下面例子:
原始数据:
开始事务A,快照读查询数据,此时读取到3条数据
开启事务B,插入数据并提交事务
返回事务A,更新数据后,再次快照读取数据,居然读到了事务B提交的数据
这种情况下幻读发生的原因:更新操作是当前读,更新操作后使得MVCC版本控制失效了,快照读出现了幻读