深入讲解基于Java开发的借据锁

 更新时间:2026年03月08日 11:16:02   作者:LWH!  
这篇文章主要介绍了基于Java开发借据锁的相关资料,这个借据锁并非编程语言层面的锁,也不是数据库的行锁/表锁,而是一种业务层面的逻辑锁(也叫状态锁/软锁),需要的朋友可以参考下

基于 Java 开发的借据锁信息数据模型(DO)类,它映射了数据库中的 asset_loan_invoice_lock_info

@Table(name = "asset_loan_invoice_lock_info")
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class AssetLoanInvoiceLockInfoDo extends AssetLoanInvoiceLockInfoKeyDo implements Serializable {
    private static final long serialVersionUID = 1101362594517114915L;
    /**
     * 借据锁锁状态 EnumBool 0-否 1-是
     */
    @Column(name = "lock_status")
    private String lockStatus;
    /**
     * 锁定流水
     */
    @Column(name = "lock_serial_no")
    private String lockSerialNo;
    /**
     * 锁定交易码
     */
    @Column(name = "lock_trade_code")
    private String lockTradeCode;
    /**
     * 锁定交易描述
     */
    @Column(name = "lock_trade_description")
    private String lockTradeDescription;
    /**
     * 乐观锁版本
     */
    @Column(name = "lock_version")
    private Integer lockVersion;
    /**
     * 创建时间
     */
    @Column(name = "create_time")
    private java.util.Date createTime;
    /**
     * 更新时间
     */
    @Column(name = "update_time")
    private java.util.Date updateTime;
    /**
     * 锁定时间
     */
    @Column(name = "lock_time")
    private java.util.Date lockTime;
}
字段名类型对应数据库列含义
serialVersionUIDlong-序列化版本号,保证序列化 / 反序列化的兼容性
lockStatusStringlock_status借据锁状态,枚举值(0 - 未锁定,1 - 已锁定)
lockSerialNoStringlock_serial_no锁定操作的流水号,用于追踪锁定行为
lockTradeCodeStringlock_trade_code锁定交易的编码,标识具体的锁定业务类型
lockTradeDescriptionStringlock_trade_description锁定交易的描述信息,补充说明锁定原因 / 场景
lockVersionIntegerlock_version乐观锁版本号,用于解决并发更新冲突
createTimeDatecreate_time记录创建时间
updateTimeDateupdate_time记录更新时间
lockTimeDatelock_time借据锁定的具体时间

借据锁的核心本质

这个 “借据锁”并非编程语言层面的锁(如 Java 的 synchronized、ReentrantLock),也不是数据库的行锁 / 表锁,而是一种业务层面的逻辑锁(也叫状态锁 / 软锁),本质是通过数据库记录的状态标识来控制借据的操作权限,属于金融系统中典型的 “乐观锁 + 业务状态” 组合设计。

简单来说:它是给每一笔借据(贷款凭证)加的 “业务标记”,标记该借据是否被占用,防止同一笔借据在不同业务流程中被重复操作(比如重复放款、重复核销)。

借据锁的核心作用场景

以金融资产系统为例,借据锁主要用于这些高并发、高风险的场景:

  1. 放款操作:发起放款时,先锁定借据,防止同一笔借据被多个放款请求同时处理,导致重复放款;放款完成后解锁。
  2. 核销 / 冲正操作:处理借据的核销、冲正时,锁定借据,避免和放款、还款等操作冲突。
  3. 资产处置 / 转让:借据涉及资产转让、抵债等操作时,锁定后防止其他流程修改借据状态。
  4. 批量处理:系统夜间批量计算利息、罚息时,锁定相关借据,避免批量处理和实时操作冲突。

关键字段的配合逻辑:

  • lockStatus:核心开关,0 = 未锁、1 = 已锁,是判断能否操作的核心依据;
  • lockSerialNo:锁定流水号,可追溯是谁(哪个业务请求)锁定了借据,便于问题排查;
  • lockTradeCode/lockTradeDescription:记录锁定的业务类型(比如 “放款”“核销”),明确锁定原因;
  • lockVersion:乐观锁,防止多线程同时更新 lockStatus,比如两个请求同时查到 lockStatus=0,更新时通过版本号保证只有一个能成功;
  • lockTime:记录锁定时间,可设置 “锁超时” 逻辑(比如锁定超过 30 分钟自动解锁,防止死锁)。

借据锁 vs 技术锁的区别

类型借据锁(业务逻辑锁)Java 锁(编程语言锁)数据库行锁
作用范围跨 JVM、跨服务(分布式)单个 JVM 进程内数据库事务内
持久化持久化到数据库,重启不丢失内存中,进程重启消失事务结束释放
锁超时可自定义(比如 30 分钟)需手动实现依赖数据库事务超时
适用场景分布式业务流程控制单进程内并发控制数据库操作的原子性

重复加锁问题:

一、为什么会出现重复加锁?

重复加锁的本质是并发场景下,“查状态 - 加锁” 这两步操作不是原子性的,常见成因有 3 类:

1. 最典型的并发竞态问题(核心原因)

线程A:查询lockStatus=0 → 准备更新为1 线程B:查询lockStatus=0 → 准备更新为1 线程A:更新lockStatus=1 ✅ 线程B:更新lockStatus=1 ✅(重复加锁)

这种情况在高并发场景下(比如批量放款)极易发生,只靠lockStatus字段完全无法防范。

2. 乐观锁使用不当

如果lockVersion(乐观锁)的更新逻辑写错,比如:

-- 错误写法:只更新状态,没带版本号条件 UPDATE asset_loan_invoice_lock_info SET lock_status = '1', lock_time = NOW() WHERE loan_id = 'xxx';

-- 正确写法:必须带版本号条件 UPDATE asset_loan_invoice_lock_info SET lock_status = '1', lock_time = NOW(), lock_version = lock_version + 1 WHERE loan_id = 'xxx' AND lock_status = '0' AND lock_version = #{oldVersion};

一旦乐观锁没生效,就会出现重复加锁。

3. 锁超时 / 解锁逻辑漏洞

  • 业务操作超时,但没触发解锁,导致锁一直占用;
  • 解锁时没校验 “锁归属”(比如用lockSerialNo判断是谁加的锁),A 加的锁被 B 解锁,解锁后又被 C 重复加锁;
  • 没有锁超时机制,某笔借据被异常锁定后,一直无法释放,业务方强行绕过锁状态加锁。

二、如何彻底避免重复加锁?

结合金融系统的最佳实践,需要从原子性、唯一性、可追溯、超时控制四个维度设计加锁逻辑:

1. 核心:保证 “查 - 加锁” 的原子性(必做)

最可靠的方式是用数据库的原子操作实现加锁,而不是在代码里分两步查和更。

/**
 * 加锁方法:通过数据库UPDATE的原子性保证不会重复加锁
 * @param loanId 借据ID
 * @param tradeCode 交易码
 * @param serialNo 流水号
 * @return true=加锁成功,false=已被锁定
 */
public boolean lockLoanInvoice(String loanId, String tradeCode, String serialNo) {
    // 1. 先查询当前锁信息(获取旧版本号)
    AssetLoanInvoiceLockInfoDo lockInfo = lockInfoMapper.selectByLoanId(loanId);
    if (lockInfo == null) {
        // 首次加锁,初始化记录
        lockInfo = new AssetLoanInvoiceLockInfoDo();
        lockInfo.setLoanId(loanId);
        lockInfo.setLockStatus("1");
        lockInfo.setLockSerialNo(serialNo);
        lockInfo.setLockTradeCode(tradeCode);
        lockInfo.setLockVersion(1);
        lockInfo.setLockTime(new Date());
        lockInfoMapper.insert(lockInfo);
        return true;
    }
    
    // 2. 原子更新:带版本号+锁状态条件
    int updateCount = lockInfoMapper.updateLockStatus(
        loanId, 
        "0",          // 原状态:未锁定
        lockInfo.getLockVersion(), // 原版本号
        "1",          // 新状态:锁定
        serialNo,
        tradeCode,
        new Date()
    );
    
    // 3. 只有更新成功(影响行数=1),才代表加锁成功
    return updateCount == 1;
}
<update id="updateLockStatus">
    UPDATE asset_loan_invoice_lock_info
    SET lock_status = #{newLockStatus},
        lock_serial_no = #{serialNo},
        lock_trade_code = #{tradeCode},
        lock_time = #{lockTime},
        lock_version = lock_version + 1,
        update_time = NOW()
    WHERE loan_id = #{loanId}
      AND lock_status = #{oldLockStatus}
      AND lock_version = #{oldVersion}
</update>

2. 辅助:增加 “锁归属” 校验(防止误解锁 / 重复加锁)

解锁时必须校验lockSerialNo(只有加锁的那个流水才能解锁)

public boolean unlockLoanInvoice(String loanId, String serialNo) {
    int updateCount = lockInfoMapper.updateUnlockStatus(
        loanId,
        "1",          // 原状态:已锁定
        serialNo,     // 必须是加锁时的流水号
        "0"           // 新状态:未锁定
    );
    return updateCount == 1;
}

3. 兜底:设置锁超时机制(防止死锁)

定时任务扫描超时的锁,自动解锁:

/**
 * 定时任务:解锁超过30分钟的借据锁
 */
@Scheduled(fixedRate = 5 * 60 * 1000) // 每5分钟执行一次
public void unlockTimeoutLock() {
    // 计算超时时间:当前时间 - 30分钟
    Date timeoutTime = new Date(System.currentTimeMillis() - 30 * 60 * 1000);
    // 只解锁超时且未解锁的锁
    lockInfoMapper.unlockTimeoutLock(timeoutTime);
}
<update id="unlockTimeoutLock">
    UPDATE asset_loan_invoice_lock_info
    SET lock_status = '0',
        lock_serial_no = '',
        lock_trade_code = '',
        lock_version = lock_version + 1,
        update_time = NOW()
    WHERE lock_status = '1'
      AND lock_time < #{timeoutTime}
</update>

只要保证 “加锁操作是原子的、解锁操作是可控的、异常情况有兜底的”,重复加锁问题就能规避

总结

到此这篇关于基于Java开发借据锁的文章就介绍到这了,更多相关Java借据锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java原生动态生成验证码

    java原生动态生成验证码

    这篇文章主要为大家详细介绍了java原生动态生成验证码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-10-10
  • FactoryBean的使用及底层工作原理分析

    FactoryBean的使用及底层工作原理分析

    FactoryBean接口解析与使用示例,详解Spring框架中FactoryBean的作用和底层原理,以及如何通过FactoryBean实现对象的动态创建is装配is代理is包装和生命周期管理
    2026-05-05
  • Java毕业设计实战之线上水果超市商城的实现

    Java毕业设计实战之线上水果超市商城的实现

    这是一个使用了java+SSM+springboot+redis开发的网上水果超市商城,是一个毕业设计的实战练习,具有水果超市商城该有的所有功能,感兴趣的朋友快来看看吧
    2022-01-01
  • MyBatis中SQL片段复用使用方法详解

    MyBatis中SQL片段复用使用方法详解

    在使用 MyBatis 进行数据库操作时,常常会遇到一些 SQL 语句的部分内容重复出现的情况,比如多个查询语句都涉及相同的字段列表,这时,MyBatis 的 SQL 片段复用功能就派上用场了,接下小编给大家介绍了MyBatis中SQL片段复用使用方法,需要的朋友可以参考下
    2024-12-12
  • 解决idea爆红 cant resolve symbol String的问题解析

    解决idea爆红 cant resolve symbol String的问题解析

    连着出差几个礼拜没有使用idea开发工具,突然一天打开电脑发现idea里的代码全部爆红,懵逼不如所措,很多朋友建议我按住Alt+回车设置jdk就能解决,但是仍然报错,经过几个小时的倒腾最终解决,遇到此问题的朋友参考下本文吧
    2021-06-06
  • Android/Java中创建类实例的各种模式实例代码

    Android/Java中创建类实例的各种模式实例代码

    这篇文章主要介绍了Android/Java中创建类实例各种模式的相关资料,包括New关键字、静态工厂方法、建造者模式、单例模式、依赖注入、抽象工厂模式、原型模式和Reflection,每个模式有其特点和适用场景,需要的朋友可以参考下
    2025-09-09
  • Java 代码本地设置Hadoop用户名密码的方法

    Java 代码本地设置Hadoop用户名密码的方法

    在Hadoop环境中,通常使用Kerberos进行身份验证,这篇文章主要介绍了Java 代码本地设置Hadoop用户名密码的方法,需要的朋友可以参考下
    2024-08-08
  • Hibernate中实现增删改查的步骤详解

    Hibernate中实现增删改查的步骤详解

    本篇文章主要介绍了Hibernate中实现增删改查的步骤与方法,具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • 快速解决 IDEA 报错: “java 找不到符号“(“cannot find symbol“)

    快速解决 IDEA 报错: “java 找不到符号“(“cannot find symbol“)

    文章详细讲解了在IntelliJIDEA中解决“找不到符号”错误的方法,包括检查导入语句、拼写错误、类路径设置、文件编译状态、JDK配置以及IDE配置问题,通过具体示例代码,展示了如何从错误代码到解决步骤,感兴趣的朋友一起看看吧
    2025-03-03
  • SpringBoot 单元测试实战(Mockito,MockBean)

    SpringBoot 单元测试实战(Mockito,MockBean)

    这篇文章主要介绍了SpringBoot 单元测试实战(Mockito,MockBean),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09

最新评论