数据库的隔离级别及相关...

Source
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiong9999/article/details/85060264

要明白隔离级别,得先明白数据库中的事务(Database Transaction)

数据库事务(Database Transaction)是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。 正常的情况下,这些操作将顺利进行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果在这一系列过程中任何一个环节出了差错,数据库中所有信息都必须保持交易前的状态不变,比如最后一步更新用户信息时失败而导致交易失败,那么必须保证这笔失败的交易不影响数据库的状态–库存信息没有被更新、用户也没有付款,订单也没有生成。否则,数据库的信息将会一片混乱而不可预测。

简单点:执行多条sql语句(DML),这些语句要么都成功,要么都失败,保证数据库不出现因为DML操作导致的数据问题。


四个特性:

1. 原子性(atomic)(atomicity)

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则完全不能对数据库有任何影响。
简单点:针对某一个事务,要么成功,要么失败。

2. 一致性(consistent)(consistency)

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
简单点:针对某一个事务,事务前后的数据要都合法。

3. 隔离性(insulation)(isolation)

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
简单点:针对多个并行的事务,各干各的事,互不干扰。

4. 持久性(Duration)(durability)

事务完成之后,它对于系统的影响是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
简单点:针对某一个事务,成功结束后,结果永远不变。



什么是隔离级别

一个事务必须与由其他事务进行的资源或数据更改相隔离的程度。当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。

简单点:针对多个 并行 的事务,如果事务之间必须有数据的交互,则通过这个隔离级别来保证数据的准确性。

并行事务,单个或顺序执行的事务不涉及隔离级别。


要明白隔离级别,还得明白如果没有这个隔离级别的设定,会出现什么问题

1、更新丢失

两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
A事务更新的一个值,B事务也更新了这个值,两个最后还都提交了。
简单点:(更新被覆盖了)A事务的更新被B事务更新覆盖了。

2. 脏读

指在一个事务处理过程里读取了另一个未提交的事务中的数据。指在一个事务处理过程里读取了另一个未提交的事务中的数据。

当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下

update account set money=money+100 where name=’B’;  (此时A通知B)

update account set money=money - 100 where name=’A’;

当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
简单点:(读了临时数据)A事务修改了、但未提交的数据,被B事务读取了,A最后还回滚了。。。

3. 不可重复读

指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
简单点:(查询不一致)A事务重复查询某个值,结果这个值不一样,因为被B事务修改了。

4. 虚读(幻读)

幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
简单点:(更新时落数据)A事务更新了些数据,提交后发现还露了一个,原来这个是B事务刚加进去的。



好,现在开始隔离级别(四种)

1. Read uncommitted(未授权读取、读未提交)

如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。

避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。

场景:
1. 第一个事务为写;
2. 其它事务只可以读、不能再写第一个事务操作的数据。**

特点:
1. 只避免了更新丢失
2. 但还是有脏读的情况出现(如果第一个事务最后没有提交,就是脏读!)==**

简单点:未提交的数据只可以被读取。(只避免了更新丢失)

2. Read committed(授权读取、读提交)

读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

场景:
1. 如果一个事务此刻在写(还没有提交的数据),则别的事务不可以读或者写它写的数据
2. 如果一个事务此刻在读(已经提交了的数据),则别的事务可以读 它读的数据

特点:
1. 避免了更新丢失、脏读
2. 但没有避免不可重复读:
如果一个事务此刻在读(已经提交了的数据),则别的事务还可以写它读的数据,如果这个事务再读这条记录,则出现不可重复读的情况。

简单点:只能读提交后的数据。(避免了更新丢失、脏读)

3. Repeatable read(可重复读取)

读取数据的事务将会禁止写事务(但允许读事务),
写事务则禁止任何其他事务。

避免了不可重复读取和脏读,但是有时可能出现幻读。这可以通过“共享读锁”和“排他写锁”实现

场景:
1. 第一个事务把数据库的记录做了更新,并提交了(写);
2. 此时第二个事务来insert了一条语句,并提交了(写);
3. 第一个事务查询了一下,发现还有一条记录没有被更新,有点见鬼了(读)。

特点:
虽然看起来是读写分离了,但事务之间还是有交叉

简单点:读就是读(多读),写就是写(单写),不能一起干。

4. Serializable(序列化)

提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

序列化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

简单点:事务必须一个一个顺序执行。

  • 隔离级别最低的是Read uncommitted级别
  • 隔离级别最高的是Serializable级别,
  • 级别越高,执行效率就越低。
  • 大多数数据库的默认级别就是Read committed。
  • 在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。

像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。

在这里插入图片描述

附加一个事务的传播属性:

事务传播属性是spring针对业务上多个事务处理方案封装的方案,不是数据库中的。

  1. PROPAGATION_REQUIRED – 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。

当前方法必须要求开启事务,如果当前线程不存在事务,则开启新的事务,如果当前线程已经存在事务,就加入到当前事务。这个是经常使用的。但是要注意的就是一旦事务中某一个方法回滚,当前事务上下文里面所有的操作都回滚。

  1. PROPAGATION_REQUIRES_NEW – 新建事务,如果当前存在事务,把当前事务挂起。

当前方法必须要求开启新的事务,如果当前线程已经存在事务上下文,就暂停当前事务,等到新事务结束之后,再继续恢复之前的事务。两个事务之间不会互相影响。经常可以用到的场景就是在业务发生异常的时候发送短消息。如果业务发生异常,业务回滚,但是由于发送段消息是新的事务,不会受到业务异常的影响。

  1. PROPAGATION_MANDATORY – 支持当前事务,如果当前没有事务,就抛出异常。

当前方法必须要求事务,如果当前线程不存在事务,就抛出异常,如果存在,就加入到事务里。

  1. PROPAGATION_SUPPORTS – 支持当前事务,如果当前没有事务,就以非事务方式执行。

当前方法支持事务,如果当前线程存在事务,就加入到事务中去,如果不存在,不做任何操作。

  1. PROPAGATION_NOT_SUPPORTED – 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

当前方法不支持事务,如果当前线程存在事务,就挂起当前事务,执行完当前方法,恢复事务。一般情况下在查询的时候使用,如果一个方法只是查询,并且非常耗时,就可以使用Not Support,避免事务时间超长。

  1. PROPAGATION_NEVER – 以非事务方式执行,如果当前存在事务,则抛出异常。

当前方法不支持事务,如果当前线程存在事务,则抛出异常。这种用的比较少。

  1. PROPAGATION_NESTED – 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,


实战

  • 在MySQL数据库中 查看 当前事务的隔离级别:
    select @@tx_isolation;
  • 在MySQL数据库中设置事务的隔离 级别:
    set  [glogal | session]  transaction isolation level 隔离级别名称;
--或者
    set tx_isolation=’隔离级别名称;

JDBC的设置:

	Connection connection = getConnection();
	try {
		//设置隔离级别
		connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
		//开启事务
		connection.setAutoCommit(false);

Spring中的事务配置:

  1. 注入事务管理的组件Bean
<!-- 配置事务管理组件 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dbcp">  <!-- dbcp是连接池组件(org.apache.commons.dbcp2.BasicDataSource)的bean -->
</bean>
  1. 使用方式
  • 注解:
<!-- 采用注解方式:有源码的情况下,将注解加在方法上 -->
<!-- 开启事务注解标记@Transactional,当调用带@Transactional标记的方法时,将txManager的事务管理功能切入进去 -->
<tx:annotation-driven transactional-manager="txManager" />
<!-- 在需要事务管理的方法上加上@Transactional注解即可 -->
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class, RuntimeException.class})
  • 注入DataSourceTransactionManager
@Autowired  
private DataSourceTransactionManager txManager; 

public void updateDb(Bean bean) {  
	DefaultTransactionDefinition def = new DefaultTransactionDefinition();
	def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
	TransactionStatus status = txManager.getTransaction(def); 
    try { 
        //do someThing
        txManager.commit(status);  
    } catch (Exception e) {  
        txManager.rollback(status);  
    }  
}  

注意:

  • 设置数据库的隔离级别一定要是在开启事务之前。
  • 隔离级别的设置只对当前链接有效。
  • @Transactional只能被应用到public方法上,对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能.
  • 默认情况下,一个有事务方法, 遇到RuntiomeException 时会回滚 . 遇到 受检查的异常 是不会回滚 的. 要想所有异常都回滚,要加上 @Transactional( rollbackFor={Exception.class,其它异常}) 。

数据库锁(Mysql)

先讲死锁吧

  • 死锁产生的根本原因是两个以上的进程都要求对方释放资源,以至于进程都一直等待。在代码上是因为两个或者以上的事务都要求另一个释放资源。

  • 死锁产生的四个必要条件:互斥条件、环路条件、请求保持、不可剥夺,缺一不可,相对应的只要破坏其中一种条件死锁就不会产生。

例如:
下面语句会优先使用name索引,因为name不是主键索引,还会用到主键索引

update mk_user set name ='1' where `name`='idis12';

第二条语句是首先使用主键索引,再使用name索引 如果两条语句同时执行,

update mk_user set name='12'  where id=12;

结果:第一条语句执行了name索引等待第二条释放主键索引,第二条执行了主键索引等待第一条的name索引,这样就造成了死锁。
解决方法:改造第一条语句 使其根据主键值进行更新

update mk_user set name='1' where id=(select id from mk_user where name='idis12' );
从数据操作的粒度分类:
  1. 表锁:每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
  2. 行级锁:每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
    注意:是通过对索引上锁来实现,也就是说在查询索引字段时,上行锁,不是针对记录加的锁。
  3. 间隙锁(Next-Key锁)当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL就是上的间隙锁:

Select * from emp where empid > 100 for update;

防止其它事务插入102以后的数据,因此要尽量使用相等条件来访问更新数据,避免使用范围条件。
请求不存在的数据时,也是上的间隙锁。
5. 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

从对数据操作的类型分类
  1. 读锁(共享锁、S锁):针对同一份数据,多个读操作可以同时进行而不会互相影响,多个事务只能读数据不能改数据。
  2. 写锁(排它锁、X锁):当前写操作没有完成前,它会阻断其他写锁和读锁。
从数据库引擎分类:
  1. InnoDB三个锁都有;
  2. MyISM只有表锁(也就是说这个库没有事务)

在这里插入图片描述