什么是事务
数据库中一些操作的集合是一个独立的单元,事务就是构成单一逻辑工作单位的集合。
为什么需要事务
事务是为解决数据安全操作提出的,事务控制实际上就是控制数据的安全访问。
比如:银行转帐业务,账户A给账户B转帐100元,需要账户A余额减100元,账户B余额加100元,两个需要同时发生。完成这种操作需要保证要么全部成功,要么全部失败。
什么是回滚
未能成功完成的事务成为中止事务,对中止事务造成的变更需要进行撤销处理,称为事务回滚。
事务的特性(ACID 原则)
- 原子性(atomicity):对事务中的全部操作是不可分割的,要么全部完成,要么都不执行。
- 一致性(consistency):事务执行之前和执行之后,数据库都必须处于一致性状态。
- 隔离性(isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
- 持久性(durability):对于任意已提交的事务,系统必须保证该对数据库的改变不丢失,即使数据库出现故障。
Java JDBC 事务机制
比如有个业务:当我们修改一个信息后再去查询这个信息。这是一个简单的业务,实现起来也非常容易,但是当这个业务放在多线程高并发的平台下,问题自然就出现了。
比如当执行了一个修改后,在查询之前有一个线程也执行了修改语句,这时再执行查询,看到的信息就有可能和我们修改的不同。为了解决这一问题,就引入了引入JDBC事务机制。
如何操作
把事务操作设置为不自动提交,通过手动提交就能实现事务的处理。
搭建实验环境实验,在当前数据库中创建一个测试表:
CREATE TABLE tb (
id INT UNSIGNED NOT NULL AUTO_INCREMENT KEY COMMENT '编号',
name VARCHAR(20) NOT NULL COMMENT '姓名',
sex VARCHAR(4) NOT NULL COMMENT '性别',
phone VARCHAR(11) NOT NULL COMMENT '手机号码'
);
插入几条测试数据:
INSERT INTO tb
(name, sex, phone)
VALUES
("张三", "男", "139****2234"),
("李四", "女", "130****3239"),
("王二", "男", "136****1234"),
("小王", "女", "137****1735"),
("赵云", "男", "131****1255"),
("关羽", "男", "139****1930");
相关延伸
事务并发处理可能引起的问题
- 脏读(dirty read):一个事务读取了另一个事务尚未提交的数据。
- 不可重复读(non-repeatable read):一个事务的操作导致另一个事务前后两次读取到不同的数据。
- 幻读(phantom read):一个事务的操作导致另一个事务前后两次查询到的结果数据量不同。
举例:
- 事务A、B并发执行,当A事务update后,B事务select读取到A尚未提交的数据,此时A事务rollback,则B读取到的数据是无效的“脏”数据。
- 当B事务select读取数据后,A事务update操作更改B事务select到的数据,此时B事务再次读取该数据,发现前后两次的数据不一样。
- 当B事务select读取数据后,A事务insert或delete了一条满足A事务的select条件的记录,此时B事务再次select,发现查询到前不存在的数据(“幻影”),或者前面的某个记录不见了。
JDBC的事务支持
JDBC对事务的支持体现在三个方面:
1.自动提交模式(Auto-commit mode)
Connection提供了一个auto-commit
的属性来指定事务何时结束。
- a.当auto-commit为
true
时,当每个独立的SQL操作执行完毕,事务立即自动提交,也就是说每个SQL操作都是一个事务的。auto-commit默认为true
。
一个独立SQL操作什么时候什么执行完毕呢?
JDBC规范这样规定
对数据操作语言(DML如insert,update,delete)和数据定义语言(DDL如create,drop),语句一执行完就视为执行完毕。
对select语句,当与它关联的ResultSet对象关闭时,视为执行完毕。
对存储过程或者其他返回多个结果的语句,当与它关联的所有ResultSet对象全部关闭,所有update count(update,delete等语句操作影响的行数)和output parameter(存储过程的输出参数)都已经获取之后,视为执行完毕。
- b.当auto-commit为
false
时,每个事务都必须显式调用commit
方法进行提交,或者显式调用commit方法进行回滚。
2.事务隔离级别(Transaction Isolation Levels)
JDBC提供了5种不同的事务隔离级别。在Connection中进行了定义。
-
TRANSACTION_NONE
:JDBC不支持事务 -
TRANSACTION_READ_UNCOMMITTED
:允许脏读、不可重复读和幻读 -
TRANSACTION_READ_COMMITTED
:禁止脏读,但允许不可重复读和幻读 -
TRANSACTION_REPEATABLE_READ
:禁止脏读和不可重复读,单可以幻读 -
TRANSACTION_SERIALIZABLE
:禁止脏读、不可重复读和幻读
3.保存点(SavePoint)
JDBC定义了SavePoint接口,提供在一个更细粒度的事务控制机制。当设置了一个保存点后,可以rollback到该保存点处的状态,而不是rollback整个事务。
连接对象获取和关闭的时机
现状:
- 连接对象的获取和销毁比较浪费时间
- 一个事务中多个操作,若每个操作都生成一个连接对象,多用户同时连接数据库时,访问效率会非常的低。
需求:
- 一个事务中的多个操作应该用同一个连接对象控制,不然无法实现提交和回滚。一个事务开始时获得连接对象,一个事务结束时关闭连接。
解决:
- 将连接对象的申请交给专门的连接管理类
事务申请连接对象时,通过连接管理类申请,而不再通过DriverManager申请 - 一个事务一定是由一个线程完成的,使用线程局部变量获得同一个连接对象
接口DataSource
该工厂用于提供到此 DataSource 对象所表示的物理数据源的连接。
作为 DriverManager 工具的替代项,DataSource 对象是获取连接的首选方法。
DataSource
接口由驱动程序供应商实现。共有三种类型的实现:
- 基本实现:生成标准的 Connection 对象
- 连接池实现:生成自动参与连接池的 Connection 对象。此实现与中间层连接池管理器一起使用。
- 分布式事务实现:生成一个 Connection 对象,该对象可用于分布式事务,大多数情况下总是参与连接池。此实现与中间层事务管理器一起使用,大多数情况下总是与连接池管理器一起使用。
方法:
-
getConnection()
:尝试建立与此 DataSource 对象所表示的数据源的连接。 -
getConnection(String username, String password)
:尝试建立与此 DataSource 对象所表示的数据源的连接。
实例:
先在src目录下新建配置文件config.properties
:
driver=com.mysql.jdbc.Driver
url=jdbc\:mysql\://127.0.0.1\:3306/empmgs?useUnicode\=true&characterEncoding\=utf-8
username=root
password=root
实例代码:
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;
public class demo4 {
static Properties prop = new Properties();
static DataSource ds = null;
static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
private static Connection conn = null;
private static Statement sm = null;
/**
* 静态初始化块加载注册驱动
*/
static {
try {
prop.load(new FileInputStream("src/config.properties"));
ds = BasicDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
e.printStackTrace();
}
}
static public Connection getConnection(){
try {
//先看线程局部变量
conn = threadLocal.get();
if(conn == null){//线程局部变量中没有保存连接对象
conn = ds.getConnection();
threadLocal.set(conn);//设置连接对象到线程局部变量
}
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
public static void main(String[] args) {
try{
conn = getConnection ();
sm = conn.createStatement();
// 关闭自动提交
conn.setAutoCommit(false);
sm.executeUpdate("UPDATE tb SET name = '老王' WHERE id = 3 ");
sm.executeUpdate("INSERT INTO tb (name, sex, phone) VALUES ('美丽','女','132****5555')");
conn.commit();
System.out.println("运行结束");
} catch (Exception e) {
try {
conn.rollback();
} catch (Exception ex) {
e.printStackTrace();
}
} finally {
if (sm != null) {
try {
sm.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
conn = null;
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}