乐观锁
@Version 注解使用指南
概述
乐观锁通过版本号机制解决并发更新问题,在更新时检查版本号是否匹配,防止数据覆盖。
基本用法
实体定义
@Entity
@Table(name = "user")
public class User {
@Id
private Long id;
private String name;
private Integer age;
// 乐观锁版本号
@Version
private Integer version;
}
数据库结构
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
age INT,
version INT DEFAULT 0 -- 版本号,初始值为 0
);
使用效果
// 查询用户(version = 0)
User user = userDao.findById(1L);
// 更新用户
user.setName("新名字");
userDao.updateById(user);
// 实际执行的 SQL
UPDATE user SET name = '新名字', age = 25, version = 1
WHERE id = 1 AND version = 0;
// 如果 version 不匹配,更新行数为 0,表示并发冲突
工作原理
┌─────────────────────────────────────────────────────────────────┐
│ 乐观锁工作流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 查询数据 │
│ SELECT * FROM user WHERE id = 1 │
│ 返回: { id: 1, name: '张三', version: 0 } │
│ │
│ 2. 更新数据(自动版本号 +1) │
│ UPDATE user SET name = '李四', version = 1 │
│ WHERE id = 1 AND version = 0 │
│ │
│ 3. 检查更新结果 │
│ - 更新成功:返回行数 = 1 │
│ - 版本冲突:返回行数 = 0(version 已被其他事务修改) │
│ │
└─────────────────────────────────────────────────────────────────┘
@Version 属性
@Version
private Integer version;
该注解标记字段为乐观锁版本号字段。
版本号类型
Integer 类型(推荐)
@Version
private Integer version; // 初始值 0,每次更新 +1
Long 类型
@Version
private Long version; // 初始值 0,每次更新 +1
并发冲突处理
检测冲突
User user = userDao.findById(1L);
user.setName("新名字");
int rows = userDao.updateById(user);
if (rows == 0) {
// 版本冲突,更新失败
throw new OptimisticLockException("数据已被其他用户修改,请刷新后重试");
}
重试机制
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
public void updateUserName(Long id, String newName) {
User user = userDao.findById(id);
user.setName(newName);
int rows = userDao.updateById(user);
if (rows == 0) {
throw new OptimisticLockException("乐观锁冲突");
}
}
}
完整示例
// 用户实体
@Entity
@Table(name = "user")
public class User {
@Id
private Long id;
private String name;
private Integer age;
@Version
private Integer version;
}
// DAO 接口
@Repository
public interface UserDao extends SimpleDao<User, UserQuery, Long> {
}
// Service 使用
@Service
public class UserService {
@Autowired
private UserDao userDao;
public boolean updateUser(Long id, String newName, Integer newAge) {
User user = userDao.findById(id);
if (user == null) {
return false;
}
user.setName(newName);
user.setAge(newAge);
int rows = userDao.updateById(user);
return rows > 0; // 返回是否更新成功
}
public void updateWithRetry(Long id, String newName) {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
User user = userDao.findById(id);
user.setName(newName);
int rows = userDao.updateById(user);
if (rows > 0) {
return; // 更新成功
}
// 版本冲突,重试
}
throw new RuntimeException("更新失败,请稍后重试");
}
}
与其他功能配合
与逻辑删除配合
@Entity
@Table(name = "user")
public class User {
@Id
private Long id;
private String name;
@LogicDelete
private Integer status;
@Version
private Integer version;
}
// 删除时也会检查版本号
// UPDATE user SET status = 1, version = version + 1
// WHERE id = 1 AND version = 0 AND status = 0
与字段值生成配合
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(IdValueProcessor.class)
private Long id;
private String name;
@Version
private Integer version;
}
注意事项
-
每个实体只能有一个版本号字段:多个
@Version注解会导致异常 -
版本号初始值:建议初始值为 0 或 null(自动填充为 0)
-
更新返回值检查:务必检查更新返回的行数,判断是否冲突
-
批量更新不适用:乐观锁不适用于批量更新场景
-
new 对象更新:如果 new 一个新对象设置 id 和 version 进行更新,确保 version 正确