数据库——事务基础
什么是数据库事务?
数据库事务有严格的定义,必须同时满足4个特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),简称ACID。
- 原子性:表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交,事务中任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。
- 一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。
- 隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。
- 持久性:一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须保证能够通过某种机制恢复数据。
数据“一致性”是最终目标,其他的特性都是为达到这个目标的措施、要求或手段。
数据库管理系统一般采用重执行日志保证原子性、一致性和持久性,重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可以根据重执行日志撤销已经执行的操作。对于已经提交的事务,即使数据库崩溃,在重启数据库时也能够根据日志对尚未持久化的数据进行相应的重执行操作。
数据库锁机制
数据库通过锁的机制解决并发访问的问题。
按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表锁定,后者对表中特定行进行锁定。
从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定,也防止其他的共享锁定。
事务隔离级别
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别。不同的事务隔离级别能够解决的数据并发问题的能力是不同的。
- READ UNCOMMITTED(未提交读):事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read),使用这个级别的数据库拥有最高的并发性和吞吐量。在实际应用中一般很少使用。
- READ COMMITTED(提交读):大多数数据库系统的默认隔离级别都是这个(但MySQL不是)。这个级别满足隔离性的定义:一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read),因为两次执行同样的查询,可能得到不一样的结果。
- REPEATABLE READ(可重复读):该级别解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但还是没解决幻读(Phantom Read)的问题。所谓幻读:指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。不过MySQL的InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。该级别是MySQL的默认事务隔离级别。
- SERIALIZABLE(可串行化):该级别是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。该级别会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
事务日志
事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志使用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。事务日志持久之后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。
MySQL中的事务
MySQL 提供了两种事务型的存储引擎:InnoDB 和 NDBCluster。
MySQL 默认采用自动提交(AUTOCOMMIT)模式。也就是说,如果不是显示地开始一个事务,则每个查询都被当做一个事务执行提交操作。在当前连接中,可以通过设置AUTOCOMMIT变量来启动或者禁用自动提交模式:
SHOW VARIABLES LIKE 'AUTOCOMMIT';
SET AUTOCOMMIT= 1;
1 或者 ON 表示启用,0 或者 OFF 表示禁用。当 AUTOCOMMIT=0时,所有的查询都是在一个事务中,直到显示地执行 COMMIT 提交或者 ROLLBACK 回滚,该事务结束,同时又开始了另一个新事务。
JDBC 对事务的支持
并不是所有的数据库都支持事务,即使支持事务的数据库也并非支持所有的事务隔离级别,用户可以通过 Connection#getMetaData()方法获取 DatabaseMetaData 对象,并通过该对象的 supportsTransactions()、supportsTransactionIsolationLevel(int level)方法查看底层数据库的事务支持情况。
Connection 默认情况下是自动提交的,也即每条执行的 SQL 都对应一个事务,为了能够将多条 SQL 当成一个事务执行,必须先通过 Connection#setAutoCommit(false)阻止Connection自动提交,并可通过 Connection#setTransactionIsolation()设置事务的隔离级别,Connection 中定义了对应 SQL92 标准 4 个事务隔离级别的常量。
通过 Connection#commit()提交事务,通过 Connection#rollback()回滚事务。
1 Connection conn ; 2 try { 3 // 获取数据连接 4 conn = DriverManager.getConnection(); 5 // 关闭自动提交的机制 6 conn.setAutoCommit(false); 7 // 设置事务隔离级别 8 conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); 9 Statement stmt = conn.createStatement(); 10 11 int rows = stmt.executeUpdate("INSERT INTO t_topic VALUES(1,'tom')"); 12 rows = stmt.executeUpdate("UPDATE t_user set topic_nums=topic_nums+1 WHERE user_id=1"); 13 14 conn.commit(); // 提交事务 15 }catch(Exception e) { 16 ... 17 conn.rollback(); // 回滚事务 18 }finally { 19 ... 20 }
在JDBC 2.0中,事务最终只能有两个操作:提交和回滚。
在JDBC 3.0引入了一个全新的保存点特性,Savepoint接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点,而并非像JDBC 2.0一样只回滚到开始事务的点。
下面的代码使用了保存点的功能,在发生特定问题时,回滚到指定的保存点,而非回滚整个事务。
1 ... 2 Statement stmt = conn.createStatement(); 3 int rows = stmt.executeUpdate("INSERT INTO t_topic VALUES(1, 'tom')"); 4 Savepoint svpt = conn.setSavepoint("savePoint1");//1.设置一个保存点 5 rows = stmt.executeUpdate("UPDATE t_user set topic_nums = topic_nums + 1 WHERE user_id = 1"); 6 ... 7 conn.rollback(svpt);//2.回滚到1处的savePoint1,1之前的SQL操作,在整个事务提交后依然提交,但1到2之间的SQL操作被撤销了 8 ... 9 conn.commit();//3.提交事务