【悲观锁和乐观锁有什么区别】以及在Spring Boot、MybatisPlus、PostgreSql中使用
悲观锁和乐观锁是两种常见的并发控制方式,它们在处理并发数据访问时的策略和实现方式有很大的不同。下面是这两者的主要区别:
1. 锁的策略
悲观锁(Pessimistic Locking):
假设并发冲突频繁发生,因此在操作数据之前就会加锁,确保其他事务无法同时操作这条数据,避免数据的并发修改。只有获得锁的事务才能对数据进行修改,其他事务会被阻塞,直到当前事务完成。
- 特点:乐观地认为数据可能会被同时修改,因此采取悲观的做法,强制加锁。
- 实现方式:通过 SELECT FOR UPDATE 或数据库的锁机制(例如行锁、表锁)来实现。
- 适用场景:适用于高并发情况下需要保证数据一致性的业务场景,例如资金转账、库存扣减等。
乐观锁(Optimistic Locking):
假设并发冲突较少,事务并不会立刻加锁,而是在更新时验证数据是否被其他事务修改。如果数据在事务操作期间没有被其他事务修改(通常通过版本号等机制检查),就可以提交更新;如果数据已经被其他事务修改,则会回滚或抛出异常。
- 特点:乐观地认为数据不会被并发修改,因此不加锁,只在更新时进行冲突检查。
- 实现方式:通常通过在数据表中添加 version 字段或时间戳字段,每次更新时检查版本号是否一致来实现。
- 适用场景:适合读多写少的业务场景。适用于并发冲突较少,且更新操作不频繁的场景,例如普通的查询和更新操作。
2. 锁的粒度
悲观锁:
会对数据进行实际的加锁,其他事务在该数据的锁释放之前无法访问或修改数据。锁的粒度通常为行锁或表锁,具体取决于数据库的实现。
乐观锁:
不会对数据加锁,只是在数据修改时检查版本号或时间戳等信息,以判断数据是否被其他事务修改过。它的粒度通常为数据记录的版本字段
3. 性能与开销
悲观锁:
由于加锁机制,它的性能开销较大。多个事务竞争同一数据时,其他事务需要等待锁释放,可能导致性能瓶颈,尤其在高并发的情况下。
乐观锁:
由于没有加锁,它的性能较高,适合读取操作多、写入操作少的场景。它的开销主要在于更新时的版本检查,性能损耗较低。
4. 适用场景
悲观锁:
适用于数据竞争较为激烈的场景,例如银行转账、库存更新等高并发操作。
适用于对数据一致性要求极高的业务场景,需要强制保证同一时间只有一个事务能修改数据。
乐观锁:
适用于数据竞争较少的场景,例如用户资料更新、普通的库存查询等。
适用于不频繁更新的场景,可以减少数据库的锁竞争,提高系统的吞吐量。
5. 事务阻塞
悲观锁:
由于加锁,其他事务在等待锁释放期间会被阻塞,可能会引起性能下降或死锁。
乐观锁:
不会导致阻塞,多个事务可以同时读取数据,只有在提交时检查数据是否被修改。若数据被修改,则需要回滚或重新尝试更新,但不会影响其他事务的执行。
6. 死锁风险
悲观锁:
在并发高的情况下,悲观锁可能导致死锁(特别是当事务顺序不一致时),因为多个事务可能会相互等待对方释放锁。
乐观锁:
乐观锁不会产生死锁,因为它没有显式的锁操作。它依赖版本号来解决并发问题,即使并发冲突发生,也只是简单的版本检查或回滚。
7. 在Spring Boot中悲观锁的实现
使用 MyBatis-Plus 实现悲观锁,实际上就是通过 SQL 查询语句 来加锁。在 MyBatis 中,可以通过 FOR UPDATE
来实现悲观锁。
1.修改数据库查询,使用悲观锁:
使用 FOR UPDATE
来锁定查询到的行。你可以在 Mapper 中自定义 SQL 查询,指定 FOR UPDATE
。
假设你有一个 gift 表,表结构如下:
CREATE TABLE gift (id BIGINT PRIMARY KEY,name VARCHAR(255),quantity INT
);
在 Mapper 接口中,定义一个查询方法,使用 FOR UPDATE
:
@Mapper
public interface GiftMapper extends BaseMapper<Gift> {@Select("SELECT * FROM gift WHERE id = #{id} FOR UPDATE")Gift selectForUpdate(Long id);
}
2.服务层调用悲观锁:
在服务层使用 @Transactional 注解,确保事务的原子性。
@Service
public class GiftService {@Autowiredprivate GiftMapper giftMapper;@Transactionalpublic boolean redeemGift(Long giftId) {Gift gift = giftMapper.selectForUpdate(giftId);if (gift == null) {return false;}// 检查库存if (gift.getQuantity() > 0) {gift.setQuantity(gift.getQuantity() - 1);giftMapper.updateById(gift);return true;} else {return false;}}
}
在上述代码中,selectForUpdate 查询会加锁,确保只有一个事务可以操作该记录。如果其他事务尝试访问该记录,它们会被阻塞,直到当前事务完成。
3.数据库配置:
默认情况下,PostgreSQL 的隔离级别是 READ COMMITTED,它支持 FOR UPDATE
锁。如果你需要更高的隔离级别,可以配置事务隔离级别为 SERIALIZABLE,但是对于大多数场景,READ COMMITTED 就足够了。
可以在 application.properties 文件中配置事务隔离级别:
spring.datasource.hikari.transaction-isolation=TRANSACTION_READ_COMMITTED
8. 在Spring Boot中乐观锁的实现
<!--mybatis 官方-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version>
</dependency><!--mybatis plus 非官方-->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version>
</dependency>
乐观锁通常通过版本号机制来实现。每次更新数据时,会验证数据是否被其他事务修改过。如果数据被修改,则抛出 OptimisticLockException 异常,提示并发冲突。
1.在config包中添加乐观锁配置类:
package com.ckm.ball.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@MapperScan("com.ckm.ball.mapper") // 扫描你的 Mapper 包
public class MyBatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 注册乐观锁插件MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return mybatisPlusInterceptor;}
}
2.在实体类中添加版本字段:
在实体类中添加 version
字段,并使用 MyBatis-Plus 提供的 @Version 注解
来标识该字段为版本号字段。
import com.baomidou.mybatisplus.annotation.*;@TableName("gift")
public class Gift {@TableIdprivate Long id;private String name;private Integer quantity;@Versionprivate Long version; // 版本字段// getters and setters
}
在这个例子中,version
字段会随着每次更新而自动递增。
3.更新时使用乐观锁:
MyBatis-Plus 会自动处理乐观锁的更新。你只需在更新时,确保在实体类中包含 @Version 注解
的字段。
例如,在服务层进行更新操作时,MyBatis-Plus 会自动比较版本号,确保数据没有被其他事务修改。
@Service
public class GiftService {@Autowiredprivate GiftMapper giftMapper;@Transactionalpublic boolean redeemGift(Long giftId) {Gift gift = giftMapper.selectById(giftId);if (gift == null) {return false;}// 检查库存if (gift.getQuantity() > 0) {gift.setQuantity(gift.getQuantity() - 1);// 更新时会自动检查版本号int rows = giftMapper.updateById(gift);if (rows == 0) {// 如果更新失败,说明版本号不匹配,表示并发冲突return false;}//在这里处理里的其他逻辑//如扣除相应积分,存储兑换记录等return true;} else {return false;}}
}
在更新时,MyBatis-Plus 会自动检查 version
字段。如果数据的版本号与当前数据库中的版本号不一致,则更新失败,返回 0,表示发生了并发冲突。
4.配置乐观锁:
如果使用 MyBatis-Plus,乐观锁只需要在实体类中标记 @Version 注解
,不需要其他特殊配置。
总结
悲观锁:
通过 FOR UPDATE 锁定查询的行,适用于高并发的情况下,确保同一时刻只有一个事务修改数据。你可以通过 MyBatis 的@Select 注解配合 FOR UPDATE 来实现。乐观锁:
通过版本号机制,确保数据在更新时没有被其他事务修改。MyBatis-Plus 支持通过 @Version 注解来实现乐观锁。