https://www.toutiao.com/a6801687729378689548/?log_from=0d1a4d64f3cd3_1640655046361
这篇文章讲的很不错了;
配合着一起读的有:https://www.cnblogs.com/barrywxx/p/11546087.html) (事务隔离级别)
https://www.cnblogs.com/lxiaojun/articles/13813607.html (repatable read 如何制造幻读)
https://blog.csdn.net/whoamiyang/article/details/51901888 (MYSQL MVCC 实现机制)
https://www.jianshu.com/p/bf862c37c4c9(讲锁的 记录锁 间隙锁 next-key锁)


1 技术支持

如果需要自己模拟 不同隔离级别、非自动提交(手动提交、手动回滚)的话 ,需要用到下面的命令;这些命令都是针对当前会话(窗口)的,不用担心造成全局的影响,如果还是不放心的话,自己搞个本地数据库就行了;

  1. MYSQL:默认隔离级别是 可重复读
  2. # 设置隔离界别
  3. set session transaction isolation level read committed -- 读已提交(不可重复读)
  4. set session transaction isolation level repeatable read -- 可重复读 mysql默认隔离级别)
  5. set session transaction isolation level serializable -- 序列化读
  6. 设置自动提交事务
  7. set autocommit=1(或set autocommit=on)(默认值) 自动提交
  8. 启动事务:start transaction(或begin);
  9. 提交事务:commit;
  10. SHOW VARIABLES LIKE '%AUTOCOMMIT%';
  11. SHOW VARIABLES LIKE '%isolation%';
  12. SHOW GLOBAL STATUS LIKE '%AUTOCOMMIT%';
  13. 查询设置
  14. SHOW VARIABLES LIKE '%ngram%';

上述文章的阅读笔记、收获

  1. spring 并不直接管理事务,而是提供了多种事务管理器;

    他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 Spring事务管理器的接口是:org.springframework.transaction.PlatformTransactionManager ,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了 例如:使用spring jdbc或者ibatis进行持久化的,他的事务管理器实现就是 org.springframework.jdbc.datasource.DataSourceTransactionManager

  2. 脏读、不可重复读对比

    脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的(尤其是如果当前事务回滚了的话,那错误就更大了)。 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

    不可重复读的重点是修改(同样的条件,你读取过的数据, 再次读取出来发现值不一样了),幻读的重点在于新增或者删除(同样的条件, 第1次和第2次读出来的记录数不一样 )。


  1. 事务隔离级别()

    2 事务隔离级别

    事务隔离 与脏读、不可重复读、幻读的关系

    | 事务隔离级别 | 脏读 | 不可重复读 | 幻读 | | —- | —- | —- | —- | | 读未提交(read umcommitted) | √ | √ | √ | | 不可重复读(read committed) | × | √ | √ | | 可重复读(repatable read) (mysql默认) | × | × | √ | | 串行化(serializable) | × | × | |

关于每种隔离级别出现的脏读、不可重复读、幻读的解释: 事务隔离级别解释)

1 读未提交:这是最低的隔离级别了; 这个能狗读到未提交的数据,造成脏读很好理解了;不可重复读、幻读也就很正常了; 2 读已提交—不可重复读—read-committed: 未提交的读不到了;解决了脏读的问题; 读已提交的隔离级别之下:a读了数据还在处理业务,b这个时候修改了数据仍未提交,a有读取的话,这时候a读不到b的修改结果,a继续处理业务,b提交了事务,a再次读取数据;这个时候读取到了b修改后的结果数据;那对a来说第三次读取的结果与之前的不一致,这就出现可不可重复读; 同理,b如果不是修改了数据而是插入或者删除了数据的话,就出现了幻读的情况了; 4 串行化 serializable : 这个会缩表的,插入、删除、修改都执行不了,就没有任何脏读、幻读、不可重复读的可能性了,这个并发量很低的; 3 可重复读—repeatable-read set session transaction isolation level repeatable read — 可重复读 开启两个个会话a b c; a、b设置成可重复读的隔离级别(mysql默认的隔离级别)

执行顺序 SESSION1—a SESSION2—b
1 start transaction; start transaction;
2 select * from user;
3 select * from user;
4 update .. score = score-2 where id = 1
5 select * from user; select * from user;
6 commit;
7 select * from user; select * from user;

ab开启非自动提交事务,a 读取到80 ,b也读取到80,此时a将数据改成 80-2(手动提交事务),a重新读取的话读到的是78;b这个时候在读取依然是80;这就是解决了可重复读的问题;注意这里有个前提,就是b会话的事务跟a不是同一个,并且b的读取一定要在a的需改提交之前;解释:b的事务中,开始读取的时候就产生了快照,别的事务对数据造成的修改,这里不影响;保证了每次读取都是一样的;解决了不可重复读;

还是上边这个例子: ab开启非自动提交事务,a 读取到score=80 ,b也读取到score=80,此时a将数据改成 score-2(手动提交事务),a重新读取的话读到的是score=78;b这个时候在读取依然是score=80;解决了不可重复读;此时ab依然开启非自动提交事务,在b会话中执行update score = score -2 ,然后b查询结果,此时b的结果是score=76【也就是说读取的话,读取的是事务开始时候的快照,但是更新的时候却是用别的事务修改后数据库中最新的值(score=78)执行的-2操作;】,解释:数据的一致性没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)有个更详细的解释**。

repatable read 可重复读,可以解决脏读、不可重复读的问题;幻读解决的并不好,但是并不是说百分百会出现幻读,如果中间做过更新、插入、修改等操作的话,再次读可能就出现幻读了;详细参阅https://www.cnblogs.com/lxiaojun/articles/13813607.html
简而言之就是,

执行循序 SESSION1 SESSION2
1 start transaction; start transaction;
2 select * from user;
3 insert into user values(1, 0, 0);
4 commit;
5 select * from user; (这里读不到,不出现幻读)
6 commit;
7 select * from user; (这里能读到了,但是不算幻读了,因为已经出了之前的事务,不再是快照读了)

↑ 这种情况下没有出现幻读 ↑

执行循序 SESSION1 SESSION2
1 start transaction; start transaction;
2 select * from user where version = 1; (没有数据)
3 insert into user(id,score,version) values(2, 0, 1);(插入一条符合session1的数据)
4 commit;
5 select * from user where version = 1; (查不到,不出现幻读)
6 update user set balance = 1 where version = 1; (虽然查看的时候查不到,但是更新确实能更新的;原因就是读是快照都,更新的确实当前读!!!)
如果不做这次【当前读】的话, 第7步中就不会出现幻读所以,mysql的repatable read想要出现幻读是需要一定条件的
7 select * from user where version = 1; (读到数据了,这里强行算是幻读吧)
8 commit
9 select * from user where version = 1; (读到数据了)

↑ 这种情况下没有出现幻读 ↑

快照读、当前读解释

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。
当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:

    快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
        select * from table where ?;

    当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

        select * from table where ? lock in share mode;
        select * from table where ? for update;
        insert into table values (…);
        update table set ? where ?;
        delete from table where ?;

    所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外, 其他的操作,都加的是X锁 (排它锁)。

补充

1、事务隔离级别为读提交时,写数据只会锁住相应的行
2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
3、事务隔离级别为串行化时,读写数据都会锁住整张表
4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
5、MYSQL MVCC实现机制参考链接:https://blog.csdn.net/whoamiyang/article/details/51901888 【这个不错】
6、关于next-key 锁可以参考链接:https://blog.csdn.net/bigtree_3721/article/details/73731377


3事务传播行为

支持当前事务的情况:
TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况
TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况
TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

PROPAGATION_NESTED 是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。