MyBatis 实现乐观锁

使用 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 实现乐观锁的关键点:

  1. 版本号字段:在表中添加 version 字段
  2. UPDATE语句:在更新时检查版本号 WHERE id = #{id} AND version = #{version}
  3. 返回值判断:根据 affectedRows 判断是否更新成功
  4. 重试机制:使用循环或工具类实现自动重试
  5. 异常处理:自定义异常类处理乐观锁冲突

这种实现方式比 JPA 的 @Version 注解更灵活,可以精确控制乐观锁的逻辑。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值