#处理事务
事务是数据库操作的基本单元,确保一组操作要么全部成功,要么全部失败。
# 事务基础概念
事务遵循ACID特性,保证数据的一致性和可靠性。
原子性:事务中的所有操作要么全部完成,要么全部不完成。
一致性:事务完成后数据库处于一致状态。
隔离性:并发执行的事务互不干扰。
持久性:事务提交后,对数据库的修改永久保存。
JDBC连接默认启用自动提交,每条SQL语句执行后都会单独提交。需要将多条SQL语句作为一个事务执行时,调用setAutoCommit(false)关闭自动提交,并根据执行结果调用commit()或rollback()。
# 事务提交方法
以下示例在同一事务中完成两个账户的余额变更。两条更新都执行成功时提交事务;任意操作失败时回滚整个事务。建表和初始化数据在事务开始前完成,示例结束后删除测试表。
package quickstart;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
public class BasicTransactionExample {
private static final String URL =
"jdbc:yasdb://192.168.1.2:1688/yasdb";
private static final String TABLE = "jdbc_transaction_account";
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.setProperty("user", "username");
properties.setProperty("password", "passwd");
Class.forName("com.yashandb.jdbc.Driver");
prepareTable(properties);
try (Connection connection =
DriverManager.getConnection(URL, properties)) {
// 关闭自动提交,由应用程序控制事务边界。
connection.setAutoCommit(false);
try {
updateBalance(
connection, 1, new BigDecimal("-100.00"));
updateBalance(
connection, 2, new BigDecimal("100.00"));
// 两条更新均成功后提交事务。
connection.commit();
System.out.println("Transaction committed");
} catch (Exception exception) {
// 任意操作失败时回滚整个事务。
connection.rollback();
throw exception;
}
try (Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(
"SELECT account_id, balance FROM " + TABLE
+ " ORDER BY account_id")) {
while (resultSet.next()) {
System.out.println(
"Account " + resultSet.getInt(1)
+ " balance: "
+ resultSet.getBigDecimal(2));
}
}
} finally {
dropTable(properties);
}
}
private static void updateBalance(
Connection connection, int accountId, BigDecimal amount)
throws Exception {
String sql = "UPDATE " + TABLE
+ " SET balance = balance + ? WHERE account_id = ?";
try (PreparedStatement statement =
connection.prepareStatement(sql)) {
statement.setBigDecimal(1, amount);
statement.setInt(2, accountId);
statement.executeUpdate();
}
}
private static void prepareTable(Properties properties)
throws Exception {
try (Connection connection =
DriverManager.getConnection(URL, properties);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS " + TABLE);
statement.execute(
"CREATE TABLE " + TABLE
+ " (account_id INTEGER PRIMARY KEY, "
+ "balance DECIMAL(10, 2))");
statement.executeUpdate(
"INSERT INTO " + TABLE + " VALUES (1, 1000.00)");
statement.executeUpdate(
"INSERT INTO " + TABLE + " VALUES (2, 1000.00)");
}
}
private static void dropTable(Properties properties)
throws Exception {
try (Connection connection =
DriverManager.getConnection(URL, properties);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS " + TABLE);
}
}
}
正常情况下,程序输出如下:
Transaction committed
Account 1 balance: 900
Account 2 balance: 1100
# 事务隔离级别
JDBC定义了五种事务隔离级别,用于控制并发事务之间的数据可见性。YashanDB支持READ_COMMITTED和SERIALIZABLE隔离级别。
在事务开始前,使用setTransactionIsolation()设置隔离级别,使用getTransactionIsolation()获取当前隔离级别。
以下示例依次设置并读取YashanDB支持的两种隔离级别,用于确认连接上的隔离级别配置已经生效。
package quickstart;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;
public class IsolationLevelsExample {
public static void main(String[] args) throws Exception {
String url = "jdbc:yasdb://192.168.1.2:1688/yasdb";
Properties properties = new Properties();
properties.setProperty("user", "username");
properties.setProperty("password", "passwd");
Class.forName("com.yashandb.jdbc.Driver");
try (Connection connection =
DriverManager.getConnection(url, properties)) {
connection.setTransactionIsolation(
Connection.TRANSACTION_READ_COMMITTED);
System.out.println(
"Current isolation level: "
+ isolationLevelName(
connection.getTransactionIsolation()));
connection.setTransactionIsolation(
Connection.TRANSACTION_SERIALIZABLE);
System.out.println(
"Current isolation level: "
+ isolationLevelName(
connection.getTransactionIsolation()));
}
}
private static String isolationLevelName(int level) {
if (level == Connection.TRANSACTION_READ_COMMITTED) {
return "READ_COMMITTED";
}
if (level == Connection.TRANSACTION_SERIALIZABLE) {
return "SERIALIZABLE";
}
return "UNKNOWN(" + level + ")";
}
}
正常情况下,程序输出如下:
Current isolation level: READ_COMMITTED
Current isolation level: SERIALIZABLE
# 保存点
保存点用于在事务中标记中间位置。调用rollback(Savepoint)只撤销该保存点之后的操作,保存点之前的操作仍可以继续提交。保存点只能在关闭自动提交后使用。
以下示例先将余额减少100并创建保存点,再将余额减少200。回滚到保存点后,第二次更新被撤销,第一次更新仍然保留并最终提交。
package quickstart;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.Properties;
public class SavepointExample {
private static final String URL =
"jdbc:yasdb://192.168.1.2:1688/yasdb";
private static final String TABLE = "jdbc_transaction_savepoint";
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.setProperty("user", "username");
properties.setProperty("password", "passwd");
Class.forName("com.yashandb.jdbc.Driver");
prepareTable(properties);
try (Connection connection =
DriverManager.getConnection(URL, properties)) {
connection.setAutoCommit(false);
try (Statement statement = connection.createStatement()) {
statement.executeUpdate(
"UPDATE " + TABLE
+ " SET balance = balance - 100 "
+ "WHERE account_id = 1");
Savepoint savepoint =
connection.setSavepoint("BEFORE_SECOND_UPDATE");
statement.executeUpdate(
"UPDATE " + TABLE
+ " SET balance = balance - 200 "
+ "WHERE account_id = 1");
System.out.println(
"Balance before rollback: "
+ queryBalance(connection));
// 只撤销保存点之后的第二次更新。
connection.rollback(savepoint);
System.out.println(
"Balance after rollback: "
+ queryBalance(connection));
connection.commit();
} catch (Exception exception) {
connection.rollback();
throw exception;
}
} finally {
dropTable(properties);
}
}
private static BigDecimal queryBalance(Connection connection)
throws Exception {
try (Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(
"SELECT balance FROM " + TABLE
+ " WHERE account_id = 1")) {
resultSet.next();
return resultSet.getBigDecimal(1);
}
}
private static void prepareTable(Properties properties)
throws Exception {
try (Connection connection =
DriverManager.getConnection(URL, properties);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS " + TABLE);
statement.execute(
"CREATE TABLE " + TABLE
+ " (account_id INTEGER PRIMARY KEY, "
+ "balance DECIMAL(10, 2))");
statement.executeUpdate(
"INSERT INTO " + TABLE + " VALUES (1, 1000.00)");
}
}
private static void dropTable(Properties properties)
throws Exception {
try (Connection connection =
DriverManager.getConnection(URL, properties);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS " + TABLE);
}
}
}
正常情况下,程序输出如下:
Balance before rollback: 700
Balance after rollback: 900
# 分布式事务
YashanDB JDBC通过YasXADataSource提供XA分布式事务支持。应用程序从XAConnection获取XAResource,并通过start()、end()、prepare()和commit()控制XA事务分支。
以下示例创建一个XA事务分支,在分支中修改账户余额,然后执行准备和提交。执行示例的数据库需要启用XA事务能力。由于XAConnection未实现AutoCloseable,示例在finally代码块中显式关闭该对象。
package quickstart;
import com.yashandb.xa.YasXADataSource;
import com.yashandb.xa.YasXid;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import javax.sql.XAConnection;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
public class XATransactionExample {
private static final String URL =
"jdbc:yasdb://192.168.1.2:1688/yasdb";
private static final String TABLE = "jdbc_transaction_xa";
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.setProperty("user", "username");
properties.setProperty("password", "passwd");
Class.forName("com.yashandb.jdbc.Driver");
prepareTable(properties);
YasXADataSource dataSource = new YasXADataSource();
dataSource.setURL(URL);
dataSource.setUser(properties.getProperty("user"));
dataSource.setPassword(properties.getProperty("password"));
XAConnection xaConnection = dataSource.getXAConnection();
try {
try (Connection connection =
xaConnection.getConnection()) {
XAResource xaResource =
xaConnection.getXAResource();
Xid xid = new YasXid(
1,
"transaction-1".getBytes(
StandardCharsets.US_ASCII),
"branch-1".getBytes(
StandardCharsets.US_ASCII));
// XA分支中不能使用本地自动提交。
connection.setAutoCommit(false);
xaResource.start(xid, XAResource.TMNOFLAGS);
try (Statement statement =
connection.createStatement()) {
statement.executeUpdate(
"UPDATE " + TABLE
+ " SET balance = balance - 100 "
+ "WHERE account_id = 1");
}
xaResource.end(xid, XAResource.TMSUCCESS);
int result = xaResource.prepare(xid);
if (result == XAResource.XA_OK) {
xaResource.commit(xid, false);
System.out.println("XA transaction committed");
}
}
} finally {
xaConnection.close();
}
try (Connection connection =
DriverManager.getConnection(URL, properties);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(
"SELECT balance FROM " + TABLE
+ " WHERE account_id = 1")) {
resultSet.next();
System.out.println(
"Account balance: "
+ resultSet.getBigDecimal(1));
} finally {
dropTable(properties);
}
}
private static void prepareTable(Properties properties)
throws Exception {
try (Connection connection =
DriverManager.getConnection(URL, properties);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS " + TABLE);
statement.execute(
"CREATE TABLE " + TABLE
+ " (account_id INTEGER PRIMARY KEY, "
+ "balance DECIMAL(10, 2))");
statement.executeUpdate(
"INSERT INTO " + TABLE + " VALUES (1, 1000.00)");
}
}
private static void dropTable(Properties properties)
throws Exception {
try (Connection connection =
DriverManager.getConnection(URL, properties);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS " + TABLE);
}
}
}
正常情况下,程序输出如下:
XA transaction committed
Account balance: 900

