1. 锁粒度选择
- 行级锁:
- 设计思路:在资金转移操作时,对于涉及到的具体账户记录使用行级锁。例如,从账户A向账户B转账,锁定账户A和账户B对应的行记录。
- 原因:行级锁可以最大程度减少锁的范围,在并发环境下,其他操作可以同时访问未被锁定的行,提高并发性能。因为金融业务场景对并发操作要求较高,尽量减少锁的粒度能避免不必要的锁争用,提升系统的并发处理能力。
- 表级锁:
- 设计思路:在一些特殊情况下,如批量处理大量账户数据或者需要对整个账户表进行一致性维护操作时,使用表级锁。例如,在进行账户数据的备份、重建索引等操作时。
- 原因:表级锁的加锁和释放锁的开销小,对于涉及全表的操作,使用表级锁可以保证数据的完整性和一致性,避免在操作过程中其他并发操作对表结构或数据造成干扰。
2. 事务隔离级别选择
- 可重复读(Repeatable Read):
- 设计思路:将事务隔离级别设置为可重复读。在执行资金转移、余额查询等操作时,开启事务,在事务内进行相关操作。
- 原因:可重复读级别可以避免脏读和不可重复读问题。在金融业务中,脏读会导致读取到未提交的脏数据,这是绝对不允许的;不可重复读会使得在同一事务内多次读取同一数据出现不一致的情况,影响业务逻辑判断。可重复读级别通过MVCC(多版本并发控制)机制,在保证数据一致性的同时,一定程度上提高了并发性能,适合金融业务场景对数据一致性和并发性能的平衡需求。虽然可重复读不能完全避免幻读,但在大多数金融业务场景中,幻读对业务的影响相对较小,通过合理的业务逻辑设计(如在查询时使用范围锁等方式)可以在一定程度上解决幻读问题。
3. 整体方案示例
Connection conn = DriverManager.getConnection(url, username, password);
try {
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(false);
// 获取账户A和账户B的当前余额
String selectSql = "SELECT balance FROM accounts WHERE account_id =?";
PreparedStatement selectStmt = conn.prepareStatement(selectSql);
selectStmt.setInt(1, accountAId);
ResultSet rs1 = selectStmt.executeQuery();
rs1.next();
double balanceA = rs1.getDouble("balance");
selectStmt.setInt(1, accountBId);
ResultSet rs2 = selectStmt.executeQuery();
rs2.next();
double balanceB = rs2.getDouble("balance");
// 进行资金转移逻辑
double transferAmount = 100;
balanceA -= transferAmount;
balanceB += transferAmount;
// 更新账户A和账户B的余额,使用行级锁
String updateSql = "UPDATE accounts SET balance =? WHERE account_id =? FOR UPDATE";
PreparedStatement updateStmt = conn.prepareStatement(updateSql);
updateStmt.setDouble(1, balanceA);
updateStmt.setInt(2, accountAId);
updateStmt.executeUpdate();
updateStmt.setDouble(1, balanceB);
updateStmt.setInt(2, accountBId);
updateStmt.executeUpdate();
conn.commit();
} catch (SQLException e) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
- **设计思路**:在事务内,首先查询账户A和账户B的余额,由于事务隔离级别为可重复读,在事务未提交前,其他事务对这两个账户余额的修改不可见。然后进行资金转移的计算,最后更新账户余额,并使用`FOR UPDATE`语句获取行级锁,确保在更新过程中其他事务无法修改这两行数据,保证数据一致性。
Connection conn = DriverManager.getConnection(url, username, password);
try {
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(true);
String selectSql = "SELECT balance FROM accounts WHERE account_id =?";
PreparedStatement selectStmt = conn.prepareStatement(selectSql);
selectStmt.setInt(1, accountId);
ResultSet rs = selectStmt.executeQuery();
if (rs.next()) {
double balance = rs.getDouble("balance");
System.out.println("账户余额为: " + balance);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
- **设计思路**:在可重复读隔离级别下,查询操作不会被其他未提交事务的修改影响,能够获取到一致性的余额数据。并且由于查询操作通常不需要修改数据,所以可以设置自动提交,提高查询的并发处理效率。