数据库并发问题
1、脏读(读取未提交数据)
A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。
2、不可重复读(前后多次读取同一数据,数据内容不一致)
事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。
3、幻读(前后多次读取,数据总量不一致)
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
事务隔离级别
任何支持事务的数据库,都必须具备四个特性,分别是:
原子性(Atomicity)、
一致性(Consistency)、
隔离性(Isolation)、
持久性(Durability),也就是我们常说的事务ACID,这样才能保证事务((Transaction)中数据的正确性。
而事务的隔离性就是指,多个并发的事务同时访问一个数据库时,一个事务不应该被另一个事务所干扰,每个并发的事务间要相互进行隔离。
读未提交(Read Uncommitted) (所有并发问题都会发生)
读未提交,顾名思义,就是可以读到未提交的内容。
因此,在这种隔离级别下,读不会加任何锁。而写会加排他锁,所以这种隔离级别的一致性是最差的,可能会产生“脏读”、“不可重复读”、“幻读”。
如无特殊情况,基本是不会使用这种隔离级别的。
https://segmentfault.com/a/1190000012654564
读提交(Read Committed) (避免了脏读问题)
读提交,顾名思义,就是只能读到已经提交了的内容。
这是各种系统中最常用的一种隔离级别,也是SQL Server和Oracle的默认隔离级别。这种隔离级别能够有效的避免脏读.
写数据是使用排他锁, 读取数据不加锁而是使用了MVCC机制, 这样就可以大大提高并发读写效率, 写不影响读, 因为读并未加锁, 读的是记录的镜像版本
事务启动后(事务真正启动时会生成整个库的快照start trasaction+begin+第一条sql), 读使用的MVCC“快照读”的方式, 在一个事务中多次查询都是查到事务启动前的数据快照, 不会读到数据库未提交的更新数据. 因为一旦该数据修改被提交了, 事务查询到的数据就是这次提交成功后的快照(一个事务中2次查询数据不一样->不可重复读)
https://segmentfault.com/a/1190000012655091
可重复读(Repeated Read) (避免脏读,不可重复读, 幻读(mysql高版本))
可重复读,顾名思义,就是专门针对“不可重复读”这种情况而制定的隔离级别,自然,它就可以有效的避免“不可重复读”。而它也是MySql的默认隔离级别。
在这个级别下,普通的查询同样是使用的“快照读”,但是,和“读提交”不同的是,当事务启动时,就不允许进行“修改操作(Update)”了,而“不可重复读”恰恰是因为两次读取之间进行了数据的修改,因此,“可重复读”能够有效的避免“不可重复读”,但却避免不了“幻读”,因为幻读是由于“插入或者删除操作(Insert or Delete)”而产生的(MySql中的不可重复读级别可以避免幻读)
串行化(Serializable) (避免所有并发问题)
这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。
这种级别下,“脏读”、“不可重复读”、“幻读”都可以被避免,但是执行效率奇差,性能开销也最大,所以基本没人会用。
总结一下
为什么会出现“脏读”?因为没有“select”操作没有规矩。
为什么会出现“不可重复读”?因为“update”操作没有规矩。
为什么会出现“幻读”?因为“insert”和“delete”操作没有规矩。
MVCC:Snapshot Read(快照读) vs Current Read(当前读)
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。
MVCC最大的好处:读不加锁,读写不冲突。
在MVCC并发控制中,读操作可以分成两类:
快照读:读取的是记录的可见版本 (有可能是历史版本),不用加锁。通过MVVC(多版本控制)和undo log来实现的
当你执行select *之后,在A与B事务中都会一样的数据,这是不用想的,当执行select的时候,innodb默认会执行快照读,相当于就是给你目前的状态找了一张照片,以后执行select 的时候就会返回当前照片里面的数据,当其他事务提交了也对你不造成影响,和你没关系,这就实现了可重复读了.
那这个照片是什么时候生成的呢?不是开启事务的时候,是当你第一次执行select的时候,也就是说,当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据,之后无论再有其他事务commit都没有关系,因为照片已经生成了,而且不会再生成了,以后都会参考这张照片。
如果当前事务commit后, 再进行新的事务或者直接查询,就可以看到其他已提交的事务作出的修改(生产新快照)
使用场景: 简单的select操作,属于快照读,不加锁。
select * from table where ?;
当前读:读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。通过加record lock(记录锁)和gap lock(间隙锁)来实现的
2、update、insert、delete 当前读
当你执行这几个操作的时候默认会执行当前读,也就是会读取最新的记录,也就是别的事务提交的数据你也可以看到,这样很好理解啊,假设你要update一个记录,update会立即去查最新的数据作为修改的基准数据, 并把这个基准数据锁住, 不然其他事务在update结束前修改这个数据. 默认加的是排他锁,也就是你读都不可以,这样就可以保证数据不会出错了。但注意一点,就算你这里加了写锁,别的事务也还是能访问的,是不是很奇怪?数据库采取了一致性非锁定读,别的事务会去读取一个快照数据。
如果在事务中select, 结果看不到其他事务已经提交的修改(commit以后重新select就可以), 但update时,会以其他事务已经提交的数据做为基准进行update. update成功后,会自动commit, 再select就会看到最新的数据.(如果update不成功, 比如没有找到需要update的数据,就不会commit, select到的数据还是最开始的快照, 看不到其他事务提交的内容)
当前读:特殊的读操作和插入/更新/删除操作,属于当前读,需要加锁。
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锁 (排它锁)。
https://www.cnblogs.com/crazylqy/p/7611069.html
SpringBoot中使用事务
Maven中配置(使用JDBC事务管理器)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
常用其他事务管理器
Main方法注解事务管理器@EnableTransactionManagement
@SpringBootApplication
@EnableTransactionManagement
@EnableEurekaClient
@MapperScan("com.example.demo.dao")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
事务三大接口
PlatformTransactionManager 事务管理器: 定义使用哪一个事务管理器
TransactionDefinition 事务的一些基础信息,如超时时间、隔离级别、传播属性等: 定义默认的基础信息
TransactionStatus 事务的一些状态信息,如是否一个新的事务、是否已被标记为回滚
Spring事务隔离级别
操作名称 | 说明 | 级别 |
---|---|---|
@Transactional(isolation = Isolation.DEFAULT) | 默认隔离级别,和数据库的中的4种对应,数据库中用啥,spring事务隔离就用啥 | -1 |
@Transactional(isolation = Isolation.READ_UNCOMMITTED) | 读取未提交数据(会出现脏读, 不可重复读,幻读),基本不使用 | 1 |
@Transactional(isolation = Isolation.READ_COMMITTED)(SQLSERVER默认) | 读取已提交数据(会出现不可重复读和幻读) | 2 |
@Transactional(isolation = Isolation.REPEATABLE_READ) | 可重复读(会出现幻读) | 4 |
@Transactional(isolation = Isolation.SERIALIZABLE) | 串行化 | 8 |
Spring事务传播属性
传播属性 | 说明 |
---|---|
@Transactional(propagation = Propagation.REQUIRED) | 支持当前事务,如果当前有事务, 那么加入事务, 如果当前没有事务则新建一个(默认情况) |
@Transactional(propagation = Propagation.NOT_SUPPORTED) | 以非事务方式执行操作,如果当前存在事务就把当前事务挂起,执行完后恢复事务(忽略当前事务) |
@Transactional(propagation = Propagation.SUPPORTS ) | 如果当前有事务则加入,如果没有则不用事务 |
@Transactional(propagation = Propagation.MANDATORY ) | 支持当前事务,如果当前没有事务,则抛出异常。(当前必须有事务) |
@Transactional(propagation = Propagation.NEVER) | 以非事务方式执行,如果当前存在事务,则抛出异常。(当前必须不能有事务) |
@Transactional(propagation = Propagation.REQUIRES_NEW) | 支持当前事务,如果当前有事务,则挂起当前事务,然后新创建一个事务,如果当前没有事务,则自己创建一个事务。 |
@Transactional(propagation = Propagation.NESTED) | 如果当前存在事务,则嵌套在当前事务中。如果当前没有事务,则新建一个事务自己执行(和required一样)。嵌套的事务使用保存点作为回滚点,当内部事务回滚时不会影响外部事 |