数据库事务
数据库事务是一个被视为单一的工作单元的操作序列。这些操作应该要么完整地执行,要么完全不执行。事务管理是一个重要组成部分,RDBMS 面向企业应用程序,以确保数据完整性和一致性。事务的概念可以描述为具有以下四个关键属性描述为 ACID:
-
原子性 Atomicity:事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败的。
-
一致性 Consistency:这表示数据库的状态必须从一个一致性状态转换到另一个一致性状态
-
隔离性 Isolation:可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏。考虑到并发性需求,所以隔离是有对应的隔离等级设置(在并发性和数据安全性之间进行平衡)
-
持久性 Durability:一个事务一旦完成全部操作后,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。
多事务并发的问题
在实际项目开发中数据库操作一般都是并发执行的,即有多个事务并发执行,并发执行就可能遇到问题,目前常见的问题如下:
-
丢失更新:两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的;
-
脏读:一个事务看到了另一个事务未提交的更新数据;
-
不可重复读:在同一事务中,多次读取同一数据却返回不同的结果;也就是有其他事务更改了这些数据;
-
幻读:一个事务在执行过程中读取到了另一个事务已提交的插入数据;即在第一个事务开始时读取到一批数据,但此后另一个事务又插入了新数据并提交,此时第一个事务又读取这批数据但发现多了一条,即好像发生幻觉一样。
1、第一类丢失更新(两个事务同时更新,因为其中一个事务的回滚,将另一事务已提交的数据丢失),因为两个事务都去做写操作,然后其中一个事务回滚,导致另一事务已提交的数据丢失
2、脏读(两个事务,其中一事务读另一事务修改后,回滚前的记录),因为读还没有提交事务的数据,当其事务回滚后,读的数据是以前的临时数据
3、不可重复读(其中一个事务中两次读取的数据不一致)
4、第二类丢失更新:两事务全部成功, 没有回滚导致丢失更新,与第一类丢失比较:
第一类是其中一个事务成功, 另一事务失败, 而第二类, 两个事务都全部成功)
5、幻读:一个事务在执行过程中读取到了另一个事务已提交的插入数据;即在第一个事务开始时读取到一批数据,但此后另一个事务又插入了新数据并提交,此时第一个事务又读取这批数据但发现多了一条,即好像发生幻觉一样
针对以上 4 种问题的解决方案为设置对应的隔离等级,这个隔离等级实际上是并发性和数据安全性平衡的结果
标准 SQL 规范中定义了四种隔离级别
-
未提交读 Read Uncommitted:最低隔离级别,一个事务能读取到别的事务未提交的更新数据,很不安全,可能出现丢失更新、脏读、不可重复读、幻读;
-
提交读 Read Committed:一个事务能读取到别的事务提交的更新数据,不能看到未提交的更新数据,不可能出现丢失更新、脏读,但可能出现不可重复读、幻读;
-
可重复读 Repeatable Read:保证同一事务中先后执行的多次查询将返回同一结果,不受其他事务影响,不可能出现丢失更新、脏读、不可重复读,但可能出现幻读;
-
序列化 Serializable:最高隔离级别,不允许事务并发执行,而必须串行化执行,最安全,不可能出现更新、脏读、不可重复读、幻读。
隔离级别越高,数据库事务并发执行性能越差,能处理的操作越少。因此在实际项目开发中为了考虑并发性能一般使用提交读隔离级别,它能避免丢失更新和脏读,尽管不可重复读和幻读不能避免,但可以在可能出现的场合使用悲观锁或乐观锁来解决这些问题。
事务类型
数据库事务类型有本地事务和分布式事务:
-
本地事务:就是普通事务,能保证单台数据库上的操作的 ACID,被限定在一台数据库上;
-
分布式事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成的),分布式事务旨在保证这些本地事务的所有操作的 ACID,使事务可以跨越多台数据库;
Java 事务类型有 JDBC 事务和 JTA 事务:
-
JDBC 事务:就是数据库事务类型中的本地事务,通过 Connection 对象的控制来管理事务;
-
JTA 事务:JTA 指 Java 事务 API(Java Transaction API),是 Java EE 数据库事务规范, JTA 只提供了事务管理接口,由应用程序服务器厂商(如 WebSphere Application Server)提供实现,JTA 事务比 JDBC 更强大,支持分布式事务。
Java EE 事务类型有本地事务和全局事务:
-
本地事务:使用 JDBC 编程实现事务;
-
全局事务:由应用程序服务器提供,使用 JTA 事务;
按是否通过编程实现事务有声明式事务和编程式事务:
-
声明式事务:通过注解或 XML 配置文件指定事务信息;
-
编程式事务:通过编写代码实现事务。
乐观锁和悲观锁
Version 和时间戳
Select * from t_users where id=100 for update
Update t_users set balance=balance+100 where id=100 and version=10
session.lock
MySQL 事务操作
-
START TRANSACTION:显式地开启一个事务。
-
COMMIT 提交事务,并使已对数据库进行的所有修改变为永久性的。
-
ROLLBACK 回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
-
SAVEPOINT S1:使用 SAVEPOINT 允许在事务中创建一个回滚点,一个事务中可以有多个 SAVEPOINT;“S1”代表回滚点名称。
-
ROLLBACK TO [SAVEPOINT] S1:把事务回滚到标记点。
JDBC 事务
Connection conn=DriverManager.getConnection(...);
conn.setAutoCommit(false); //关闭自动提交
try{
PreparedStatement ps1=...
PreparedStatement ps2=...
ps1.executeUpdate();
ps2.executeUpdate();
conn.commit(); //提交修改
} catch(Exception e){
conn.rollback(); //回滚撤销
}....
Spring 中的事务
作为企业级应用程序框架, Spring 在不同的事务管理 API 之上定义了一个抽象层. 而应用程序开发人员不必了解底层的事务管理 API, 就可以使用 Spring 的事务管理机制
引入 Spring 的一个最重要的原因就是过去只有 EJB 能实现的声明式事务管理 CMP,现在通过 Spring 可以在 POJO 上直接实现
Spring 既支持编程式事务管理, 也支持声明式的事务管理.
-
编程式事务管理: 将事务管理代码嵌入到业务方法中来控制事务的提交和回滚. 在编程式管理事务时, 必须在每个事务操作中包含额外的事务管理代码.
-
声明式事务管理: 大多数情况下比编程式事务管理更好用. 它将事务管理代码从业务方法中分离出来, 以声明的方式来实现事务管理. 事务管理作为一种横切关注点, 可以通过 AOP 方法模块化. Spring 通过 Spring AOP 框架支持声明式事务管理(环绕通知)
Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进
行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据
执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。因为事务管理本身就是一个典型的横切逻辑,正是 AOP 的用武之地。Spring 开发团队也意识到了这一点,为声明式事务提供了简单而强大的支持。
通常情况下强烈建议在开发中使用声明式事务,不仅因为其简单,更主要是因为这样使得纯业务代码不被污染,极大方便后期的代码维护。
和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等
Spring 从不同的事务管理 API 中抽象了一整套的事务机制. 开发人员不必了解底层的事务 API, 就可以利用这些事务机制. 有了这些事务机制, 事务管理代码就能独立于特定的事务技术了.
Spring 核心事务管理抽象是 PlatformTransactionManager 接口。它为事务管理封装了一组独立于技术的方法. 无论使用 Spring 的哪种事务管理策略(编程式或声明式), 事务管理器都是必须的