MyBatis 用户请注意!告别 @Lock,在 XML 中手动为你的数据加把“锁” ✨

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 执行

Service层调用
mapper.findByIdWithLock(id)
MyBatis代理执行
找到Mapper接口方法
'findByIdWithLock'
根据命名空间和ID
匹配到XML中的 'select' 标签
执行XML中定义的SQL语句
'SELECT ... FOR UPDATE'
数据库收到SQL,
锁定数据行并返回结果

时序图:MyBatis 加锁的交互过程

Service层Mapper接口 (代理)MyBatis核心数据库findByIdWithLock(101)执行 'findByIdWithLock'加载 MemberUserMapper.xml找到 id="findByIdWithLock" 的 select 语句发送 SQL:SELECT ... FROM member_userWHERE id = 101 FOR UPDATE返回锁定的数据行映射为 MemberUser 对象返回 MemberUser 对象Service层Mapper接口 (代理)MyBatis核心数据库

状态图:数据行的锁定状态 (与JPA场景一致)

"初始状态"
"事务A:SELECT ... FOR UPDATE"
"事务A:COMMIT 或 ROLLBACK"
"事务B:SELECT ... FOR UPDATE"
"事务A释放锁后,事务B获取锁"
"未锁定"
"已锁定"
Fork_State
"事务B被阻塞"

类图:MyBatis 架构下的组件关系

"依赖"
"依赖"
"实现"
"实现"
TransactionProcessingService
+<<@Transactional>> processTransactionByAdmin()
«interface»
MemberUserMapper
+MemberUser findByIdWithLock(Integer id)
«interface»
UserCouponMapper
+UserCoupon findByIdWithLock(Integer id)
«XML Mapping»
MemberUserMapperXML
+select(id="findByIdWithLock")
«XML Mapping»
UserCouponMapperXML
+select(id="findByIdWithLock")

实体关系图 (ERD - Entity Relationship Diagram)

ERD (Entity Relationship Diagram, 实体关系图) 保持不变,因为它描述的是数据库表之间的关系,与使用的持久层框架无关。

MEMBER_USERintidPK主键stringnickname昵称longtotal_points总积分longused_points已用积分USER_COUPONintidPK主键intmember_user_idFK用户IDbytestatus状态POINT_TRANSACTIONintidPK主键intmember_user_idFK用户IDlongpoints_change积分变动拥有产生

思维导图总结 🧠

在这里插入图片描述

结语

无论你使用 Spring Data JPA 还是 MyBatis,实现悲观锁的底层原理都是相通的——利用数据库的行级锁机制。JPA 用优雅的注解封装了细节,而 MyBatis 则让我们直面 SQL,提供了最大的灵活性。

现在,即使你的项目是基于 MyBatis 构建的,你也能充满信心地为你的核心业务加上那把不可或缺的“安全锁”了!希望这篇博客对你有所帮助!✨

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值