40. 什么是悲观锁和乐观锁?如何在MySQL和Java中实现它们?
大约 3 分钟
悲观锁和乐观锁是两种常见的并发控制机制,用于解决多个事务并发访问同一资源时的数据一致性问题。
1. 悲观锁
悲观锁假设在并发环境下,数据的竞争是频繁且激烈的,因此在一个事务读取数据之前,会先锁定资源,以防止其他事务修改该资源。这种锁通常依赖数据库的锁机制,确保在持有锁期间,其他事务无法修改或读取被锁定的数据。
在MySQL中实现悲观锁
MySQL的悲观锁通常通过SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
来实现。
SELECT ... FOR UPDATE
:对读取的数据行加排他锁(写锁),其他事务不能对该数据进行任何操作,直到锁释放。SELECT ... LOCK IN SHARE MODE
:对读取的数据行加共享锁(读锁),其他事务可以读但不能写,直到锁释放。
示例:
-- 启动事务
START TRANSACTION;
-- 对某行数据加排他锁,其他事务无法更新此行数据
SELECT * FROM products WHERE id = 1 FOR UPDATE;
-- 在此处执行更新操作
UPDATE products SET quantity = quantity - 1 WHERE id = 1;
-- 提交事务
COMMIT;
在Java中实现悲观锁
在Java中,使用JDBC配合MySQL的悲观锁语句可以实现悲观锁。
示例:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class PessimisticLockExample {
public static void main(String[] args) {
Connection connection = null;
try {
connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/your_database",
"username",
"password");
connection.setAutoCommit(false);
// 执行SELECT ... FOR UPDATE获取锁
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM products WHERE id = ? FOR UPDATE");
stmt.setInt(1, 1);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
int quantity = rs.getInt("quantity");
// 更新操作
PreparedStatement updateStmt = connection.prepareStatement("UPDATE products SET quantity = ? WHERE id = ?");
updateStmt.setInt(1, quantity - 1);
updateStmt.setInt(2, 1);
updateStmt.executeUpdate();
}
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
try {
if (connection != null) {
connection.rollback();
}
} catch (SQLException rollbackEx) {
rollbackEx.printStackTrace();
}
} finally {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException closeEx) {
closeEx.printStackTrace();
}
}
}
}
2. 乐观锁
乐观锁假设数据冲突的可能性较低,因此不会在操作前锁定资源。它通常在更新数据时进行版本检查,只有在没有其他事务修改数据的情况下,才能成功更新。这种机制通常依赖于版本号或时间戳。
在MySQL中实现乐观锁
乐观锁可以通过一个额外的“版本号”字段或“时间戳”字段来实现。在更新时,检查当前版本号是否与读取时的一致,只有一致时才执行更新。
示例:
-- 假设表中有一个version字段
UPDATE products
SET quantity = quantity - 1, version = version + 1
WHERE id = 1 AND version = 1;
如果更新的记录数为0,则说明数据已经被其他事务修改,需要重新读取并尝试更新。
在Java中实现乐观锁
在Java中,可以在业务逻辑中通过版本号字段实现乐观锁。
示例:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class OptimisticLockExample {
public static void main(String[] args) {
Connection connection = null;
try {
connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/your_database",
"username",
"password");
connection.setAutoCommit(false);
// 查询当前version
PreparedStatement selectStmt = connection.prepareStatement("SELECT quantity, version FROM products WHERE id = ?");
selectStmt.setInt(1, 1);
ResultSet rs = selectStmt.executeQuery();
if (rs.next()) {
int quantity = rs.getInt("quantity");
int version = rs.getInt("version");
// 尝试更新数据
PreparedStatement updateStmt = connection.prepareStatement(
"UPDATE products SET quantity = ?, version = version + 1 WHERE id = ? AND version = ?");
updateStmt.setInt(1, quantity - 1);
updateStmt.setInt(2, 1);
updateStmt.setInt(3, version);
int rowsAffected = updateStmt.executeUpdate();
if (rowsAffected == 0) {
// 更新失败,可能是版本号不匹配,处理并发问题
System.out.println("更新失败,数据已被其他事务修改");
} else {
connection.commit();
}
}
} catch (SQLException e) {
e.printStackTrace();
try {
if (connection != null) {
connection.rollback();
}
} catch (SQLException rollbackEx) {
rollbackEx.printStackTrace();
}
} finally {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException closeEx) {
closeEx.printStackTrace();
}
}
}
}
总结
- 悲观锁:通过锁定资源,防止并发修改,适合高竞争环境,但可能导致性能瓶颈。
- 乐观锁:通过版本号或时间戳检查更新的有效性,适合低竞争环境,减少锁的开销。
在实际开发中,应根据具体业务需求和数据竞争程度选择合适的锁机制。