Skip to content

乐观锁是什么,给出实现思路

乐观锁概述

  • 定义
  • 乐观锁(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
}
  1. 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 存储 nameversion
  • 更新流程:
@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 源码),请告诉我,我可以进一步优化!