前言
乐观锁是一种并发控制机制,它假设在大多数情况下不会发生冲突,因此在事务执行过程中不加锁。只有在提交时才会检查数据是否被其他事务修改过。如果数据在此期间被修改了,则当前事务会被回滚或者需要重新执行。乐观锁的主要用途和优势包括:
-
提高读取性能:由于乐观锁不会锁定资源,因此在读取数据时没有阻塞,可以极大地提高读取操作的性能,特别适合读多写少的应用场景。
-
减少死锁的可能性:因为乐观锁不使用实际的数据库锁,所以避免了传统悲观锁可能导致的死锁问题。
-
简化代码实现:乐观锁的实现通常比悲观锁简单,特别是在分布式系统中,因为它不需要复杂的锁管理逻辑。
-
支持高并发场景:在许多用户同时访问相同数据的情况下,乐观锁能够更好地处理并发请求,减少了等待时间,提高了系统的吞吐量。
-
用户体验改善:在Web应用等环境中,乐观锁可以减少用户界面的等待时间,提供更流畅的用户体验。
-
适用于长事务:对于那些涉及长时间运行的业务逻辑,采用乐观锁可以避免长时间持有锁导致的资源浪费。
-
支持离线或异步更新:乐观锁允许客户端在离线状态下进行数据修改,并在上线后合并这些更改,这在移动应用开发中尤其有用。
适用场景
- 读多写少:当系统中读取操作远多于写入操作时。
- 低冲突率:当数据项被多个事务同时修改的概率较低时。
- 分布式系统:在分布式系统中,乐观锁可以简化跨节点的数据一致性问题。
- 对最终一致性要求不高:如果应用程序可以接受短时间内的数据不一致,直到下一个成功的更新操作完成。
注意事项
- 高冲突率下的效率降低:如果数据项频繁地被多个事务修改,乐观锁会导致大量重试,影响性能。
- 版本号或时间戳的维护:需要正确设计和维护用于检测冲突的版本号或时间戳字段。
- 业务逻辑复杂性增加:虽然乐观锁简化了某些方面,但处理冲突和重试逻辑可能会增加业务逻辑的复杂度。
1. 什么是乐观锁
乐观锁通常通过版本号(Version Number)或时间戳(Timestamp)来实现。当一个记录被读取时,会同时获取它的版本号或时间戳。在更新这条记录之前,会再次检查这个版本号或时间戳,确保自上次读取以来没有被修改。如果有修改,那么更新操作将失败,应用程序可以根据具体需求选择重试或者其他处理方式。
1.1 使用版本号实现乐观锁
- 在表中添加一个版本字段,例如
version
。 - 读取记录时也读取版本信息。
- 更新记录时,根据版本号进行条件判断;
- 如果
UPDATE
语句影响的行数为0,说明在尝试更新的过程中,已经有其他事务修改了该记录,这时可以采取相应的措施,如提示用户数据已更改、重试等。
比如这个SQL语句,更新会将版本号+1,同时只有版本号未有其他操作时才能执行成功。
UPDATE table_name
SET column1 = value1, column2 = value2, version = version + 1
WHERE id = some_id AND version = current_version;
这种方式的优点是避免了锁定资源,提高了并发性能,但缺点是在高并发场景下可能会出现较多的重试情况。
1.2 使用时间戳实现乐观锁
第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp),和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
此方案有缺点,就是当并发事务时间间隔小于当前系统平台的最小时间单位时,会发生覆盖前一个事务结果的问题。
2. Mybatis实现乐观锁
通常的做法是通过版本号(version)字段来实现
<insert id="isExistsUser" >
select count(1) from t_users where userId = #{id}
</insert>
<select id="selectVersion" parameterType="user" resultType="long" >
select version from t_users where userName = #{userName}
</select>
<update id="updateByVersion" parameterType="user">
update t_users set version=version+1, password= #{password} where userName = #{userName} and version=#{version}
</update>
<select id="selectUsers" resultType="int">
select count(1)
from t_users
where id = #{id}
</select>
程序逻辑实现:
int userCount = UserMapper.selectUsers(id);
if (userCount==0)
{
Long version = UserMapper.selectVersion(user);
int call = 0;
user.setVersion(version);
while (UserMapper.updateByVersion(user)==0)
{
if (call++==3)
{
break;
}
}
}
3. MyBatis-Plus实现乐观锁
以下时基于SpringBoot的实现,与非此方法大相径庭
3.1 添加依赖
首先确保你的项目中已经添加了 MyBatis-Plus 的依赖。如果你使用的是 Maven,可以在 pom.xml
文件中加入以下依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>版本号</version>
</dependency>
3.2. 配置乐观锁插件
在 Spring Boot 应用程序的配置类或主类中,你需要配置 MyBatis-Plus 的乐观锁插件。通常这一步是通过 MybatisPlusConfig
类来完成的。
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
3.3. 实体类设置
在实体类中,需要添加一个字段来作为版本号。MyBatis-Plus 默认会识别名为 version
或带有 @Version
注解的字段作为乐观锁的版本号。
import com.baomidou.mybatisplus.annotation.Version;
public class ExampleEntity {
private Long id;
private String data;
@Version
private Integer version;
// getters and setters
}
或者,如果你的版本字段名不是 version
,你可以直接使用 @Version
注解指定它:
import com.baomidou.mybatisplus.annotation.Version;
public class ExampleEntity {
private Long id;
private String data;
@Version
private Integer optimisticLockVersion; // 自定义名称
// getters and setters
}
注意事项
- 支持的数据类型包括:
int
,Integer
,long
,Long
,Date
,Timestamp
,LocalDateTime
。 - 对于整数类型,
newVersion
是oldVersion + 1
。 newVersion
会自动回写到实体对象中。- 支持内置的
updateById(entity)
和update(entity, wrapper)
,saveOrUpdate(entity)
,insertOrUpdate(entity) (version >=3.5.7)
方法。 - 自定义方法更新时如果满足内置参数的参数条件方式也会执行乐观锁逻辑,例如自定义
myUpate(entity)
这个和updateById(entity)
是等价的,会提取参数进行乐观锁填充,但更新实现需要自行处理。 - 在
update(entity, wrapper)
方法中,wrapper
不能复用。
3.4. 使用乐观锁
当你更新数据时,MyBatis-Plus 会自动处理乐观锁逻辑。例如:
@Service
public class ExampleService {
@Autowired
private ExampleMapper exampleMapper;
@Transactional
public void updateData(Long id, String newData) {
ExampleEntity entity = exampleMapper.selectById(id);
if (entity == null) {
throw new RuntimeException("Entity not found");
}
entity.setData(newData);
int result = exampleMapper.updateById(entity);
if (result == 0) {
throw new OptimisticLockingFailureException("失败了,请重试");
}
}
}
这里,当调用 updateById
方法时,MyBatis-Plus 会自动检查并更新版本号。如果在尝试更新时发现版本号不匹配(即数据已被其他事务修改),则更新操作不会成功,返回值将是 0,这时可以抛出异常或进行重试。