事务的隔离级别
- 事务的概念及特性
事务,一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit).
例如:在关系数据库中,一个事务可以是一条SQL语句,一组SQL语句或整个程序。
特性
事务是恢复和并发控制的基本单位。
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
- 事务的隔离级别
定义:
在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。
问题的提出:
数据库是要被广大客户所共享访问的,那么在数据库操作过程中很可能出现以下几种不确定的情况.
更新丢失
两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
脏读
一个事务读取到了另一个事务未提交的数据操作结果。这是相当危险的,因为很可能所有的操作都被回滚。
不可重复读
不可重复读(Non-repeatable Reads):一个事务对同一行数据重复读取两次,但是却得到了不同的结果。
包括以下情况:
(1) 虚读:事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时得到与前一次不同的值。
(2) 幻读(Phantom Reads):事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。
解决方案
为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。
未授权读取
也称为读未提交(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
授权读取
也称为读提交(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
可重复读取(Repeatable Read)
可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
序列化(Serializable)
序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
Mysql中查看事务隔离级别
SELECT @@tx_isolation
设置事务的隔离级别
SET tx_isolation='read-uncommitted';
SET tx_isolation='read-committed'
SET tx_isolation='repeatable-read';
package cn.bdqn.test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class TransactionLevel {
public static void main(String[] args) {
// 1. 创建连接
Connection connection = createConnection();
try {
//设置手动关闭事务
connection.setAutoCommit(false);
transactionLevel(connection);
// 设置级别
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
transactionLevel(connection);
insert(connection);
delete(connection);
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
close(connection);
}
//获取数据库链接
public static Connection createConnection(){
Connection connection = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test","root","root");
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
//新增用户
public static void insert(Connection connection)throws SQLException{
Statement stat = connection.createStatement();
String sql = "insert into emp(name,salary) value('haha',5000)";
stat.execute(sql);
close(stat);
}
//删除指定的用户
public static void delete(Connection connection)throws SQLException{
Statement stat = connection.createStatement();
String sql = "delete from emp where id = 5";
stat.execute(sql);
close(stat);
}
//关闭链接
public static void close(Connection connection){
if(connection!=null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
connection=null;
}
}
//关闭链接
public static void close(Statement stat){
if(stat!=null){
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
}
stat=null;
}
}
//获取当前事务的级别
public static void transactionLevel(Connection connection) throws SQLException{
int level = connection.getTransactionIsolation();
if (level == Connection.TRANSACTION_NONE) {
System.out.println("当前事务的级别: TRANSACTION_NONE");
System.out.println(level);
} else if (level == Connection.TRANSACTION_READ_UNCOMMITTED) {
System.out.println("当前事务的级别: TRANSACTION_READ_UNCOMMITTED");
System.out.println(level);
} else if (level == Connection.TRANSACTION_READ_COMMITTED) {
System.out.println("当前事务的级别: TRANSACTION_READ_COMMITTED");
System.out.println(level);
} else if (level == Connection.TRANSACTION_REPEATABLE_READ) {
System.out.println("当前事务的级别: TRANSACTION_REPEATABLE_READ");
System.out.println(level);
} else if (level == Connection.TRANSACTION_SERIALIZABLE) {
System.out.println("当前事务的级别: TRANSACTION_SERIALIZABLE");
System.out.println(level);
}
}
}
Set transaction isolation level [read committed||serializable]
3. oracle中的锁
排他锁(Exclusive Locks,简称X锁),又称为写锁、独占锁,是一种基本的锁类型。
定义
若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
Oracle数据库支持多个用户同时与数据库进行交互,每个用户都可以同时运行自己的事务,从而也需要对并发访问进行控制。Oracle也是用“锁”的机制来防止各个事务之间的相互影响,对并发访问进行控制的,保证数据的一致性和完整性。当一个事务或操作企图防止另一个事务对其操作的对象产生影响时,该事务或操作就对该对象进行锁定,其他事务就只能在该事务释放锁之后才能操作该对象。
在大多数情况下,锁对于开发人员来说是透明的,不用显式地加锁,即不用指定锁的分类、级别、类型或模式。如,当更改记录时,Oracle会自动地对相关的记录加相应的锁;当执行一个PL/SQL过程时,该过程就会自动地处于被锁定的状态,允许其他用户执行它,但不允许其他用户采用任何方式更改该过程。当然,Oracle也允许用户使用Lock Table语句显式地对被锁定的对象加指定模式的锁。
锁有2种最基本、最简单的类型:排他锁(eXclusive lock,即X锁)、共享锁(Share lock,即S锁)。这两种锁及其锁定的示意图如下图所示。该图中显式了不同的锁的作用,以及相互之间的相容规则。
排他锁又称为写锁。如果事务T在数据库对象A上加了X锁,则只允许T读取、更改A。其他任何事务Ti都既不能对A加S锁也不能加X锁,直到T释放A上的X锁为止。这就保证了其他事务Ti在事务T释放A上的X锁之前,不能再读取、更改、使用A,即保护A不被同时地读、写。
共享锁又称为读锁。如果事务T在数据库对象A上加了S锁,则只允许T读取A但不能更改A。其他任何事务Ti都只能对A加S锁而不能加X锁,直到T释放A上的S锁为止。这就保证了其他事务Ti在事务T释放A上的S锁之前,只能读取A但不能更改A,即保护A不被同时地写。
锁除了对操作进行限制外,锁对锁也进行限制,排他锁与共享锁的相容规则如表所示。
一、锁定的粒度与意向锁
锁定对象的大小被称为锁定的粒度(granularity)。锁定对象可以是逻辑单元(如数据库、表、记录、列、索引等),也可以是物理单元(如数据页、索引页等)。
锁定的粒度与系统的并发度和并发控制的开销密切相关。一般地,锁定的粒度越大,需要锁定的对象就越少,可选择性就越小,并发度就越小,开销就越小;反之,锁定的粒度越小,需要锁定的对象就越多,可选择性就越大,并发度就越大,开销就越大。
例如,如果锁定的粒度是表,事务T1需要操作表A中的记录r1,则T1必须对包含记录r1的表A加锁。在T1对A加锁之后,事务T2需要操作表A中的记录r2,因此就需要对表A加锁,但此时因为T1已经在表A上加了锁,所以T2的加锁就会失败,就被迫等待,直到T1释放锁为止,事务T1和T2就不能并发执行了;如果锁定的粒度是记录,则事务T1可以对表A中的记录r1加锁,同时事务T2也可以对表A中的记录r2加锁,所以事务T1和T2就可以并发执行了,并发度就增大了,但维护这个并发度就需要更多的开销了。
1、多粒度锁定与多粒度树
如果在一个数据库管理系统中,同时支持多种锁定粒度供事务选择,这种锁定方法就被称为多粒度锁定(multiple granularity locking)。
如下图所示是一个四级的多粒度树。该树的根节点是数据库,表示最大的粒度;页节点是列,表示最小的粒度。
从前面的例子可知,选择锁定粒度时应该同时考虑并发度与开销两个因素。以求最优的效果。一般地,需要处理大量记录的事务可以以表为锁定粒度;需要处理多个表中的大量记录的事务可以以数据库为锁定粒度;而只需要处理某个表中少量记录的事务,则可以以记录为锁定粒度。
多粒度锁定协议是指,允许对多粒度树中的节点单独地加锁,另外,对一个节点加锁还意味着对这个节点的各级子节点也加同样的锁。
因此,可以用两种方法对多粒度树中的节点加锁:显式锁定、隐式锁定。显式锁定是在事务中明确指定要加在节点上的锁;隐式锁定是由于在其上级节点中加显式锁时而使该节点获得的锁。
在多粒度锁定中,显式锁定与隐式锁定的作用与相容规则都是一样的。因此,当系统检查锁定的冲突时,不仅要检查显式锁定还要检查隐式锁定。
一般地,当对一个节点加锁时,系统第一要检查在该节点上有无显式锁定与之冲突;第二要检查其所有上级节点有无显式锁定与之冲突,以便查看在该节点上加该显式锁定,是否会与从上级节点获得的隐式锁定有冲突;第三要检查所有下级节点有无显式锁定与之冲突,以便查看在该节点上加该显式锁定,是否会使下级节点从该节点获得的隐式锁定与其显式锁定有冲突。
这种检查锁定冲突的方法的效率很低,所以引进了意向锁(Intended lock)的概念。
2、意向锁
意向锁的含义是,如果对一个节点加某种意向锁,则会对该节点的各级下级节点加这种锁;如果对一个节点加某种锁,则必须先对该节点的各级上级节点加这种意向锁。
例如,对记录r1加锁时,必须先对它所在的表A加意向锁。于是,事务T1对表A加X锁时,系统只需要检查根节点数据库D和表A是否已经加了不相容的锁,而不再需要检查表A中每个记录是否加了X锁。
有如下几种意向锁。
一、IS锁(Intended Share lock,意向共享锁)
如果对一个节点加IS锁,则表示对它的所有下级节点有加S锁的意向;如果对一个节点加S锁,则必须先对该节点的各级上级节点加IS锁。
二、IX锁(Intended eXclusive lock,意向排他锁)
如果对一个节点加IX锁,则表示对它的所有下级节点有加X锁的意向;如果对一个节点加X锁,则必须先对该节点的各级上级节点加IX锁。
三、SIX锁(Share Intended eXclusive lock,共享意向排他锁)
如果对一个节点加SIX锁,则表示对它加S锁,然后再加IX锁,即SIX=S+IX。例如,如果事务T对表A加SIX锁,则表示事务T要读取整个表(S锁的作用),同时还会更新某些记录(IX锁的作用)。
包括意向锁的各种锁之间的相容规则如表所示。
例如,当事务T对表A保持的锁是S锁时,事务Ti不可以获得对表A的IX锁,但可以获得对A的IS锁。所以S锁与IX锁的强度相当,但比IS锁的强度要强。
同上理,可以逐个地推导出这些锁的强度关系,如图所示。图中上面的锁比下面的锁的强度要强,同级的锁强度相当。一个事务在申请锁定时以强锁代替弱锁是安全的(但会降低并发度),反之则不然。申请锁定时应该按自上而下的次序进行,释放锁定时则应该按自下而上的次序进行。
具有意向锁的多粒度锁定方法能提高系统的并发度,减少加锁和解锁的开销,已经在实际DBMS中得到应用,如Oracle数据库。
二、锁的分类
Oracle主要有2种锁:DDL锁(字典锁)、DML锁(数据锁)。
1、 DDL锁
当用户发布DDL(Data Definition Language)语句时会对涉及的对象加DDL锁。由于DDL语句会更改数据字典,所以该锁也被称为字典锁。
DDL锁能防止在用DML语句操作数据库表时,对表进行删除,或对表的结构进行更改。
对于DDL锁,要注意的是:
$$ DDL锁只锁定DDL操作所涉及的对象,而不会锁定数据字典中的所有对象。
$$ DDL锁由Oracle自动加锁和释放。不能显式地给对象加DDL锁,即没有加DDL锁的语句。
$$ 在过程中引用的对象,在过程编译结束之前不能被改变或删除,即不能被加排他DDL锁。
DDL锁的类型与特征如表所示。
DDL锁很少会在系统里引起争用,因为它们的保持时间都非常短暂。但DDL锁的存在却不容忽视,尤其是在申请排他DDL锁时。
2、DML锁
当用户发布DML(Data Manipulation Language)语句(如insert、update、delete)时会对涉及的对象加DML锁。由于DML语句涉及的是数据操作,所以该锁也被称为数据锁。
DML锁能防止多个事务并发访问数据时,对数据的一致性和完整性的破坏。
根据锁定的粒度和意向,DML锁有几种模式。在执行Lock Table语句或执行不同的DML语句时会加不同模式的DML锁。当一个事务在一个表上加了某种模式的DML锁之后,另一个事务在该表上所能执行的DML操作会受到限制,如下表所示。其中有的锁模式有2个名称,这是因为不同的Oracle版本在提到它们时使用了不同的表示方法。
三、死锁现象及其解决
死锁是一种比较严重的、特殊的锁争用类型。在这种锁争用中,两个或两个以上的用户正在等待对方锁定的资源。因此,如果不进行某种干预,任意一个事务都无法完成。
假设当前有两个会话,分别执行的事务如图所示。显然这两个会话之间由于互相锁定了对方所需要锁定的对象,所以发生了死锁。
“提示”Oracle提供了有效的死锁检测机制,周期性地(通常只有几秒钟)诊断系统中有无死锁,并提示用户。
当Oracle检测到死锁后,会在预警文件和跟踪文件中记录下死锁的信息,在跟踪文件中详细地记录了检测到死锁的时间、被死锁的语句、阻塞者会话、等待者会话、操作系统用户的信息(如用户名、用户所在的计算机、用户使用的应用程序)等有助于确定死锁及其处理方法的信息。
即使Oracle为会话A提示了检测到死锁的信息,并且回退了会话A的第2个update语句,但会话A仍然在第1个update语句上保持着锁,并阻塞了会话B的第2个update语句.
当出现检测到死锁的信息后,用户自己(或DBA通知相应的用户)执行commit语句或rollback语句,使事务结束,释放所加的锁。还可以使用 ALTER SYSTEM KILL SESSION 'sid, serial#' 语句来杀死会话,强行解决锁争用。