乐观锁是什么,给出实现思路
乐观锁概述
- 定义:
- 乐观锁(Optimistic Lock)是一种并发控制机制,假设数据在并发操作时通常不会发生冲突,仅在提交更新时检查数据是否被其他事务修改。它适用于 读多写少 的场景,相比悲观锁(锁定资源)性能更高。
- 核心思想:
- 不提前锁定资源,操作时假设数据未被修改,提交时通过版本号或其他机制验证冲突。
- 特点:
- 非阻塞,效率高。
- 冲突时需重试或回滚。
- 常用于数据库、分布式系统。
核心点
- 乐观锁通过 版本号 或 时间戳 检测冲突,适合高并发、低冲突场景。
1. 乐观锁原理
- 工作流程:
- 读取数据:获取数据及其版本号(或时间戳)。
- 执行操作:基于读取的数据进行计算或修改。
- 提交更新:检查版本号是否一致,若一致则更新数据并更新版本号;若不一致,说明数据被修改,操作失败,需重试。
- 冲突检测:
- 版本号:记录数据版本(如整数
version
),每次更新递增。 - 时间戳:记录最后修改时间,比较时间是否变化。
- 字段比较:直接比较所有字段值(较少使用)。
- 示例(版本号):
- 初始:
id=1, name=Alice, version=1
- 事务 A 读取:
version=1
- 事务 B 更新:
name=Bob, version=2
- 事务 A 提交:发现
version=1
不匹配,失败,重试。
2. 实现思路
乐观锁的实现主要依赖数据库或程序逻辑,以下是常见方式及思路:
(1) 数据库实现(基于版本号)
- 思路:
- 数据库表添加
version
列(整数)。 - 读取数据时获取
version
。 - 更新时在 SQL 中检查
version
是否一致。 - 步骤:
- 表结构:
sql CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(50), version INT DEFAULT 1 );
- 读取数据:
sql SELECT id, name, version FROM users WHERE id = 1;
- 假设得到:
name=Alice, version=1
- 假设得到:
- 更新操作:
- 修改
name
,构造 SQL 检查版本:sql UPDATE users SET name = 'Bob', version = version + 1 WHERE id = 1 AND version = 1;
- 修改
- 检查结果:
- 如果
affected_rows = 1
,更新成功。 - 如果
affected_rows = 0
,说明版本不匹配,需重试。
- 如果
- 重试逻辑:
- 重新读取最新数据,重试更新。
- 代码示例(Java + MyBatis):
@Mapper
public interface UserMapper {
@Select("SELECT id, name, version FROM users WHERE id = #{id}")
User selectById(Long id);
@Update("UPDATE users SET name = #{name}, version = version + 1 WHERE id = #{id} AND version = #{version}")
int update(User user);
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void updateUser(Long id, String newName) {
int retries = 3;
while (retries-- > 0) {
User user = userMapper.selectById(id); // 读取
user.setName(newName);
int rows = userMapper.update(user); // 更新
if (rows > 0) {
return; // 成功
}
// 失败重试
Thread.sleep(100); // 简单延时
}
throw new RuntimeException("Update failed after retries");
}
}
(2) 数据库实现(基于时间戳)
- 思路:
- 表添加
last_modified
列(TIMESTAMP
)。 - 更新时检查
last_modified
是否一致。 - 表结构:
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- 更新 SQL:
UPDATE users
SET name = 'Bob', last_modified = CURRENT_TIMESTAMP
WHERE id = 1 AND last_modified = '2023-10-01 10:00:00';
- 优缺点:
- 优点:时间戳直观,数据库自动维护。
- 缺点:时间戳精度可能导致冲突(毫秒级重复)。
(3) 程序逻辑实现(CAS 方式)
- 思路:
- 使用 Compare And Swap(CAS) 思想,程序维护版本号或状态。
- 适合非数据库场景(如内存对象、分布式系统)。
- 步骤:
- 定义对象带版本号:
public class User {
private Long id;
private String name;
private int version;
// getters and setters
}
- CAS 更新:
public class UserManager {
private Map<Long, User> userCache = new ConcurrentHashMap<>();
public boolean updateUser(Long id, String newName) {
User user = userCache.get(id);
int currentVersion = user.getVersion();
User updatedUser = new User(id, newName, currentVersion + 1);
// CAS 操作
if (userCache.replace(id, user, updatedUser)) {
return true; // 更新成功
}
return false; // 失败,需重试
}
}
- 适用:
- 分布式缓存(如 Redis)。
- 内存对象管理。
(4) 分布式系统实现(基于 Redis)
- 思路:
- 使用 Redis 的
WATCH
+MULTI
实现乐观锁。 - 步骤:
- 存储数据:
- Redis 键:
user:1
存储name
和version
。
- Redis 键:
- 更新流程:
@Autowired
private StringRedisTemplate redisTemplate;
public void updateUser(String userId, String newName) {
while (true) {
redisTemplate.watch("user:" + userId);
String version = redisTemplate.opsForHash().get("user:" + userId, "version").toString();
String oldName = redisTemplate.opsForHash().get("user:" + userId, "name").toString();
// 开始事务
redisTemplate.multi();
redisTemplate.opsForHash().put("user:" + userId, "name", newName);
redisTemplate.opsForHash().put("user:" + userId, "version", String.valueOf(Integer.parseInt(version) + 1));
// 提交事务
List<Object> results = redisTemplate.exec();
if (results != null) {
return; // 成功
}
// 失败重试
redisTemplate.unwatch();
}
}
- 原理:
WATCH
监控键,事务提交时检查是否被修改。
3. 乐观锁优缺点
优点
- 高并发:
- 无锁操作,适合读多写少。
- 性能好:
- 避免悲观锁的阻塞开销。
- 简单实现:
- 仅需版本号或时间戳。
缺点
- 冲突重试:
- 高冲突场景需多次重试,增加延迟。
- ABA 问题:
- 版本号可能掩盖中间修改(可用时间戳或更复杂机制)。
- 适用有限:
- 不适合写频繁或长事务。
4. 使用场景
- 数据库更新:
- 商品库存扣减、用户信息修改。
- 分布式系统:
- 分布式锁、缓存更新(如 Redis、ZooKeeper)。
- 高并发:
- 秒杀系统、订单处理。
- 乐观锁示例:
- MySQL 的
UPDATE ... WHERE version = ?
。 - Spring Data JPA 的
@Version
。
5. 面试角度
- 问“定义”:
- 提无锁、版本号、高并发。
- 问“实现”:
- 提数据库 SQL 或 Redis CAS。
- 问“场景”:
- 提库存、秒杀。
- 问“优劣”:
- 对比悲观锁,提重试问题。
6. 总结
乐观锁通过版本号或时间戳检测冲突,假设无竞争,提交时验证,适合读多写少场景。实现方式包括数据库的 version
字段、Redis 的 WATCH
或程序的 CAS。核心是高效无锁,但需处理重试和 ABA 问题。面试可提 SQL 示例或分布式场景,清晰展示理解。
如果您想深入某实现(如 JPA @Version
或 Redis 源码),请告诉我,我可以进一步优化!