思路:
在分布式系统中,乐观锁的核心设计思想是 “假设冲突概率低,通过版本控制避免显式加锁”,适用于并发更新频繁但冲突较少的场景(如库存扣减、订单状态更新等)。
与悲观锁(如分布式锁)通过阻塞控制并发不同,乐观锁通过 “版本校验” 实现无阻塞并发,性能更优。
version或update_time),用于标记资源的当前状态。
以电商库存扣减为例:
CREATE TABLE `product_stock` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`stock` int NOT NULL COMMENT '当前库存',
`version` int NOT NULL DEFAULT 0 COMMENT '版本号,用于乐观锁',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT '商品库存表';
-- 初始化数据:商品ID=1001,库存=100
INSERT INTO `product_stock` (`id`, `stock`, `version`) VALUES (1001, 100, 0);
核心逻辑:查询库存与版本号 → 校验库存是否充足 → 带版本号更新库存 → 校验更新结果(失败则重试)。
// 实体类:商品库存
@Data
public class ProductStock {
private Long id;
private Integer stock;
private Integer version; // 版本号
}
// Mapper接口(MyBatis)
public interface ProductStockMapper {
// 查询商品库存(含版本号)
ProductStock selectById(Long id);
// 扣减库存(带版本号校验)
// SQL:UPDATE product_stock SET stock = stock - #{deductNum}, version = version + 1
// WHERE id = #{id} AND version = #{version} AND stock >= #{deductNum}
int deductStock(@Param("id") Long id, @Param("deductNum") int deductNum, @Param("version") int version);
}
@Service
public class StockService {
@Autowired
private ProductStockMapper productStockMapper;
// 最大重试次数(避免无限重试)
private static final int MAX_RETRY = 3;
/**
* 扣减商品库存
* @param productId 商品ID
* @param deductNum 扣减数量
* @return 是否成功
*/
public boolean deductStock(Long productId, int deductNum) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
// 1. 查询当前库存与版本号(分布式共享数据)
ProductStock stock = productStockMapper.selectById(productId);
if (stock == null) {
throw new RuntimeException("商品不存在");
}
// 2. 提前校验库存是否充足(减少无效更新)
if (stock.getStock() < deductNum) {
System.out.println("库存不足,当前库存:" + stock.getStock());
return false;
}
// 3. 带版本号更新库存(核心:乐观锁校验)
int affectRows = productStockMapper.deductStock(productId, deductNum, stock.getVersion());
if (affectRows > 0) {
// 更新成功:版本号已递增,其他线程将读取到新版本
System.out.println("库存扣减成功,剩余库存:" + (stock.getStock() - deductNum));
return true;
}
// 4. 更新失败(版本号不匹配,说明被其他线程修改),重试
retryCount++;
System.out.println("扣减冲突,重试次数:" + retryCount);
}
// 超过最大重试次数,降级处理(如返回失败,由用户重试)
System.out.println("超过最大重试次数,扣减失败");
return false;
}
}
扣减库存的 SQL 语句是乐观锁的核心,通过WHERE条件同时校验版本号和库存充足性:
UPDATE product_stock
SET stock = stock - #{deductNum}, version = version + 1
WHERE id = #{id}
AND version = #{version} -- 版本号校验:确保期间未被修改
AND stock >= #{deductNum} -- 库存充足性校验
version与查询时一致,且库存充足,则更新成功(影响行数 = 1);version已被其他线程修改(版本号不匹配),或库存不足,则更新失败(影响行数 = 0)。重试策略设计:冲突时需重试,但需限制重试次数(如 3 次),避免高冲突场景下的无限循环导致性能雪崩。可结合退避策略(如重试间隔递增)降低并发压力。
版本号的原子性:版本号的更新必须与业务操作(如库存扣减)在同一事务中,确保原子性(依赖数据库事务 ACID)。
与悲观锁的配合:若业务场景冲突频繁(如秒杀最后几件商品),乐观锁的重试可能导致性能下降,此时可降级为悲观锁(如SELECT ... FOR UPDATE),避免无效重试。
非数据库场景的适配:若共享资源存储在 Redis 中,可通过WATCH命令实现乐观锁(监控版本号键,事务中校验版本号是否变化):
// Redis乐观锁伪代码
String key = "product:stock:1001";
String versionKey = "product:version:1001";
int deductNum = 1;
Jedis jedis = new Jedis("localhost");
try {
jedis.watch(versionKey); // 监控版本号
int currentVersion = Integer.parseInt(jedis.get(versionKey));
int currentStock = Integer.parseInt(jedis.get(key));
if (currentStock < deductNum) {
return false;
}
// 事务中更新库存和版本号
Transaction tx = jedis.multi();
tx.decrBy(key, deductNum);
tx.incr(versionKey);
List<Object> result = tx.exec(); // 若versionKey被修改,result为null
return result != null && !result.isEmpty();
} finally {
jedis.unwatch();
jedis.close();
}
分布式乐观锁通过 “版本号校验 + 无阻塞重试” 实现高效并发控制,适合冲突较少的场景。核心是依赖共享存储的版本标识,确保所有节点能感知资源状态变化,同时通过应用层重试解决冲突。相比分布式悲观锁(如 Redis 分布式锁),乐观锁避免了锁竞争导致的阻塞,性能更优,但需合理设计重试策略以应对高冲突场景。