面试题答案
一键面试1. 事务隔离级别特点及可能产生的问题
- 读未提交(Read Uncommitted):
- 特点:一个事务可以读取另一个未提交事务的数据。这是隔离级别最低的一种。
- 可能问题:脏读(Dirty Read),即一个事务读取到了另一个未提交事务修改的数据,如果未提交事务回滚,那么读取到的数据就是无效的。
- 读已提交(Read Committed):
- 特点:一个事务只能读取已经提交的事务的数据。在大多数数据库系统中,这是默认的隔离级别。
- 可能问题:不可重复读(Non - Repeatable Read),在同一个事务内,两次相同的查询可能会得到不同的结果,因为在两次查询之间,另一个已提交的事务可能修改了数据。
- 可重复读(Repeatable Read):
- 特点:在一个事务内,多次读取同一数据时,得到的结果是一致的,即使在这期间其他事务对该数据进行了修改并提交。
- 可能问题:幻读(Phantom Read),当一个事务按照某个条件多次查询返回的记录数相同,但是每次查询数据时,却发现数据行发生了变化,好像出现了“幻影”。这是因为其他事务插入或删除了符合查询条件的记录。
- 串行化(Serializable):
- 特点:最高的隔离级别,事务按照顺序依次执行,就像单线程环境一样。
- 可能问题:并发性能极低,因为所有事务都要排队执行,可能会导致大量的等待。
2. 结合MySQL锁机制选择事务隔离级别与运用锁机制
- 事务隔离级别选择:
- 读未提交:一般不建议在实际项目中使用,因为脏读问题严重影响数据一致性。
- 读已提交:适用于大多数OLTP(联机事务处理)场景,能保证读一致性且并发性能相对较好。例如电商订单查询场景,订单状态可能随时被其他事务修改并提交,允许查询到最新已提交状态。
- 可重复读:MySQL默认隔离级别,适合对数据一致性要求较高,同时允许一定并发的场景,如银行转账业务,在一个转账事务内,多次查询账户余额需保持一致。
- 串行化:适用于对数据一致性要求极高,并发量极低的场景,如涉及金融核心数据的修改等。
- 锁机制运用:
- 共享锁(S锁):也叫读锁,多个事务可以同时获取共享锁读取数据,但不允许其他事务获取排他锁修改数据。用于读多写少的场景,如商品详情页浏览。
- 排他锁(X锁):也叫写锁,一个事务获取排他锁后,其他事务不能再获取任何锁。用于写操作,确保数据修改的原子性,如商品库存扣减。
- 意向锁:分为意向共享锁(IS锁)和意向排他锁(IX锁),用于表级锁和行级锁的协调,提高锁获取效率。例如,事务先获取表的意向锁,再获取行锁。
3. 复杂业务场景代码示例及分析
假设一个电商场景,用户购买商品时,需要检查库存、扣减库存、生成订单并更新用户积分。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class EcommerceTransaction {
private static final String URL = "jdbc:mysql://localhost:3306/ecommerce";
private static final String USER = "root";
private static final String PASSWORD = "password";
public static void main(String[] args) {
Connection conn = null;
try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 设置事务隔离级别为可重复读
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// 开启事务
conn.setAutoCommit(false);
// 检查库存
String checkStockSql = "SELECT stock FROM products WHERE product_id =?";
PreparedStatement checkStockStmt = conn.prepareStatement(checkStockSql);
checkStockStmt.setInt(1, 1); // 假设商品ID为1
checkStockStmt.executeQuery();
int stock = 0;
if (checkStockStmt.getResultSet().next()) {
stock = checkStockStmt.getResultSet().getInt("stock");
}
if (stock < 1) {
throw new RuntimeException("库存不足");
}
// 扣减库存
String reduceStockSql = "UPDATE products SET stock = stock - 1 WHERE product_id =?";
PreparedStatement reduceStockStmt = conn.prepareStatement(reduceStockSql);
reduceStockStmt.setInt(1, 1);
reduceStockStmt.executeUpdate();
// 生成订单
String createOrderSql = "INSERT INTO orders (user_id, product_id) VALUES (?,?)";
PreparedStatement createOrderStmt = conn.prepareStatement(createOrderSql);
createOrderStmt.setInt(1, 100); // 假设用户ID为100
createOrderStmt.setInt(2, 1);
createOrderStmt.executeUpdate();
// 更新用户积分
String updatePointsSql = "UPDATE users SET points = points + 10 WHERE user_id =?";
PreparedStatement updatePointsStmt = conn.prepareStatement(updatePointsSql);
updatePointsStmt.setInt(1, 100);
updatePointsStmt.executeUpdate();
// 提交事务
conn.commit();
} catch (SQLException | ClassNotFoundException e) {
// 回滚事务
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 关闭连接
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
分析:
- 事务隔离级别选择:使用可重复读隔离级别,确保在整个购买事务内,查询库存数据不会因为其他事务的修改而改变,避免不可重复读问题。
- 锁机制:在执行更新操作(扣减库存、生成订单、更新积分)时,MySQL会自动获取排他锁,确保数据修改的原子性和一致性。在查询库存时,虽然未显式加锁,但由于可重复读隔离级别,会使用MVCC(多版本并发控制)机制来保证数据一致性,避免幻读问题。同时,数据库内部的意向锁机制保证了行级锁和表级锁的有效协调,提高并发效率。