使用 MyBatis 实现乐观锁需要手动处理版本号的检查和更新。
一、数据表设计
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL,
version INT DEFAULT 0 COMMENT '版本号,用于乐观锁'
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
二、实体类设计
// 产品实体类
@Data
public class Product {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private Integer version; // 乐观锁版本号
}
// 订单实体类
@Data
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer quantity;
private String status;
private Date createTime;
}
三、MyBatis Mapper 接口
@Mapper
public interface ProductMapper {
/**
* 根据ID查询产品
*/
Product selectById(Long id);
/**
* 乐观锁更新库存
* 返回受影响的行数:1表示成功,0表示版本冲突或库存不足
*/
int updateStockWithVersion(@Param("id") Long id,
@Param("quantity") Integer quantity,
@Param("version") Integer version);
/**
* 查询产品信息(包含版本号)
*/
Product selectForUpdate(Long id);
/**
* 批量更新产品信息(带乐观锁)
*/
int batchUpdate(@Param("list") List<Product> products);
}
@Mapper
public interface OrderMapper {
/**
* 插入订单
*/
int insert(Order order);
/**
* 更新订单状态
*/
int updateStatus(@Param("id") Long id, @Param("status") String status);
}
四、MyBatis XML 映射文件
ProductMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ProductMapper">
<resultMap id="ProductResultMap" type="Product">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="price" column="price" />
<result property="stock" column="stock" />
<result property="version" column="version" />
</resultMap>
<select id="selectById" resultMap="ProductResultMap">
SELECT id, name, price, stock, version
FROM products
WHERE id = #{id}
</select>
<select id="selectForUpdate" resultMap="ProductResultMap">
SELECT id, name, price, stock, version
FROM products
WHERE id = #{id}
</select>
<!-- 乐观锁更新:版本号+1,同时检查旧版本号和库存 -->
<update id="updateStockWithVersion">
UPDATE products
SET stock = stock - #{quantity},
version = version + 1
WHERE id = #{id}
AND version = #{version}
AND stock >= #{quantity}
</update>
<!-- 批量更新(带乐观锁) -->
<update id="batchUpdate">
<foreach collection="list" item="product" separator=";">
UPDATE products
SET name = #{product.name},
price = #{product.price},
stock = #{product.stock},
version = version + 1
WHERE id = #{product.id}
AND version = #{product.version}
</foreach>
</update>
</mapper>
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.OrderMapper">
<resultMap id="OrderResultMap" type="Order">
<id property="id" column="id" />
<result property="userId" column="user_id" />
<result property="productId" column="product_id" />
<result property="quantity" column="quantity" />
<result property="status" column="status" />
<result property="createTime" column="create_time" />
</resultMap>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO orders (user_id, product_id, quantity, status)
VALUES (#{userId}, #{productId}, #{quantity}, #{status})
</insert>
<update id="updateStatus">
UPDATE orders SET status = #{status} WHERE id = #{id}
</update>
</mapper>
五、Service 层实现
基础版本服务
@Service
@Transactional
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 基础乐观锁实现 - 扣减库存
*/
public boolean deductStock(Long productId, Integer quantity) {
// 1. 查询当前产品信息(包含版本号)
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("产品不存在");
}
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
// 2. 尝试乐观锁更新
int affectedRows = productMapper.updateStockWithVersion(
productId, quantity, product.getVersion());
return affectedRows > 0;
}
}
带重试机制的服务
@Service
public class OrderServiceWithRetry {
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderMapper orderMapper;
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY = 100; // 基础延迟100ms
/**
* 创建订单 - 带重试的乐观锁实现
*/
public OrderResult createOrder(OrderRequest request) {
int retryCount = 0;
while (retryCount < MAX_RETRIES) {
try {
return tryCreateOrder(request);
} catch (OptimisticLockException e) {
retryCount++;
log.warn("乐观锁冲突,重试第{}次,产品ID: {}", retryCount, request.getProductId());
if (retryCount >= MAX_RETRIES) {
break;
}
// 指数退避策略
long delay = (long) (BASE_DELAY * Math.pow(2, retryCount - 1));
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
return OrderResult.fail("系统繁忙,请稍后重试");
}
/**
* 单次尝试创建订单
*/
@Transactional(rollbackFor = Exception.class)
private OrderResult tryCreateOrder(OrderRequest request) {
// 1. 查询产品信息(包含版本号)
Product product = productMapper.selectById(request.getProductId());
if (product == null) {
return OrderResult.fail("产品不存在");
}
if (product.getStock() < request.getQuantity()) {
return OrderResult.fail("库存不足");
}
// 2. 乐观锁更新库存
int affectedRows = productMapper.updateStockWithVersion(
request.getProductId(),
request.getQuantity(),
product.getVersion()
);
if (affectedRows == 0) {
// 抛出异常触发重试
throw new OptimisticLockException("版本冲突");
}
// 3. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setStatus("PAID");
orderMapper.insert(order);
return OrderResult.success(order.getId());
}
}
自定义异常类
public class OptimisticLockException extends RuntimeException {
public OptimisticLockException(String message) {
super(message);
}
public OptimisticLockException(String message, Throwable cause) {
super(message, cause);
}
}
六、批量操作实现
@Service
public class BatchProductService {
@Autowired
private ProductMapper productMapper;
/**
* 批量更新产品信息(带乐观锁)
*/
@Transactional
public BatchUpdateResult batchUpdateProducts(List<Product> products) {
try {
int totalCount = products.size();
int successCount = productMapper.batchUpdate(products);
return new BatchUpdateResult(totalCount, successCount);
} catch (Exception e) {
log.error("批量更新失败", e);
throw new RuntimeException("批量更新失败", e);
}
}
/**
* 批量扣减库存
*/
@Transactional
public boolean batchDeductStock(Map<Long, Integer> productQuantities) {
// 1. 查询所有产品信息
List<Long> productIds = new ArrayList<>(productQuantities.keySet());
List<Product> products = productMapper.selectByIds(productIds);
// 2. 验证库存
for (Product product : products) {
Integer quantity = productQuantities.get(product.getId());
if (product.getStock() < quantity) {
throw new RuntimeException("产品ID: " + product.getId() + " 库存不足");
}
}
// 3. 批量更新(在XML中实现乐观锁)
int affectedRows = productMapper.batchUpdateStock(productQuantities);
return affectedRows == productQuantities.size();
}
}
批量操作的XML配置
<!-- 添加到 ProductMapper.xml -->
<update id="batchUpdateStock">
<foreach collection="productQuantities" item="quantity" index="id" separator=";">
UPDATE products
SET stock = stock - #{quantity},
version = version + 1
WHERE id = #{id}
AND version = (SELECT version FROM products WHERE id = #{id})
AND stock >= #{quantity}
</foreach>
</update>
<select id="selectByIds" resultMap="ProductResultMap">
SELECT id, name, price, stock, version
FROM products
WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
七、工具类和配置
重试工具类
@Component
public class RetryTemplate {
public <T> T executeWithRetry(Callable<T> task, int maxRetries,
long initialDelay, String operationName) {
int retryCount = 0;
while (retryCount < maxRetries) {
try {
return task.call();
} catch (Exception e) {
retryCount++;
log.warn("{}操作失败,重试第{}次", operationName, retryCount, e);
if (retryCount >= maxRetries) {
throw new RuntimeException(operationName + "失败,已达最大重试次数", e);
}
long delay = initialDelay * (long) Math.pow(2, retryCount - 1);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(operationName + "被中断", ie);
}
}
}
throw new RuntimeException(operationName + "失败");
}
}
使用重试工具类
@Service
public class ProductServiceWithRetryTemplate {
@Autowired
private ProductMapper productMapper;
@Autowired
private RetryTemplate retryTemplate;
public boolean deductStockWithRetry(Long productId, Integer quantity) {
return retryTemplate.executeWithRetry(() -> {
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
int affectedRows = productMapper.updateStockWithVersion(
productId, quantity, product.getVersion());
if (affectedRows == 0) {
throw new OptimisticLockException("版本冲突");
}
return true;
}, 3, 100L, "扣减库存");
}
}
八、测试用例
@SpringBootTest
public class ProductServiceTest {
@Autowired
private ProductService productService;
@Test
public void testOptimisticLock() {
// 模拟并发场景
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
if (productService.deductStock(1L, 1)) {
successCount.incrementAndGet();
}
} finally {
latch.countDown();
}
}).start();
}
latch.await();
// 验证:成功次数应该等于库存量
assert successCount.get() <= initialStock;
}
}
九、监控和日志
@Aspect
@Component
@Slf4j
public class OptimisticLockMonitorAspect {
@Around("execution(* com.example.service..*.*(..)) && @annotation(Retryable)")
public Object monitorOptimisticLock(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();
int retryCount = 0;
while (true) {
try {
return joinPoint.proceed();
} catch (OptimisticLockException e) {
retryCount++;
log.info("方法 {} 乐观锁冲突,第{}次重试", methodName, retryCount);
if (retryCount >= 3) {
log.warn("方法 {} 乐观锁冲突达到最大重试次数", methodName);
throw e;
}
Thread.sleep(100 * retryCount);
}
}
}
}
// 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retryable {
int maxAttempts() default 3;
long backoff() default 100;
}
总结
MyBatis 实现乐观锁的关键点:
- 版本号字段:在表中添加 version 字段
- UPDATE语句:在更新时检查版本号
WHERE id = #{id} AND version = #{version} - 返回值判断:根据 affectedRows 判断是否更新成功
- 重试机制:使用循环或工具类实现自动重试
- 异常处理:自定义异常类处理乐观锁冲突
这种实现方式比 JPA 的 @Version 注解更灵活,可以精确控制乐观锁的逻辑。
4040

被折叠的 条评论
为什么被折叠?



