MyBatis 用户请注意!告别 @Lock,在 XML 中手动为你的数据加把“锁” 🔒
大家好!在上一篇博客中,我们探讨了如何在 Spring Data JPA 中使用 @Lock 注解轻松实现悲观锁,以保护我们的核心交易流程。但问题来了:“如果我的项目用的是 MyBatis,没有了方便的注解,我该怎么办?” 🤔
别担心!MyBatis 作为一款灵活强大的持久层框架,同样为我们提供了实现悲观锁的能力,只是方式更加“原始”和“直接”。今天,我们就来一场从 JPA 到 MyBatis 的“穿越之旅”,看看如何在 XML (eXtensible Markup Language, 可扩展标记语言) 映射文件中,手动为我们的数据加上那把至关重要的悲观锁!
快速概览:JPA vs. MyBatis 的悲观锁实现 📝
| 对比项 (Aspect) | Spring Data JPA 实现 | MyBatis 实现 | 核心思想 (Core Idea) |
|---|---|---|---|
| 实现方式 | 注解驱动 (Annotation-driven):在 Repository 接口的方法上使用 @Lock 和 @Query 注解。 | SQL (Structured Query Language, 结构化查询语言) 驱动 (SQL-driven):在 Mapper XML 文件中,直接编写带有 FOR UPDATE 子句的 SQL 语句。 | 异曲同工:最终都是向数据库发送带有锁定指令的 SQL 查询。 |
| 代码位置 | 锁的逻辑在 Java 接口中声明,对业务层更透明。 | 锁的逻辑在 XML 映射文件中定义,与 SQL 紧密耦合。 | 关注点分离:JPA 将锁的意图保留在 Java 代码中,MyBatis 将其视为 SQL 实现的一部分。 |
| 优点 | 简洁、类型安全:IDE (Integrated Development Environment, 集成开发环境) 支持好,代码可读性强。 | 灵活、强大:可以编写任何复杂的、数据库特有的带锁查询,完全控制 SQL。 | 各取所需:JPA 追求开发效率和规范性,MyBatis 追求灵活性和对 SQL 的终极控制。 |
| 缺点 | 灵活性稍差:对于非常复杂的或数据库特有的锁定语法,可能支持不佳。 | 代码分散:业务逻辑(需要加锁)和实现(XML中的SQL)分离,需要同时查看 Java 和 XML 文件。 | 权衡利弊:没有绝对的好坏,选择取决于团队的技术栈偏好和项目需求。 |
MyBatis 实现悲观锁:Show Me The Code! 💻
假设我们有和之前一样的场景:需要在一个事务中,锁定并更新 MemberUser 的积分和 UserCoupon 的状态。
1. 修改 Mapper 接口
在你的 Java Mapper 接口中,定义新的带锁查询方法。方法名可以清晰地表达其意图。
MemberUserMapper.java
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
// ...
@Mapper
public interface MemberUserMapper {
// ... 其他方法 ...
/**
* 根据ID查询用户,并施加排他锁 (FOR UPDATE)
* @param id 用户ID
* @return 用户实体
*/
MemberUser findByIdWithLock(@Param("id") Integer id);
}
UserCouponMapper.java
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
// ...
@Mapper
public interface UserCouponMapper {
// ... 其他方法 ...
/**
* 根据ID查询用户优惠券,并施加排他锁 (FOR UPDATE)
* @param id 优惠券ID
* @return 用户优惠券实体
*/
UserCoupon findByIdWithLock(@Param("id") Integer id);
}
2. 在 XML 映射文件中实现 SQL
这是最关键的一步!我们需要在对应的 XML 文件中,为上面定义的方法编写 SQL,并在末尾加上 FOR UPDATE。
MemberUserMapper.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.productQualification.coupon.repository.MemberUserMapper">
<!-- 定义 ResultMap 以匹配实体字段和数据库列 -->
<resultMap id="BaseResultMap" type="com.productQualification.coupon.domain.MemberUser">
<id column="id" property="id" />
<result column="total_points" property="totalPoints" />
<result column="used_points" property="usedPoints" />
<!-- ... 其他所有字段的映射 ... -->
</resultMap>
<!-- 带锁查询的实现 -->
<select id="findByIdWithLock" resultMap="BaseResultMap" parameterType="java.lang.Integer">
SELECT
id, total_points, used_points, <!-- ... 其他所有列 ... -->
FROM
member_user
WHERE
id = #{id}
FOR UPDATE
</select>
<!-- 其他 CRUD (Create, Read, Update, Delete) 操作 -->
</mapper>
UserCouponMapper.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.productQualification.coupon.repository.UserCouponMapper">
<resultMap id="BaseResultMap" type="com.productQualification.coupon.domain.UserCoupon">
<id column="id" property="id" />
<result column="status" property="status" />
<!-- ... 其他所有字段的映射 ... -->
</resultMap>
<!-- 带锁查询的实现 -->
<select id="findByIdWithLock" resultMap="BaseResultMap" parameterType="java.lang.Integer">
SELECT
id, status, <!-- ... 其他所有列 ... -->
FROM
user_coupon
WHERE
id = #{id}
FOR UPDATE
</select>
<!-- 其他 CRUD 操作 -->
</mapper>
核心改动: 在 SELECT 语句的末尾,我们加上了 FOR UPDATE。当这个查询在事务中执行时,数据库就会锁定被查询到的数据行。
3. 在 Service 层调用
Service 层的调用逻辑与使用 JPA 时非常相似,只是现在调用的是 MyBatis Mapper 的方法。
TransactionProcessingService.java
import org.springframework.transaction.annotation.Transactional;
// ...
@Service
public class TransactionProcessingService {
// 注入 MyBatis Mappers
private final UserCouponMapper userCouponMapper;
private final MemberUserMapper memberUserMapper;
// ...
@Transactional // 事务注解依然至关重要!
public TransactionProcessResultDto processTransactionByAdmin(...) {
// ...
// 处理优惠券时
UserCoupon userCoupon = userCouponMapper.findByIdWithLock(payload.getUserCouponId());
if (userCoupon == null) {
throw new NotFoundException(...);
}
// 处理积分时
MemberUser freshMemberUserForPoints = memberUserMapper.findByIdWithLock(memberUser.getId());
if (freshMemberUserForPoints == null) {
throw new NotFoundException(...);
}
// ...
}
}
可视化分析:MyBatis 的悲观锁世界 🗺️
流程图:从 Java 调用到 SQL 执行
时序图:MyBatis 加锁的交互过程
状态图:数据行的锁定状态 (与JPA场景一致)
类图:MyBatis 架构下的组件关系
实体关系图 (ERD - Entity Relationship Diagram)
ERD (Entity Relationship Diagram, 实体关系图) 保持不变,因为它描述的是数据库表之间的关系,与使用的持久层框架无关。
思维导图总结 🧠

结语
无论你使用 Spring Data JPA 还是 MyBatis,实现悲观锁的底层原理都是相通的——利用数据库的行级锁机制。JPA 用优雅的注解封装了细节,而 MyBatis 则让我们直面 SQL,提供了最大的灵活性。
现在,即使你的项目是基于 MyBatis 构建的,你也能充满信心地为你的核心业务加上那把不可或缺的“安全锁”了!希望这篇博客对你有所帮助!✨
155

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



