在数据库并发场景下,多个事务同时操作同一行数据时,很容易出现数据不一致的问题,比如丢失更新、脏读等。为了避免这类问题,数据库提供了多种锁机制,其中悲观锁和乐观锁是最常用的两种并发控制方案,而MyBatis作为主流的持久层框架,也支持这两种锁的落地实现。

悲观锁与乐观锁的核心原理
悲观锁
悲观锁的核心思想是“悲观地认为并发冲突一定会发生”,因此在操作数据之前,会先对数据加锁,直到事务提交才会释放锁,这样其他事务就无法同时操作这行数据。
在关系型数据库中,悲观锁通常依赖数据库的锁机制实现,比如MySQL的InnoDB引擎支持行级锁,通过SELECT ... FOR UPDATE语句可以对查询到的行加排他锁,其他事务想要对这些行执行更新、删除或者加排他锁的操作时,会被阻塞直到当前事务提交。
悲观锁的优点是能完全避免并发冲突,数据一致性有保障,但缺点是加锁会增加系统开销,而且如果锁持有时间过长,会影响系统的并发性能,还可能引发死锁问题。
乐观锁
乐观锁的核心思想是“乐观地认为并发冲突发生的概率很低”,因此操作数据时不会先加锁,而是在更新数据时判断这段时间有没有其他事务修改过这条数据。
乐观锁最常用的实现方式是版本号机制:给数据表增加一个版本号字段,每次读取数据时把版本号一起查出来,更新数据时判断当前数据库中的版本号和之前读取的版本号是否一致,如果一致就把版本号加1并更新数据,如果不一致就说明数据已经被其他事务修改过,当前更新操作失败。
乐观锁的优点是不需要加锁,并发性能高,适合读多写少的场景,缺点是如果并发冲突频繁,会导致大量更新失败,需要业务层做重试处理。
MyBatis中悲观锁for update方案落地
在MyBatis中使用for update实现悲观锁非常简单,只需要在查询语句中添加for update后缀即可,前提是使用的数据库引擎支持行级锁,比如MySQL的InnoDB。
表结构与实体类
假设我们有一个商品库存表,结构如下:
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_name` varchar(50) NOT NULL COMMENT '商品名称', `stock` int(11) NOT NULL COMMENT '库存数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
对应的实体类:
public class Product {
private Long id;
private String productName;
private Integer stock;
// getter和setter方法省略
}
Mapper接口与XML配置
首先在Mapper接口中定义查询方法:
public interface ProductMapper {
/**
* 根据id查询商品并加悲观锁
* @param id 商品id
* @return 商品信息
*/
Product selectByIdForUpdate(@Param("id") Long id);
}
对应的MyBatis XML映射文件中的语句:
<select id="selectByIdForUpdate" resultType="com.example.demo.entity.Product">
SELECT id, product_name, stock
FROM product
WHERE id = #{id}
FOR UPDATE
</select>
业务层使用示例
在业务层中使用悲观锁扣减库存的示例:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class ProductService {
@Resource
private ProductMapper productMapper;
/**
* 扣减库存(悲观锁方案)
* @param productId 商品id
* @param reduceCount 扣减数量
* @return 是否扣减成功
*/
@Transactional(rollbackFor = Exception.class)
public boolean reduceStockWithPessimisticLock(Long productId, Integer reduceCount) {
// 查询商品并加悲观锁,此时其他事务无法修改该商品数据
Product product = productMapper.selectByIdForUpdate(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
if (product.getStock() < reduceCount) {
throw new RuntimeException("库存不足");
}
// 扣减库存
product.setStock(product.getStock() - reduceCount);
int updateCount = productMapper.updateById(product);
return updateCount > 0;
}
}
对应的更新方法XML配置:
<update id="updateById" parameterType="com.example.demo.entity.Product">
UPDATE product
SET stock = #{stock}
WHERE id = #{id}
</update>
需要注意的是,for update语句必须在事务中生效,否则查询完之后锁会立刻释放,达不到悲观锁的效果,因此业务方法上需要添加@Transactional注解开启事务。
MyBatis中乐观锁版本号方案落地
乐观锁的版本号方案需要先给数据表增加版本号字段,然后在更新时判断版本号是否匹配。
表结构改造与实体类更新
给商品表增加版本号字段:
ALTER TABLE product ADD COLUMN `version` int(11) NOT NULL DEFAULT 0 COMMENT '版本号';
更新后的实体类:
public class Product {
private Long id;
private String productName;
private Integer stock;
private Integer version;
// getter和setter方法省略
}
Mapper接口与XML配置
Mapper接口定义查询和更新方法:
public interface ProductMapper {
/**
* 根据id查询商品(乐观锁用,不需要加锁)
* @param id 商品id
* @return 商品信息
*/
Product selectById(@Param("id") Long id);
/**
* 乐观锁更新库存,只有版本号匹配时才更新,同时版本号加1
* @param id 商品id
* @param reduceCount 扣减数量
* @param oldVersion 旧的版本号
* @return 更新影响的行数
*/
int updateStockWithOptimisticLock(@Param("id") Long id, @Param("reduceCount") Integer reduceCount, @Param("oldVersion") Integer oldVersion);
}
对应的XML配置:
<select id="selectById" resultType="com.example.demo.entity.Product">
SELECT id, product_name, stock, version
FROM product
WHERE id = #{id}
</select>
<update id="updateStockWithOptimisticLock">
UPDATE product
SET stock = stock - #{reduceCount},
version = version + 1
WHERE id = #{id}
AND version = #{oldVersion}
</update>
业务层使用示例
乐观锁扣减库存的业务逻辑:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class ProductService {
@Resource
private ProductMapper productMapper;
/**
* 扣减库存(乐观锁版本号方案)
* @param productId 商品id
* @param reduceCount 扣减数量
* @return 是否扣减成功
*/
@Transactional(rollbackFor = Exception.class)
public boolean reduceStockWithOptimisticLock(Long productId, Integer reduceCount) {
// 查询商品信息,获取当前版本号
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
if (product.getStock() < reduceCount) {
throw new RuntimeException("库存不足");
}
// 执行乐观锁更新,返回影响的行数
int updateCount = productMapper.updateStockWithOptimisticLock(productId, reduceCount, product.getVersion());
if (updateCount == 0) {
// 更新失败,说明版本号不匹配,数据被其他事务修改过
throw new RuntimeException("并发冲突,扣减库存失败,请重试");
}
return true;
}
}
如果业务中需要更高的成功率,可以在更新失败的时候加入重试逻辑,比如重试3次,每次重试前重新查询最新的商品信息和版本号再执行更新操作。
两种方案的选型建议
在实际项目中,选择悲观锁还是乐观锁需要结合业务场景:
- 如果业务场景是写多读少,或者并发冲突概率高,比如库存扣减、账户余额变更这类操作,建议使用悲观锁,能避免大量更新失败的问题。
- 如果业务场景是读多写少,并发冲突概率低,比如商品信息修改、用户资料更新这类操作,建议使用乐观锁,性能更高,也不会产生死锁问题。
另外需要注意,for update悲观锁如果查询条件没有命中索引,会从行锁升级为表锁,会严重影响数据库的并发性能,因此使用悲观锁时一定要确保查询条件使用了索引。
悲观锁乐观锁MyBatisfor_update版本号修改时间:2026-06-18 03:45:55