深入讲解基于Java开发的借据锁
作者:LWH!
基于 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;
}| 字段名 | 类型 | 对应数据库列 | 含义 |
|---|---|---|---|
serialVersionUID | long | - | 序列化版本号,保证序列化 / 反序列化的兼容性 |
lockStatus | String | lock_status | 借据锁状态,枚举值(0 - 未锁定,1 - 已锁定) |
lockSerialNo | String | lock_serial_no | 锁定操作的流水号,用于追踪锁定行为 |
lockTradeCode | String | lock_trade_code | 锁定交易的编码,标识具体的锁定业务类型 |
lockTradeDescription | String | lock_trade_description | 锁定交易的描述信息,补充说明锁定原因 / 场景 |
lockVersion | Integer | lock_version | 乐观锁版本号,用于解决并发更新冲突 |
createTime | Date | create_time | 记录创建时间 |
updateTime | Date | update_time | 记录更新时间 |
lockTime | Date | lock_time | 借据锁定的具体时间 |
借据锁的核心本质
这个 “借据锁”并非编程语言层面的锁(如 Java 的 synchronized、ReentrantLock),也不是数据库的行锁 / 表锁,而是一种业务层面的逻辑锁(也叫状态锁 / 软锁),本质是通过数据库记录的状态标识来控制借据的操作权限,属于金融系统中典型的 “乐观锁 + 业务状态” 组合设计。
简单来说:它是给每一笔借据(贷款凭证)加的 “业务标记”,标记该借据是否被占用,防止同一笔借据在不同业务流程中被重复操作(比如重复放款、重复核销)。
借据锁的核心作用场景
以金融资产系统为例,借据锁主要用于这些高并发、高风险的场景:
- 放款操作:发起放款时,先锁定借据,防止同一笔借据被多个放款请求同时处理,导致重复放款;放款完成后解锁。
- 核销 / 冲正操作:处理借据的核销、冲正时,锁定借据,避免和放款、还款等操作冲突。
- 资产处置 / 转让:借据涉及资产转让、抵债等操作时,锁定后防止其他流程修改借据状态。
- 批量处理:系统夜间批量计算利息、罚息时,锁定相关借据,避免批量处理和实时操作冲突。
关键字段的配合逻辑:
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借据锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
