乐观锁:并发控制的智慧之道
什么是乐观锁?
乐观锁(Optimistic Locking)是一种并发控制机制,其核心思想是"假设冲突很少发生"。与悲观锁(Pessimistic Locking)不同,悲观锁在访问共享资源前会先加锁,阻止其他线程同时访问;而乐观锁则假设数据在大多数情况下不会发生冲突,因此在读取数据时不加锁,仅在更新数据时检查数据是否被其他线程修改过。
乐观锁的实现原理
乐观锁通常通过版本号(Version Number)或时间戳(Timestamp)来实现:
- 版本号机制:为每个数据记录添加一个版本号字段
- 读取数据:读取数据时同时读取版本号
- 更新数据:更新数据前检查版本号是否发生变化
- 验证冲突:如果版本号未变化则更新数据并递增版本号,否则更新失败
核心实现逻辑
sql
-- 读取数据
SELECT id, name, version FROM table WHERE id = 1;
-- 更新数据(带版本号验证)
UPDATE table
SET name = 'new_value', version = version + 1
WHERE id = 1 AND version = current_version;
如果UPDATE语句影响的行数为0,说明数据在读取后被其他线程修改过,需要重新获取数据并重试操作。
乐观锁的优势
- 高并发性能:读取操作无需加锁,适合读多写少的场景
- 减少锁竞争:避免了线程阻塞和上下文切换的开销
- 资源消耗低:不占用系统锁资源
- 可扩展性好:适合分布式系统
乐观锁的劣势
- 冲突重试开销:高并发写入时可能需要多次重试
- 不适合高冲突场景:写入频繁时性能可能不如悲观锁
- 业务逻辑复杂:需要处理重试逻辑和异常情况
实际应用场景
乐观锁广泛应用于以下场景:
- 电商系统:库存管理、订单状态更新
- 银行系统:账户余额更新
- 内容管理系统:文档编辑、版本控制
- 酒店预订系统:房间状态更新
乐观锁触发示例:酒店预订系统场景
让我通过一个完整的示例来说明乐观锁是如何在实际场景中被触发的。
场景设定
假设我们有一个酒店预订系统,两个前台工作人员Alice和Bob同时处理同一个预订记录:
- 预订ID: 1001
- 当前版本号: 1
- 预订状态: 待确认
事件时间线
时间点T1 - Alice操作
Alice打开预订记录,准备更新联系人信息:
javascript
// Alice读取预订信息
const reservation = await getRoomReservationById(1001);
// 返回数据:
// {
// id: 1001,
// contactName: "张三",
// phoneNumber: "13800138000",
// version: 1,
// status: "待确认"
// }
时间点T2 - Bob操作
几乎同时,Bob也打开了同一份预订记录:
javascript
// Bob读取预订信息
const reservation = await getRoomReservationById(1001);
// 返回相同的数据:
// {
// id: 1001,
// contactName: "张三",
// phoneNumber: "13800138000",
// version: 1,
// status: "待确认"
// }
时间点T3 - Alice提交更新
Alice首先完成修改并提交:
javascript
// Alice尝试更新预订信息
const updateData = {
id: 1001,
contactName: "张三丰", // 修改联系人姓名
phoneNumber: "13800138000",
version: 1, // Alice读取时的版本号
status: "待确认"
};
// 发送到后端的更新请求
await updateRoomReservation(updateData);
后端执行SQL:
sql
UPDATE room_reservation
SET contact_name = '张三丰', version = version + 1
WHERE id = 1001 AND version = 1;
结果: 影响1行记录,更新成功,数据库中版本号变为2。
时间点T4 - Bob提交更新(触发乐观锁)
Bob稍后提交他的修改(更新电话号码):
javascript
// Bob尝试更新预订信息
const updateData = {
id: 1001,
contactName: "张三", // Bob未修改联系人姓名
phoneNumber: "13900139000", // 修改电话号码
version: 1, // Bob读取时的版本号(仍然是1)
status: "待确认"
};
// 发送到后端的更新请求
await updateRoomReservation(updateData);
后端执行SQL:
sql
UPDATE room_reservation
SET phone_number = '13900139000', version = version + 1
WHERE id = 1001 AND version = 1; -- 注意:这里的version是1
乐观锁触发!
关键点: 此时数据库中预订ID为1001的记录的版本号已经是2(被Alice的更新操作递增了),但Bob的更新请求仍然使用版本号1作为条件。
执行结果:
- SQL影响0行记录(因为没有version=1的记录了)
- 更新失败,触发乐观锁机制
- 后端返回错误信息
前端处理
javascript
try {
await updateRoomReservation(updateData);
console.log("更新成功");
} catch (error) {
if (error.message.includes("乐观锁")) {
// 乐观锁冲突处理
alert("检测到数据冲突,请刷新后重试");
// 重新加载最新数据
const latestReservation = await getRoomReservationById(1001);
// 提示用户数据已被其他用户修改
console.log("最新数据:", latestReservation);
}
}
完整的乐观锁处理流程
javascript
// 带重试机制的更新函数
async function updateReservationWithRetry(reservationData, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
// 尝试更新
await updateRoomReservation(reservationData);
console.log("更新成功");
return true;
} catch (error) {
if (error.message.includes("乐观锁") || error.response?.data?.code === 500) {
retries++;
if (retries >= maxRetries) {
alert("更新失败次数过多,请稍后重试");
return false;
}
console.log(`乐观锁冲突,第${retries}次重试`);
// 重新获取最新数据
const latestData = await getRoomReservationById(reservationData.id);
// 合并用户的新修改到最新数据中
reservationData.version = latestData.data.version; // 使用最新版本号
// 这里可能需要合并用户的修改到最新数据中
// 具体实现取决于业务逻辑
console.log(`重试第${retries}次,使用版本号: ${reservationData.version}`);
} else {
// 非乐观锁错误,直接抛出
throw error;
}
}
}
}
总结
乐观锁的触发是并发控制的正常表现:
- 触发条件: 多个用户同时修改同一数据记录
- 检测机制: 通过版本号验证数据是否被修改
- 处理方式: 失败后重试或提示用户
- 业务价值: 保证数据一致性,避免脏写
这种机制确保了在高并发场景下数据的完整性,同时避免了传统锁机制可能带来的性能问题。